diff --git a/.asf.yaml b/.asf.yaml index 9aa07d545a..a2e0b4aa67 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -33,16 +33,10 @@ github: protected_branches: master: required_status_checks: - # Keep workflows providing these checks unfiltered; path skips can leave required checks pending. # strict means "Require PR to be up-to-date before merging". (enable when branch unstable) strict: false - # contexts are the names of checks that must pass (now only enable the basic check) - contexts: - - Analyze (java) - - CodeQL - - check-license - - build-server (memory, 11) - - build-commons (11) + # CI remains visible but advisory while shared runner queues are constrained. + contexts: [] required_pull_request_reviews: dismiss_stale_reviews: true require_code_owner_reviews: false diff --git a/.editorconfig b/.editorconfig index c64c7bae2b..7e5f93a2f5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -36,6 +36,7 @@ continuation_indent_size = 8 [*.md] max_line_length = off +ij_wrap_on_typing = false [*.java] ij_java_doc_add_p_tag_on_empty_lines = true diff --git a/.gitattributes b/.gitattributes index 216223172b..bfadaca912 100755 --- a/.gitattributes +++ b/.gitattributes @@ -15,5 +15,8 @@ install-dist/scripts/ export-ignore hugegraph-commons/hugegraph-dist/ export-ignore docker/ export-ignore +# Documentation images are not needed in source releases. +/docs/images/ export-ignore + # Keep the generated golden corpus byte-identical across checkouts hugegraph-commons/hugegraph-common/src/test/resources/longencoding-golden.txt text eol=lf diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000000..b359fe93cc --- /dev/null +++ b/.github/codecov.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. + +# Keep coverage from unexecuted test groups in affected-input CI reports. +flag_management: + default_rules: + carryforward: true + +comment: + show_carryforward_flags: true diff --git a/.github/images/architecture.png b/.github/images/architecture.png new file mode 100644 index 0000000000..d14f6838a8 Binary files /dev/null and b/.github/images/architecture.png differ diff --git a/.github/outdated/APACHE_PROPOSAL.md b/.github/outdated/APACHE_PROPOSAL.md index bc3bb84044..b26681d2c1 100644 --- a/.github/outdated/APACHE_PROPOSAL.md +++ b/.github/outdated/APACHE_PROPOSAL.md @@ -34,7 +34,7 @@ Although most of the main development of HugeGraph has been completed, there are #### Meritocracy -HugeGraph was incubated at Baidu in 2016 and open-sourced on [GitHub](https://github.com/hugegraph/hugegraph) in 2018. The project (including sub-projects) now has 30+ contributors from many companies. Some of these contributors become committers, and the project has hundreds of known users around the world. We will follow Apache's Meritocracy way to re-organize the community roles. We have set up the PPMC Team and Committer Team. Of course, contributions are welcomed and highly valued. New contributors are guided and reviewed by existing PMC members. When an active contributor has submitted enough good patches, PMC will start a vote to promote him/her to become a member of the Committer Team or PMC Team. +HugeGraph was incubated at Baidu in 2016 and open-sourced on [GitHub](https://github.com/apache/hugegraph) in 2018. The project (including sub-projects) now has 30+ contributors from many companies. Some of these contributors become committers, and the project has hundreds of known users around the world. We will follow Apache's Meritocracy way to re-organize the community roles. We have set up the PPMC Team and Committer Team. Of course, contributions are welcomed and highly valued. New contributors are guided and reviewed by existing PMC members. When an active contributor has submitted enough good patches, PMC will start a vote to promote him/her to become a member of the Committer Team or PMC Team. #### Community @@ -52,7 +52,7 @@ HugeGraph implements the API of [Apache TinkerPop](https://tinkerpop.apache.org/ #### Project Name -We have checked and believe the name is [suitable](https://github.com/hugegraph/hugegraph/issues/1646) and the project has legal permission to continue using its current name. There are no other projects found using this name through Google Search. +We have checked and believe the name is [suitable](https://github.com/apache/hugegraph/issues/1646) and the project has legal permission to continue using its current name. There are no other projects found using this name through Google Search. #### Relationship with Titan/Janus Graph @@ -94,10 +94,10 @@ HugeGraph documentation is provided on https://hugegraph.github.io/hugegraph-doc This project consists of 2 core sub-projects and 2 other sub-projects, all of which are hosted by [GitHub hugegraph organization](https://github.com/orgs/hugegraph/repositories) since 2018. The codes are already under Apache License Version 2.0. The git address of sub-project repositories are as follows: -1. The graph database repository `hugegraph`, core sub-project, including graph server, graph engine, and graph storage: https://github.com/hugegraph/hugegraph -2. The graph computing repository `hugegraph-computer`, core sub-project, including graph computing and graph algorithms: https://github.com/hugegraph/hugegraph-computer -3. The common functions repository `hugegraph-commons`: https://github.com/hugegraph/hugegraph-commons -4. The ecosystem repository `hugegraph-toolchain`, including `hugegraph-client`, `hugegraph-loader`, `hugegraph-tools`, `hugegraph-hubble`, `hugegraph-test`, `hugegraph-doc`: https://github.com/hugegraph/hugegraph-toolchain +1. The graph database repository `hugegraph`, core sub-project, including graph server, graph engine, and graph storage: https://github.com/apache/hugegraph +2. The graph computing repository `hugegraph-computer`, core sub-project, including graph computing and graph algorithms: https://github.com/apache/hugegraph-computer +3. The common functions repository `hugegraph-commons`: https://github.com/apache/hugegraph/tree/master/hugegraph-commons +4. The ecosystem repository `hugegraph-toolchain`, including `hugegraph-client`, `hugegraph-loader`, `hugegraph-tools`, `hugegraph-hubble`, `hugegraph-test`, `hugegraph-doc`: https://github.com/apache/hugegraph-toolchain ### Source and Intellectual Property Submission Plan @@ -107,7 +107,7 @@ The code is currently under Apache License Version 2.0, and it was verified ther As all dependencies are managed by Apache Maven, none of the external libraries need to be packaged in a source distribution. All dependencies have Apache compatible licenses except for 4 dependencies: MySQL Connector (GPL), word (GPL), JBoss Logging 3 (LGPL), and jnr-posix (LGPL+GPL), we will remove these dependencies in the future. -HugeGraph has the following external [dependencies](https://github.com/hugegraph/hugegraph/issues/1632): +HugeGraph has the following external [dependencies](https://github.com/apache/hugegraph/issues/1632): - Apache License @@ -263,7 +263,7 @@ None - hugegraph-dev: [dev@hugegraph.incubator.apache.org](mailto:dev@hugegraph.incubator.apache.org) for development and users discussions. - hugegraph-private: [private@hugegraph.incubator.apache.org](mailto:private@hugegraph.incubator.apache.org) for PPMC discussions. -- hugegraph-commits: [commits@hugegraph.incubator.apache.org](mailto:commits@hugegraph.incubator.apache.org) for commits / [pull requests](https://github.com/hugegraph/hugegraph/pulls) and other notifications like code review comments. +- hugegraph-commits: [commits@hugegraph.incubator.apache.org](mailto:commits@hugegraph.incubator.apache.org) for commits / [pull requests](https://github.com/apache/hugegraph/pulls) and other notifications like code review comments. #### Subversion Directory @@ -271,18 +271,18 @@ None #### Git Repositories -1. `hugegraph`: https://github.com/hugegraph/hugegraph.git -2. `hugegraph-computer`: https://github.com/hugegraph/hugegraph-computer.git -3. `hugegraph-commons`: https://github.com/hugegraph/hugegraph-commons.git -4. `hugegraph-toolchain`: https://github.com/hugegraph/hugegraph-toolchain.git +1. `hugegraph`: https://github.com/apache/hugegraph.git +2. `hugegraph-computer`: https://github.com/apache/hugegraph-computer.git +3. `hugegraph-commons`: https://github.com/apache/hugegraph.git +4. `hugegraph-toolchain`: https://github.com/apache/hugegraph-toolchain.git #### Issue Tracking -The community would like to continue using [GitHub Issues](https://github.com/hugegraph/hugegraph/issues) (but will move to github.com/apache/). +The community would like to continue using [GitHub Issues](https://github.com/apache/hugegraph/issues) (but will move to github.com/apache/). #### Other Resources -- The community has already chosen [GitHub actions](https://github.com/hugegraph/hugegraph/actions) as continuous integration tools. +- The community has already chosen [GitHub actions](https://github.com/apache/hugegraph/actions) as continuous integration tools. - The community has already used [codecov](https://github.com/marketplace/codecov) to check code coverage. - The community has already used [mvn repository](https://mvnrepository.com/search?q=hugegraph) as binary package release platform. diff --git a/.github/outdated/cla.yml b/.github/outdated/cla.yml index a182b9515b..d5779dcb37 100644 --- a/.github/outdated/cla.yml +++ b/.github/outdated/cla.yml @@ -18,8 +18,8 @@ jobs: PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} with: path-to-signatures: '.github/cla-signatures/cla.json' - path-to-document: 'https://github.com/hugegraph/hugegraph-doc/blob/master/CLA.md' # e.g. a CLA or a DCO document - custom-allsigned-prcomment: '**Good**! All Contributors have signed the [CLA](https://github.com/hugegraph/hugegraph-doc/blob/master/CLA.md).' + path-to-document: 'https://www.apache.org/licenses/contributor-agreements.html' # e.g. a CLA or a DCO document + custom-allsigned-prcomment: '**Good**! All Contributors have signed the [CLA](https://www.apache.org/licenses/contributor-agreements.html).' # branch should not be protected branch: 'cla-assist' allowlist: bot* diff --git a/.github/scripts/check-docker-images.sh b/.github/scripts/check-docker-images.sh new file mode 100644 index 0000000000..41c8911246 --- /dev/null +++ b/.github/scripts/check-docker-images.sh @@ -0,0 +1,103 @@ +#!/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. +# + +set -euo pipefail +: "${IMAGE_TAG:?set the unique CI image tag}" +for pair in \ + hugegraph-pd/Dockerfile=hugegraph/pd \ + hugegraph-store/Dockerfile=hugegraph/store \ + hugegraph-server/Dockerfile=hugegraph/hugegraph \ + hugegraph-server/Dockerfile-hstore=hugegraph/server; do + ( + dockerfile="${pair%%=*}" + IMAGE_ID=$(docker image inspect --format '{{.Id}}' "${pair#*=}:${IMAGE_TAG}") + echo "Checking ${pair#*=}:${IMAGE_TAG} ($IMAGE_ID)" + HC=$(docker image inspect --format '{{json .Config.Healthcheck}}' "$IMAGE_ID") + [[ "$HC" != null ]] || { echo "Missing HEALTHCHECK: $dockerfile"; exit 1; } + + # Helm reuses these images for tool containers instead of pulling curlimages/curl. + # Check the same non-root/read-only tool environment as the Server test hook. + if [[ "$dockerfile" == hugegraph-store/* || "$dockerfile" == hugegraph-server/* ]]; then + HELM_TOOLS=(curl tr date sleep) + if [[ "$dockerfile" == hugegraph-server/* ]]; then + HELM_TOOLS+=(getent base64 awk grep sort tail) + fi + docker run --rm --pull never --read-only --user 100:101 --entrypoint sh "$IMAGE_ID" -c ' + for tool in "$@"; do + command -v "$tool" >/dev/null || { + echo "Missing Helm tool: $tool" >&2 + exit 1 + } + case "$tool" in + getent) getent ahosts localhost >/dev/null ;; + base64) test "$(printf Helm | base64 | tr -d "\n")" = SGVsbQ== ;; + esac + done + curl --version >/dev/null + ' helm-tools "${HELM_TOOLS[@]}" + fi + + JAVA_VERSION=$(docker run --rm --pull never --entrypoint java "$IMAGE_ID" -version 2>&1) + echo "$JAVA_VERSION" + grep -Eq 'version "17\.' <<< "$JAVA_VERSION" || { + echo "ERROR: expected a Java 17 runtime in $dockerfile" + exit 1 + } + + if [[ "$dockerfile" == hugegraph-server/* ]]; then + STATE=$(docker run --rm --pull never "$IMAGE_ID" bash -c \ + 'source /hugegraph-server/bin/util.sh && port_listen_state 8080') + echo "port_listen_state 8080 -> $STATE" + # Require a usable answer from the socket-table tool in the image. + [[ "$STATE" == "free" || "$STATE" == "busy" ]] || { + echo "ERROR: no usable socket-table tool (ss/netstat) in $dockerfile" + exit 1 + } + fi + + if [[ "$dockerfile" == hugegraph-server/* ]]; then + CHECK_DIR=$(mktemp -d) + trap 'rm -rf "$CHECK_DIR"' EXIT + docker run --rm --pull never --entrypoint bash \ + -v "$CHECK_DIR:/check" "$IMAGE_ID" -c \ + 'cp /hugegraph-server/lib/hugegraph-api-*.jar \ + /hugegraph-server/lib/hugegraph-common-*.jar /check/' + + API_JAR=$(find "$CHECK_DIR" -name 'hugegraph-api-*.jar' -print -quit) + COMMON_JAR=$(find "$CHECK_DIR" -name 'hugegraph-common-*.jar' -print -quit) + EXPECTED_MANIFEST=$(sed -n \ + 's|.*\([^<]*\).*|\1|p' \ + hugegraph-server/hugegraph-api/pom.xml) + ACTUAL_MANIFEST=$(unzip -p "$API_JAR" META-INF/MANIFEST.MF | + sed -n 's/^Implementation-Version: *//p' | tr -d '\r') + EXPECTED_PROPERTY=$(sed -n 's/^ApiVersion=//p' \ + hugegraph-commons/hugegraph-common/src/main/resources/version.properties) + ACTUAL_PROPERTY=$(unzip -p "$COMMON_JAR" version.properties | + sed -n 's/^ApiVersion=//p' | tr -d '\r') + + [[ "$ACTUAL_MANIFEST" == "$EXPECTED_MANIFEST" ]] || { + echo "ERROR: API manifest is $ACTUAL_MANIFEST; expected $EXPECTED_MANIFEST" + exit 1 + } + [[ "$ACTUAL_PROPERTY" == "$EXPECTED_PROPERTY" ]] || { + echo "ERROR: API property is $ACTUAL_PROPERTY; expected $EXPECTED_PROPERTY" + exit 1 + } + fi + ) +done diff --git a/.github/scripts/ci-policy.py b/.github/scripts/ci-policy.py new file mode 100644 index 0000000000..6241fffda4 --- /dev/null +++ b/.github/scripts/ci-policy.py @@ -0,0 +1,453 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Select affected CI checks from the current event and cumulative PR diff.""" + +import argparse +import json +import os +from pathlib import Path +import subprocess + +MAINLINE = {"server", "commons", "struct", "pd", "store", "hstore", "cluster", "docker", "helm", + "dependency_license", "macos", "security", "distributed_smoke"} +MODULES = { + "server": sorted(MAINLINE | {"macos_full", "hbase", "riscv64"}), + "toolchain": ["client", "loader", "tools", "spark", "hubble", "go"], +} +PREFIXES = { + "server": {"hugegraph-server/": "server", "hugegraph-commons/": "commons", + "hugegraph-struct/": "struct", "hugegraph-pd/": "pd", "hugegraph-store/": "store", + "hugegraph-cluster-test/": "cluster", "docker/": "docker", "helm/": "helm"}, + "toolchain": {"hugegraph-client/": "client", "hugegraph-client-go/": "go", + "hugegraph-loader/": "loader", "hugegraph-tools/": "tools", + "hugegraph-spark-connector/": "spark", "hugegraph-hubble/": "hubble"}, +} +WORKFLOWS = { + "server": {"server-ci.yml": MAINLINE, "commons-ci.yml": {"commons"}, + "pd-store-ci.yml": {"struct", "pd", "store", "hstore"}, "cluster-test-ci.yml": {"cluster"}, + "docker-build-ci.yml": {"docker"}, "helm-chart-ci.yml": {"helm"}, + "codeql-analysis.yml": {"security"}, "server-memory-ci.yml": {"server"}, + "server-tests.yml": {"server", "hbase"}, + "server-compatibility-ci.yml": {"macos", "macos_full", "hbase", "riscv64"}, + "riscv64-ci.yml": {"riscv64"}, "check-dependencies.yml": {"dependency_license"}}, + "toolchain": {"client-ci.yml": {"client"}, "client-go-ci.yml": {"go"}, + "loader-ci.yml": {"loader"}, "tools-ci.yml": {"tools"}, + "spark-connector-ci.yml": {"spark"}, "hubble-ci.yml": {"hubble"}, + "codeql-analysis.yml": set()}, +} +# Shared runtime consumers; ordinary Server edits do not fan out to PD/Store. +DEPENDENTS = { + "server": {"commons": {"server", "pd", "store", "hstore", "cluster"}, + "struct": {"server", "pd", "store", "hstore", "cluster"}, + "pd": {"store", "hstore", "cluster"}, "store": {"hstore", "cluster"}, "hstore": {"cluster"}}, + "toolchain": {"client": {"loader", "tools", "spark", "hubble"}, "loader": {"hubble"}}, +} +MAINTENANCE_INPUTS = { + ".asf.yaml": set(), ".editorconfig": set(), ".licenserc.yaml": set(), + ".github/PULL_REQUEST_TEMPLATE.md": set(), ".github/dependabot.yml": set(), + ".github/workflows/.java-version": MAINLINE | {"macos_full", "hbase", "riscv64"}, + ".github/scripts/check-docker-images.sh": {"docker"}, + ".github/scripts/docker-deployment.py": {"docker"}, + ".github/scripts/test_docker_deployment.py": {"docker"}, +} +PRESETS = { + "mainline": MAINLINE, + "distributed": {"struct", "pd", "store", "hstore", "cluster", "distributed_smoke"}, + "macos-arm": {"macos", "macos_full"}, "hbase": {"hbase"}, + "riscv64": {"riscv64"}, "codeql": {"security"}, "full": set(MODULES["server"]), +} +SERVER_CORE = "hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/" +SERVER_API = "hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/" +SECURITY_INPUTS = { + SERVER_CORE + "backend/cache/CachedSchemaTransaction.java", + SERVER_CORE + "backend/cache/CachedSchemaTransactionV2.java", +} +DISTRIBUTED_SERVER_INPUTS = ( + "hugegraph-server/hugegraph-hstore/", SERVER_CORE + "backend/", SERVER_CORE + "meta/", + SERVER_CORE + "space/", SERVER_CORE + "task/", SERVER_CORE + "masterelection/", SERVER_CORE + "HugeGraph", + SERVER_CORE + "StandardHugeGraph", SERVER_API + "core/", SERVER_API + "rpc/", + SERVER_API + "api/raft/", SERVER_API + "api/space/", + "hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/store/", + "hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/meta/", +) +# Test/resources/templates can contain executable examples and runtime fixtures. +RUNTIME_PARTS = {"src", "resources", "fixtures", "templates", "static", "bin", "conf", "configs"} +# These build files declare native JNI dependencies/classifiers or the RISC-V protobuf profile. +PLATFORM_POMS = {"pom.xml", "hugegraph-server/pom.xml", "hugegraph-server/hugegraph-core/pom.xml", + "hugegraph-server/hugegraph-rocksdb/pom.xml", "hugegraph-server/hugegraph-dist/pom.xml", + "hugegraph-pd/hg-pd-grpc/pom.xml"} +RISCV_PROTO_ROOTS = ("hugegraph-server/hugegraph-core/", "hugegraph-pd/hg-pd-grpc/") +TRAVIS = "hugegraph-server/hugegraph-dist/src/assembly/travis/" +# Contract tests follow their executing suites, without runtime consumer fan-out. +CONTRACT_SUITES = { + TRAVIS + "test-check-java-runtime-logs.sh": {"struct"}, + TRAVIS + "check-java-runtime-logs.sh": {"server", "commons", "struct", "pd", "store", "hstore", "cluster", + "macos", "macos_full", "hbase", "riscv64"}, + TRAVIS + "test-stop-hugegraph-store.sh": {"struct"}, + TRAVIS + "test-check-jacoco-report.sh": {"struct"}, + TRAVIS + "test-codecov-upload-config.sh": {"struct"}, + TRAVIS + "test-riscv64-java-runtime.sh": {"server", "riscv64"}, +} + + +def git(*args): + return subprocess.check_output(["git", *args], text=True).strip() + + +def api(path): + result = subprocess.run(["gh", "api", "--method", "GET", path], check=True, capture_output=True, timeout=30) + return json.loads(result.stdout) + + +def documentation(path, mode="100644"): + """Only plain prose/static assets outside runtime and fixture directories.""" + if mode != "100644": + return False + p = Path(path) + if RUNTIME_PARTS.intersection(p.parts[:-1]): + return False + if p.suffix.lower() in {".md", ".rst"}: + return True + if len(p.parts) == 1 and (p.suffix.lower() == ".txt" or p.name in {"LICENSE", "NOTICE"}): + return True + return "docs" in p.parts[:-1] and p.suffix.lower() in {".md", ".rst", ".txt", ".png", ".jpg", ".jpeg", ".svg"} + + +def unsafe_documentation(ref, paths): + entries = subprocess.check_output(["git", "ls-tree", "-rz", "--full-tree", ref]).split(b"\0") + for entry in entries: + if not entry: + continue + metadata, name = entry.split(b"\t", 1) + path = name.decode("utf-8", "surrogateescape") + if path in paths and documentation(path) and metadata.split(b" ", 1)[0] != b"100644": + return True + return False + + +def select(project, paths): + selected = set() + contracts = set() + for path in paths: + if documentation(path): + continue + p = Path(path) + if project == "server": + if path in CONTRACT_SUITES: + contracts.update(CONTRACT_SUITES[path]) + continue + if path in MAINTENANCE_INPUTS: + selected.update(MAINTENANCE_INPUTS[path]) + continue + if p.name == "pom.xml": + selected.update({"dependency_license", "security"}) + if path in PLATFORM_POMS: + selected.update({"macos", "macos_full", "riscv64"}) + if path.startswith("install-dist/"): + selected.add("dependency_license") + continue + if (path in SECURITY_INPUTS + or {"auth", "security", "permission", "permissions"}.intersection(p.parts) + or p.stem.startswith(("Authentication", "Authorization")) + or p.stem.endswith(("Authentication", "Authenticator", "Permission", "WithAuth")) + or p.name in {"AuthOptions.java", "OkHttpBasicAuthInterceptor.java", "java-security.properties", + "test-java-security-properties.sh", "enable-auth.sh"}): + selected.add("security") + if path.startswith(".github/workflows/") and p.name in WORKFLOWS[project]: + selected.update(WORKFLOWS[project][p.name]) + continue + if path == TRAVIS + "install-backend.sh": + selected.update({"hbase", "hstore"}) + if path.startswith("docker/hbase/"): + selected.update({"docker", "hbase"}) + continue + if path.startswith("hugegraph-server/hugegraph-hbase/") or "hbase" in p.name.lower(): + selected.add("hbase") + continue + if "riscv" in path.lower(): + selected.add("riscv64") + continue + if path.startswith(DISTRIBUTED_SERVER_INPUTS) or path.endswith(".proto"): + selected.update({"hstore", "cluster", "distributed_smoke"}) + if path.endswith(".proto") and path.startswith(RISCV_PROTO_ROOTS): + selected.add("riscv64") + if path.startswith("hugegraph-server/hugegraph-dist/docker/"): + selected.add("docker") + continue + if path.startswith("hugegraph-server/hugegraph-dist/"): + selected.update({"server", "docker", "macos", "macos_full"}) + if (path.startswith("hugegraph-server/hugegraph-dist/src/assembly/static/bin/") + or "native-runtime" in p.name or "rocksdb-jni" in p.name + or p.name == "check-rocksdb-only-dist.sh"): + selected.add("riscv64") + if any(word in path for word in ("pd", "store", "distributed")): + selected.update({"hstore", "cluster", "distributed_smoke"}) + continue + if path.startswith("hugegraph-server/hugegraph-rocksdb/"): + selected.update({"server", "macos", "macos_full"}) + continue + if path in {"hugegraph-server/pom.xml", "hugegraph-server/hugegraph-api/pom.xml"}: + selected.add("docker") + if path.startswith(("hugegraph-pd/hg-pd-dist/", "hugegraph-store/hg-store-dist/")): + selected.add("docker") + if path == "hugegraph-commons/hugegraph-common/src/main/resources/version.properties": + selected.add("docker") + elif path.endswith(".proto"): + selected.update(MODULES[project]) + continue + elif path.startswith(".github/workflows/") and p.name in WORKFLOWS[project]: + selected.update(WORKFLOWS[project][p.name]) + continue + elif path.startswith("hugegraph-client/assembly/travis/"): + selected.update({"client", "go"}) + continue + module = next((value for prefix, value in PREFIXES[project].items() if path.startswith(prefix)), None) + if module is None: + selected.update(MAINLINE if project == "server" else MODULES[project]) + else: + selected.add(module) + changed = True + while changed: + before = set(selected) + for module in before: + selected.update(DEPENDENTS[project].get(module, set())) + changed = before != selected + if project == "server": + if "server" in selected: + selected.add("macos") + if selected.intersection({"pd", "store", "hstore", "struct", "cluster"}): + selected.add("distributed_smoke") + selected.update(contracts) + return selected + + +def event_selection(project, paths, event, event_name): + """Presets and post-merge smoke share the same affected-input classifier.""" + if project != "server": + return select(project, paths) + if event_name == "schedule": + return set(PRESETS["full"]) + if event_name == "workflow_dispatch": + preset = event.get("inputs", {}).get("test-set", "mainline") + if preset not in PRESETS: + raise ValueError("unknown test-set: " + str(preset)) + return set(PRESETS[preset]) + if event_name == "push" and event.get("ref") == "refs/heads/master": + # Test contracts do not change deployed runtime inputs. + selected = select(project, [path for path in paths if path not in CONTRACT_SUITES]) + smoke = selected.intersection({"docker", "helm", "dependency_license", "macos", "distributed_smoke"}) + if selected.intersection({"server", "commons", "struct", "pd", "store", "hstore", "cluster"}): + smoke.add("distributed_smoke") + return smoke + return select(project, paths) + + +def suites(project, selected): + if project == "toolchain": + return sorted(selected) + result = selected.intersection({"commons", "cluster", "docker", "helm", "dependency_license", "macos", + "hbase", "riscv64", "security", "distributed_smoke"}) + if "server" in selected: + result.update({"server_memory", "server_rocksdb"}) + if selected.intersection({"pd", "store", "hstore", "struct"}): + result.add("pd_store") + return sorted(result) + + +class StaleInputError(RuntimeError): + """The checkout no longer represents the current PR inputs.""" + + +def require_current_pr(plan, fetch): + if not plan["pr"]: + return + live = fetch(f"repos/{plan['repository']}/pulls/{plan['pr']}") + if (live.get("state") != "open" or live["head"]["sha"] != plan["head"] + or live["head"]["repo"]["full_name"] != plan["source"] + or live["head"]["ref"] != plan["branch"] + or live["base"]["repo"]["full_name"] != plan["repository"]): + raise StaleInputError("PR inputs changed; refresh the branch and start a new PR run") + + +def create_plan(project, event, repository, fetch=api, event_name=None): + event_name = event_name or os.environ.get("GITHUB_EVENT_NAME") or ( + "pull_request" if "pull_request" in event else "push") + paths = [] + plan = {"schema": 1, "project": project, "repository": repository, "pr": 0, + "source": repository, "branch": "", "base": "", "head": git("rev-parse", "HEAD"), + "reason": "affected inputs", "testedMergeSHA": git("rev-parse", "HEAD"), "event": event_name} + try: + pr = event.get("pull_request") + if "pull_request" in event: + if not isinstance(pr, dict) or type(pr.get("number")) is not int or pr["number"] <= 0: + raise StaleInputError("PR event lacks a valid number") + plan["pr"] = pr["number"] + try: + plan.update(source=pr["head"]["repo"]["full_name"], base=pr["base"]["sha"], + head=pr["head"]["sha"], branch=pr["head"]["ref"]) + except (KeyError, TypeError) as error: + raise StaleInputError("PR event lacks required input identity") from error + if not all(isinstance(plan[key], str) and plan[key] for key in ("source", "base", "head", "branch")): + raise StaleInputError("PR event has an empty input identity") + parents = git("show", "-s", "--format=%P", plan["testedMergeSHA"]).split() + if (plan["testedMergeSHA"] != os.environ.get("GITHUB_SHA") + or len(parents) != 2 or parents[1] != plan["head"]): + raise StaleInputError("checkout is not the event PR merge; start a new PR run") + plan["base"] = parents[0] + require_current_pr(plan, fetch) + ancestor = git("merge-base", plan["base"], plan["head"]) + paths = git("diff", "--no-renames", "--name-only", ancestor, plan["head"]).splitlines() + elif event_name == "push": + plan["base"] = event.get("before", "") + plan["branch"] = event.get("ref", "").removeprefix("refs/heads/") + paths = git("diff", "--no-renames", "--name-only", plan["base"], plan["head"]).splitlines() + selected = event_selection(project, paths, event, event_name) + if paths and (unsafe_documentation(plan["base"], paths) + or unsafe_documentation(plan["testedMergeSHA"], paths)): + selected = event_selection(project, ["unknown"], event, event_name) + plan["reason"] = "non-regular documentation input: conservative mainline coverage" + if event_name in {"schedule", "workflow_dispatch"}: + plan["reason"] = "weekly full coverage" if event_name == "schedule" else "manual " + event.get( + "inputs", {}).get("test-set", "mainline") + except (subprocess.SubprocessError, OSError, ValueError, KeyError, TypeError, AttributeError): + selected = event_selection(project, ["unknown"], {"ref": event.get("ref")}, + "push" if event_name == "push" else "pull_request") + plan["reason"] = "verification unavailable: conservative mainline coverage" + if not selected: + plan["reason"] = "plain prose documentation or maintenance inputs; tests unaffected" + plan["selected"] = sorted(selected) + plan["expected"] = suites(project, selected) + for module in MODULES[project]: + plan[module] = module in selected + plan["pd_store"] = any(plan.get(m, False) for m in ["pd", "store", "hstore", "struct"]) + plan["needsFixture"] = project == "toolchain" and bool(selected) + plan["fixture"] = plan["needsFixture"] + if project == "toolchain": + plan["security"] = bool(selected) or any(p.startswith(".github/workflows/codeql") for p in paths) + plan["required"] = [] + plan["security_languages"] = json.dumps(["java"]) + plan["changedPaths"] = paths + plan["selectionReasons"] = { + suite: [path for path in paths if suite in suites(project, event_selection(project, [path], event, event_name))] + for suite in plan["expected"] + } + if os.environ.get("GITHUB_STEP_SUMMARY"): + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as out: + out.write("## Selected CI checks\n" + selection_table(plan) + "\n") + return plan + +def selection_reason(plan, suite): + paths = plan.get("selectionReasons", {}).get(suite, []) + if not paths: + selected = suite in plan["expected"] or suite in plan["selected"] + return plan.get("reason", "affected inputs") if selected else "no affected inputs" + # Bound the summary size; paths in the plan artifact retain the full explanation. + shown = ["`" + path.replace("`", "\\`").replace("|", "\\|").replace("\n", " ") + "`" + for path in paths[:3]] + suffix = f" (+{len(paths) - 3} more)" if len(paths) > 3 else "" + return ", ".join(shown) + suffix + + +def selection_table(plan): + summary = "| Check | Selected | Merge requirement | Changed inputs |\n| --- | --- | --- | --- |\n" + checks = suites(plan["project"], set(MODULES[plan["project"]])) + for suite in checks: + chosen = suite in plan["expected"] + role = "advisory" + summary += f"| {suite} | {'yes' if chosen else 'no'} | {role} | {selection_reason(plan, suite)} |\n" + return summary + + +class GateFailure(ValueError): + def __init__(self, message, report): + super().__init__(message) + self.report = report + + +def gate(plan, results, fetch=None): + """Keep the Toolchain result summary and current-PR gate contract.""" + expected = list(plan["expected"]) + report = {key: plan[key] for key in ["schema", "repository", "project", "pr", "source", "branch", "base", + "head", "testedMergeSHA", "selected"]} + report["selectionReasons"] = plan.get("selectionReasons", {}) + report.update(runID=int(os.environ.get("GITHUB_RUN_ID", "0")), + runAttempt=int(os.environ.get("GITHUB_RUN_ATTEMPT", "1")), executed=expected, + results={suite: results.get(suite, {}).get("result", "missing") for suite in expected}) + errors = [] + if results.get("plan", {}).get("result") != "success": + errors.append("planner did not succeed") + errors.extend("selected suite did not succeed: " + suite for suite in expected + if report["results"][suite] != "success") + if plan["project"] == "toolchain": + if set(expected).intersection({"client", "loader", "tools", "spark", "go"}): + if results.get("fixture", {}).get("result") != "success": + errors.append("selected tests lack successful fixture") + if "hubble" in expected and results.get("hubble-fixture", {}).get("result") != "success": + errors.append("selected Hubble tests lack successful baseline fixture") + summary = "Selection: " + plan.get("reason", "affected inputs") + "\n" + summary += "Tested base: `" + plan["base"] + "`; merge: `" + plan["testedMergeSHA"] + "`\n\n" + summary += "| Check | Selected | Merge requirement | Changed inputs | Result |\n| --- | --- | --- | --- | --- |\n" + for suite in suites(plan["project"], set(MODULES[plan["project"]])): + chosen = suite in expected + status = report["results"].get(suite, "not selected") + summary += f"| {suite} | {'yes' if chosen else 'no'} | advisory | {selection_reason(plan, suite)} | {status} |\n" + if os.environ.get("GITHUB_STEP_SUMMARY"): + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as out: + out.write("## Affected module results\n" + summary + "\n") + print(summary) + if errors: + raise GateFailure("; ".join(errors), report) + if plan["project"] == "toolchain": + require_current_pr(plan, fetch or api) + return report + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + p = sub.add_parser("plan") + p.add_argument("--project", choices=MODULES, required=True) + p.add_argument("--event-path", required=True) + p.add_argument("--repository", required=True) + p.add_argument("--output", required=True) + p.add_argument("--event-name") + p = sub.add_parser("gate") + p.add_argument("--plan", required=True) + p.add_argument("--results", required=True) + p.add_argument("--mode", choices=["all"], default="all") + p.add_argument("--output", required=True) + args = parser.parse_args() + if args.command == "plan": + value = create_plan(args.project, json.loads(Path(args.event_path).read_text()), args.repository, + event_name=args.event_name) + if os.environ.get("GITHUB_OUTPUT"): + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + for key, item in value.items(): + if isinstance(item, bool): + output.write(f"{key}={str(item).lower()}\n") + output.write(f"security_languages={value['security_languages']}\nplan_file={args.output}\n") + else: + try: + value = gate(json.loads(Path(args.plan).read_text()), json.loads(Path(args.results).read_text())) + except GateFailure as error: + Path(args.output).write_text(json.dumps(error.report, indent=2) + "\n") + raise + Path(args.output).write_text(json.dumps(value, indent=2) + "\n") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/docker-deployment.py b/.github/scripts/docker-deployment.py new file mode 100644 index 0000000000..12b253ec2f --- /dev/null +++ b/.github/scripts/docker-deployment.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Start the four locally built PR images using the shipped Compose files.""" + +import base64 +import json +import os +from pathlib import Path +import subprocess +import sys +import urllib.error +import urllib.request +import xml.etree.ElementTree as ET + + +ADMIN_PASSWORD = "ci-compose-password" +PD_PASSWORD = "ci-compose-pd-secret" + + +def command(*args): + return subprocess.check_output(args, text=True, timeout=60).strip() + + +def verify_image(container, image, expected): + actual = command("docker", "inspect", "--format", "{{.Image}}", container) + if not expected or actual != expected: + raise RuntimeError(f"Container {container} uses {actual}; expected {image} ({expected})") + + +def response(url, credentials=None): + request = urllib.request.Request(url) + if credentials: + encoded = base64.b64encode(credentials.encode()).decode() + request.add_header("Authorization", "Basic " + encoded) + try: + with urllib.request.urlopen(request, timeout=15) as result: + return result.status, result.read().decode() + except urllib.error.HTTPError as error: + return error.code, error.read().decode() + + +def expect_response(url, expected=200, credentials=None): + actual, body = response(url, credentials) + if actual != expected: + raise RuntimeError(f"{url}: HTTP {actual}, expected {expected}") + return body + + +def expected_versions(): + root = Path(__file__).resolve().parents[2] + ns = {"m": "http://maven.apache.org/POM/4.0.0"} + revision = ET.parse(root / "pom.xml").findtext("m:properties/m:revision", namespaces=ns) + gremlin = ET.parse(root / "hugegraph-server/pom.xml").findtext( + "m:properties/m:tinkerpop.version", namespaces=ns) + properties = dict(line.split("=", 1) for line in + (root / "hugegraph-commons/hugegraph-common/src/main/resources/version.properties") + .read_text().splitlines() if "=" in line and not line.startswith("#")) + if properties["VersionInBash"] != revision: + raise RuntimeError("VersionInBash does not match the project revision") + # Packaged API classes use their manifest version before the resource fallback. + api = ET.parse(root / "hugegraph-server/hugegraph-api/pom.xml").findtext( + ".//m:manifestEntries/m:Implementation-Version", namespaces=ns) or properties["ApiVersion"] + expected = {"version": "v1", "core": revision, "gremlin": gremlin, "api": api} + if any(not isinstance(value, str) or not value for value in expected.values()): + raise RuntimeError("Source POMs did not define non-empty expected versions") + return expected + + +def verify_versions(payload): + versions = payload.get("versions") if isinstance(payload, dict) else None + expected = expected_versions() + if not isinstance(versions, dict) or any(versions.get(key) != value for key, value in expected.items()): + raise RuntimeError(f"Server did not return the expected versions object: {expected}; got {versions}") + + +def verify_server(): + url = "http://localhost:8080" + expect_response(url + "/graphspaces/DEFAULT/graphs", 401) + graphs = json.loads(expect_response(url + "/graphspaces/DEFAULT/graphs", + credentials=f"admin:{ADMIN_PASSWORD}")) + names = graphs.get("graphs") if isinstance(graphs, dict) else None + if not isinstance(names, list) or "hugegraph" not in names: + raise RuntimeError("Server did not return its initialized hugegraph in the graphs array") + payload = json.loads(expect_response(url + "/versions")) + verify_versions(payload) + + +def verify_storage(): + for port in (8620, 8520): + if expect_response(f"http://localhost:{port}/v1/health") != "": + raise RuntimeError(f"Port {port} did not return the PD/Store health response") + ready = json.loads(expect_response("http://localhost:8620/v1/ready")) + if not isinstance(ready, dict) or ready.get("ready") is not True: + raise RuntimeError("PD did not report ready=true") + url = "http://localhost:8620/v1/stores" + expect_response(url, 401) + payload = json.loads(expect_response(url, credentials=f"hg:{PD_PASSWORD}")) + data = payload.get("data") if isinstance(payload, dict) else None + if (not isinstance(data, dict) or payload.get("status") != 0 or + not isinstance(data.get("stores"), list) or not data["stores"]): + raise RuntimeError("Authenticated PD did not return its registered stores array") + + +def verify_graph(run_id): + root = Path(__file__).resolve().parents[2] + script = root / "hugegraph-server/hugegraph-dist/src/assembly/travis/run-server-e2e-smoke-test.sh" + environment = dict(os.environ, HUGEGRAPH_USERNAME="admin", HUGEGRAPH_PASSWORD=ADMIN_PASSWORD) + # Reuse the write/read/Gremlin assertions; allow its readiness and bounded HTTP requests. + subprocess.run(["bash", str(script), "http://localhost:8080", "create", run_id], + check=True, timeout=960, env=environment) + + +def smoke(tag, topology, images): + root = Path(__file__).resolve().parents[2] + project = f"hg-pr-{tag}-{Path(topology).stem}" + compose = ["docker", "compose", "-p", project, "-f", str(root / "docker" / topology)] + # Capture identities before startup so a tag change cannot bless an unrelated container. + expected = {service: command("docker", "image", "inspect", "--format", "{{.Id}}", + f"{repository}:{tag}") for service, repository in images.items()} + failed = False + try: + subprocess.run(compose + ["up", "-d", "--pull", "never", "--wait", + "--wait-timeout", "600"] + list(images), check=True, timeout=660) + for service, repository in images.items(): + container = command(*compose, "ps", "-q", service) + if not container or "\n" in container: + raise RuntimeError(f"Expected exactly one {service} container") + verify_image(container, f"{repository}:{tag}", expected[service]) + health = command("docker", "inspect", "--format", "{{.State.Health.Status}}", container) + if health != "healthy": + raise RuntimeError(f"{service} is {health}") + verify_server() + if "pd" in images: + verify_storage() + verify_graph(project.replace("-", "_")) + except BaseException: + failed = True + for args in (["ps"], ["logs", "--no-color", "--tail", "200"]): + try: + subprocess.run(compose + args, check=False, timeout=60) + except (OSError, subprocess.SubprocessError) as error: + print(f"Could not collect Compose diagnostics: {error}", file=sys.stderr) + raise + finally: + try: + subprocess.run(compose + ["down", "-v", "--remove-orphans"], check=True, timeout=120) + except (OSError, subprocess.SubprocessError) as error: + if not failed: + raise + print(f"Compose cleanup also failed: {error}", file=sys.stderr) + + +def main(tag): + if not tag.startswith("ci-") or any(c not in "abcdefghijklmnopqrstuvwxyz0123456789-" for c in tag): + raise ValueError("Use a unique ci- image tag") + os.environ.update(HUGEGRAPH_VERSION=tag, HUGEGRAPH_PULL_POLICY="never", + HUGEGRAPH_ADMIN_PASSWORD=ADMIN_PASSWORD, + HUGEGRAPH_AUTH_TOKEN_SECRET="0123456789abcdef" * 4, + HG_PD_AUTH_SECRET_KEY=PD_PASSWORD) + smoke(tag, "docker-compose.yml", {"server": "hugegraph/hugegraph"}) + smoke(tag, "docker-compose-hstore.yml", {"pd": "hugegraph/pd", "store": "hugegraph/store", + "server": "hugegraph/server"}) + + +if __name__ == "__main__": + main(sys.argv[1]) diff --git a/.github/scripts/test_ci_policy.py b/.github/scripts/test_ci_policy.py new file mode 100644 index 0000000000..65e7666324 --- /dev/null +++ b/.github/scripts/test_ci_policy.py @@ -0,0 +1,521 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Path/event selection, real PR snapshots, and coordinator scheduling contracts.""" +import importlib.util +import json +import os +from pathlib import Path +import re +import subprocess +import tempfile +import textwrap +import unittest +from unittest.mock import patch + +spec = importlib.util.spec_from_file_location("policy", Path(__file__).with_name("ci-policy.py")) +policy = importlib.util.module_from_spec(spec) +spec.loader.exec_module(policy) + + +class GitFixture: + def __init__(self, root): + self.root = root + self.git("init", "-q") + self.git("config", "user.email", "ci@example.invalid") + self.git("config", "user.name", "CI") + + def git(self, *args): + return subprocess.check_output(["git", *args], cwd=self.root, text=True).strip() + + def commit(self, path, content, message): + file = self.root / path + file.parent.mkdir(parents=True, exist_ok=True) + file.write_text(content) + self.git("add", ".") + self.git("commit", "-qm", message) + return self.git("rev-parse", "HEAD") + + def plan(self, event, fetch=None, event_name=None): + with patch.object(policy, "git", side_effect=self.git), patch.dict( + os.environ, {"GITHUB_SHA": self.git("rev-parse", "HEAD"), "GITHUB_EVENT_NAME": ""}): + old = os.getcwd() + try: + os.chdir(self.root) + return policy.create_plan("server", event, "apache/server", fetch=fetch, event_name=event_name) + finally: + os.chdir(old) + + +class PolicyTest(unittest.TestCase): + def plan_for(self, paths, event_name="pull_request", event=None): + # Identity checks have their own real-Git fixtures; isolate event selection here. + def git(*args): + if args[0] == "diff": + return "\n".join(paths) + if args[0] == "show": + return "base head" + return "merge" + event = event or {"before": "base"} + if event_name == "pull_request": + event = {"pull_request": {"number": 7, "head": {"sha": "head", "ref": "feature", + "repo": {"full_name": "alice/server"}}, "base": {"sha": "base"}}} + with patch.object(policy, "git", side_effect=git), patch.object( + policy, "unsafe_documentation", return_value=False), patch.object( + policy, "require_current_pr"), patch.dict(os.environ, {"GITHUB_SHA": "merge"}): + return policy.create_plan("server", event, "apache/server", event_name=event_name) + + def test_ordinary_server_changes_only_run_mainline_server_and_arm_smoke(self): + for path in ["hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/A.java", + "hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/A.java", + "hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/A.java"]: + with self.subTest(path=path): + plan = self.plan_for([path]) + self.assertEqual({"server", "macos"}, set(plan["selected"])) + self.assertEqual({"server_memory", "server_rocksdb", "macos"}, set(plan["expected"])) + self.assertFalse(plan["macos_full"]) + self.assertFalse(plan["pd_store"]) + + def test_distributed_server_contracts_keep_hstore_cluster_without_pd_store_units(self): + for path in [policy.SERVER_CORE + "backend/store/BackendStore.java", + policy.SERVER_CORE + "meta/MetaManager.java", policy.SERVER_CORE + "StandardHugeGraph.java", + policy.SERVER_CORE + "task/DistributedTaskScheduler.java", policy.SERVER_CORE + "task/TaskManager.java", + policy.SERVER_CORE + "masterelection/GlobalMasterInfo.java", + policy.SERVER_API + "core/GraphManager.java", + "hugegraph-server/hugegraph-hstore/src/main/A.java"]: + with self.subTest(path=path): + plan = self.plan_for([path]) + self.assertTrue({"server", "macos", "hstore", "cluster", "distributed_smoke"}.issubset(plan["selected"])) + self.assertTrue(plan["pd_store"]) + self.assertFalse(plan["pd"]) + self.assertFalse(plan["store"]) + self.assertFalse(plan["hbase"]) + self.assertFalse(plan["riscv64"]) + + def test_shared_runtime_consumers_and_protocols(self): + for module in ["commons", "struct"]: + selected = policy.select("server", [f"hugegraph-{module}/src/main/A.java"]) + self.assertTrue({"server", "pd", "store", "hstore", "cluster", "macos"}.issubset(selected)) + self.assertNotIn("hbase", selected) + self.assertNotIn("riscv64", selected) + for module in ["pd", "store"]: + plan = self.plan_for([f"hugegraph-{module}/src/main/A.java"]) + self.assertTrue(plan["pd_store"]) + self.assertTrue(plan["hstore"]) + self.assertTrue(plan["cluster"]) + self.assertFalse(plan["server"]) + self.assertFalse(plan["macos"]) + self.assertTrue({"pd", "store", "hstore", "cluster"}.issubset(policy.select( + "server", ["hugegraph-pd/hg-pd-grpc/src/main/proto/common.proto"]))) + + def test_noncore_platforms_only_follow_direct_inputs(self): + for path in ["hugegraph-server/hugegraph-hbase/src/main/A.java", + "hugegraph-server/hugegraph-dist/src/assembly/travis/install-hbase.sh", + "hugegraph-server/hugegraph-dist/src/assembly/travis/hbase-ci-site.xml"]: + self.assertEqual({"hbase"}, policy.select("server", [path])) + self.assertEqual({"riscv64"}, policy.select("server", [".github/workflows/riscv64-ci.yml"])) + self.assertTrue(self.plan_for([".github/workflows/server-compatibility-ci.yml"])["riscv64"]) + self.assertTrue(self.plan_for([".github/workflows/server-tests.yml"])["hbase"]) + self.assertTrue(self.plan_for(["hugegraph-server/hugegraph-rocksdb/pom.xml"])["riscv64"]) + self.assertTrue(self.plan_for(["hugegraph-server/hugegraph-dist/pom.xml"])["riscv64"]) + for path in ["hugegraph-server/hugegraph-rocksdb/src/main/A.java", + "hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh"]: + plan = self.plan_for([path]) + self.assertTrue(plan["macos"]) + self.assertTrue(plan["macos_full"]) + self.assertFalse(plan["hbase"]) + self.assertEqual(path.startswith("hugegraph-server/hugegraph-dist/src/assembly/static/bin/"), + plan["riscv64"]) + for path in ["docker/hbase/Dockerfile", "docker/hbase/entrypoint.sh", policy.TRAVIS + "install-backend.sh"]: + self.assertTrue(self.plan_for([path])["hbase"]) + install = self.plan_for([policy.TRAVIS + "install-backend.sh"]) + self.assertTrue(install["hstore"]) + java = self.plan_for([".github/workflows/.java-version"]) + self.assertTrue(java["macos_full"]) + self.assertTrue(java["hbase"]) + self.assertTrue(java["riscv64"]) + for path in policy.PLATFORM_POMS: + self.assertTrue(self.plan_for([path])["riscv64"]) + self.assertTrue(self.plan_for([path])["macos_full"]) + for path in ["hugegraph-server/hugegraph-core/src/main/resources/proto/A.proto", + "hugegraph-pd/hg-pd-grpc/src/main/proto/A.proto"]: + self.assertTrue(self.plan_for([path])["riscv64"]) + self.assertFalse(self.plan_for(["hugegraph-store/hg-store-grpc/src/main/proto/A.proto"])["riscv64"]) + self.assertFalse(self.plan_for(["hugegraph-pd/hg-pd-core/pom.xml"])["riscv64"]) + native = self.plan_for(["hugegraph-server/hugegraph-dist/src/assembly/travis/run-native-runtime-smoke-test.sh"]) + self.assertTrue(native["macos_full"]) + self.assertTrue(native["riscv64"]) + self.assertTrue(self.plan_for([policy.TRAVIS + "check-rocksdb-only-dist.sh"])["riscv64"]) + + def test_security_tracks_sensitive_inputs_and_dependencies(self): + paths = [policy.SERVER_CORE + "auth/AuthManager.java", policy.SERVER_CORE + "security/HugeSecurityManager.java", + policy.SERVER_CORE + "config/AuthOptions.java", + policy.SERVER_API + "api/filter/AuthenticationFilter.java", "hugegraph-pd/hg-pd-core/pom.xml", + ".github/workflows/codeql-analysis.yml", + policy.SERVER_CORE + "backend/cache/CachedSchemaTransaction.java", + policy.SERVER_CORE + "backend/cache/CachedSchemaTransactionV2.java"] + for path in paths: + with self.subTest(path=path): + self.assertTrue(self.plan_for([path])["security"]) + for path in [policy.SERVER_CORE + "api/Author.java", "hugegraph-pd/hg-pd-core/src/main/A.java", + "docker/Dockerfile", "helm/templates/service.yaml", + policy.SERVER_CORE + "backend/cache/CacheManager.java", + policy.SERVER_CORE + "backend/cache/CachedSchemaTransactionTest.java"]: + self.assertFalse(self.plan_for([path])["security"]) + + def test_documentation_and_nested_readmes_skip_every_test_lane(self): + paths = ["README.md", "CONTRIBUTING.md", "notes.txt", "LICENSE", "NOTICE", "docs/ci.md", + ".github/outdated/APACHE_PROPOSAL.md", "hugegraph-pd/design.md", + "hugegraph-pd/docs/raft.md", "hugegraph-store/docs/guide.txt", "hugegraph-server/hugegraph-hstore/README.md", + "hugegraph-server/nested/component/README.md", "hugegraph-pd/hg-pd-core/AGENTS.md", "docker/README.md"] + for event_name, event in [("pull_request", {}), ("push", {"ref": "refs/heads/master"})]: + plan = self.plan_for(paths, event_name, event) + self.assertEqual([], plan["selected"]) + self.assertEqual([], plan["expected"]) + self.assertFalse(any(plan[module] for module in policy.MODULES["server"])) + for path in ["hugegraph-server/hugegraph-test/src/test/resources/README.md", "docs/example.java", + "docs/config.yml", "hugegraph-pd/docs/fixtures/example.txt", "helm/templates/README.md", + "hugegraph-store/hg-store-dist/src/assembly/static/conf/startup.txt"]: + with self.subTest(path=path): + self.assertTrue(policy.select("server", [path])) + + def test_master_push_runs_smoke_without_repeated_full_suites(self): + event = {"before": "base", "ref": "refs/heads/master"} + generic = self.plan_for(["hugegraph-server/hugegraph-core/src/main/A.java"], "push", event) + self.assertEqual({"macos", "distributed_smoke"}, set(generic["selected"])) + pd = self.plan_for(["hugegraph-pd/hg-pd-core/src/main/A.java"], "push", event) + self.assertEqual({"distributed_smoke"}, set(pd["selected"])) + for path in ["hugegraph-server/hugegraph-hbase/src/main/A.java", ".github/workflows/riscv64-ci.yml", + ".github/workflows/codeql-analysis.yml"]: + self.assertEqual([], self.plan_for([path], "push", event)["expected"]) + native = self.plan_for(["hugegraph-server/hugegraph-rocksdb/src/main/A.java"], "push", event) + self.assertTrue(native["macos"]) + self.assertFalse(native["macos_full"]) + self.assertTrue(self.plan_for(["docker/Dockerfile"], "push", event)["docker"]) + + def test_manual_presets_weekly_and_release_branches(self): + for preset, selected in policy.PRESETS.items(): + with self.subTest(preset=preset): + plan = self.plan_for([], "workflow_dispatch", {"inputs": {"test-set": preset}}) + self.assertEqual(selected, set(plan["selected"])) + default = self.plan_for([], "workflow_dispatch") + self.assertEqual(policy.MAINLINE, set(default["selected"])) + self.assertFalse(default["macos_full"]) + weekly = self.plan_for([], "schedule") + self.assertEqual(set(policy.MODULES["server"]), set(weekly["selected"])) + self.assertTrue(weekly["macos_full"]) + for branch in ["release-1.7", "test-ci", "tinkerpop-3.8"]: + plan = self.plan_for(["hugegraph-server/hugegraph-core/src/main/A.java"], "push", + {"before": "base", "ref": "refs/heads/" + branch}) + self.assertTrue(plan["server"]) + self.assertFalse(plan["hbase"]) + self.assertFalse(plan["riscv64"]) + + def test_unknown_inputs_fall_back_to_mainline_without_expensive_noncore(self): + for path in [".github/scripts/ci-policy.py", ".mvn/maven.config", "unknown"]: + self.assertEqual(policy.MAINLINE, policy.select("server", [path])) + with patch.object(policy, "git", side_effect=lambda *args: "merge" if args[0] == "rev-parse" else + (_ for _ in ()).throw(subprocess.CalledProcessError(1, "git"))): + plan = policy.create_plan("server", {"before": "base"}, "apache/server", event_name="push") + self.assertEqual(policy.MAINLINE, set(plan["selected"])) + self.assertFalse(plan["macos_full"]) + self.assertFalse(plan["hbase"]) + self.assertFalse(plan["riscv64"]) + + def test_deployment_and_specific_workflows_keep_specific_owners(self): + for path in [".github/workflows/docker-build-ci.yml", ".github/scripts/check-docker-images.sh", + ".github/scripts/docker-deployment.py", ".github/scripts/test_docker_deployment.py", + "hugegraph-server/hugegraph-dist/docker/Dockerfile"]: + self.assertEqual({"docker"}, policy.select("server", [path])) + for path in [".asf.yaml", ".github/dependabot.yml", ".editorconfig"]: + self.assertEqual(set(), policy.select("server", [path])) + for module in ["pd", "store"]: + selected = policy.select("server", [f"hugegraph-{module}/hg-{module}-dist/src/assembly/static/bin/start.sh"]) + self.assertTrue({module, "hstore", "cluster", "docker", "distributed_smoke"}.issubset(selected)) + self.assertNotIn("server", selected) + version = "hugegraph-commons/hugegraph-common/src/main/resources/version.properties" + self.assertTrue({"commons", "docker"}.issubset(policy.select("server", [version]))) + + def test_toolchain_selection_and_fixture_gate_contract_remain_compatible(self): + quiet = patch("builtins.print") + quiet.start() + self.addCleanup(quiet.stop) + self.assertEqual({"client", "loader", "tools", "spark", "hubble"}, policy.select( + "toolchain", ["hugegraph-client/src/A.java"])) + self.assertEqual({"loader", "hubble"}, policy.select("toolchain", ["hugegraph-loader/src/A.java"])) + self.assertEqual(set(policy.MODULES["toolchain"]), policy.select("toolchain", ["hugegraph-client/api.proto"])) + self.assertIn("go", policy.select("toolchain", ["hugegraph-client/assembly/travis/start-hugegraph-servers.sh"])) + plan = {"schema": 1, "repository": "apache/toolchain", "project": "toolchain", "pr": 0, + "source": "apache/toolchain", "branch": "master", "base": "base", "head": "head", + "testedMergeSHA": "head", "selected": ["client"], "expected": ["client"]} + results = {"plan": {"result": "success"}, "client": {"result": "success"}, "fixture": {"result": "success"}} + self.assertEqual(["client"], policy.gate(plan, results, lambda _: self.fail("No post-run API query"))["executed"]) + for state in ["failure", "cancelled", "skipped", None]: + for job in ["plan", "client", "fixture"]: + with self.subTest(state=state, job=job), self.assertRaises(policy.GateFailure): + policy.gate(plan, dict(results, **{job: {"result": state}})) + + def test_toolchain_gate_keeps_summary_current_pr_and_cli_all(self): + plan = {"schema": 1, "repository": "apache/toolchain", "project": "toolchain", "pr": 7, + "source": "alice/toolchain", "branch": "feature", "base": "base", "head": "head", + "testedMergeSHA": "merge", "selected": ["client"], "expected": ["client"]} + live = {"state": "open", "head": {"sha": "head", "ref": "feature", "repo": {"full_name": "alice/toolchain"}}, + "base": {"repo": {"full_name": "apache/toolchain"}}} + results = {"plan": {"result": "success"}, "client": {"result": "success"}, "fixture": {"result": "success"}} + with tempfile.TemporaryDirectory() as directory, patch("builtins.print"): + root = Path(directory) + summary = root / "summary.md" + with patch.dict(os.environ, {"GITHUB_STEP_SUMMARY": str(summary)}): + policy.gate(plan, results, lambda _: live) + self.assertIn("| client | yes | advisory | affected inputs | success |", summary.read_text()) + with self.assertRaises(policy.StaleInputError): + policy.gate(plan, results, lambda _: dict(live, head=dict(live["head"], sha="new-head"))) + (root / "plan.json").write_text(json.dumps(dict(plan, pr=0))) + (root / "results.json").write_text(json.dumps(results)) + outcome = subprocess.run(["python3", str(Path(policy.__file__).resolve()), "gate", "--mode", "all", + "--plan", str(root / "plan.json"), "--results", str(root / "results.json"), + "--output", str(root / "report.json")], capture_output=True, text=True) + self.assertEqual(0, outcome.returncode, outcome.stderr) + self.assertEqual(["client"], json.loads((root / "report.json").read_text())["executed"]) + + def test_summary_has_only_advisory_checks(self): + with tempfile.TemporaryDirectory() as directory, patch.dict( + os.environ, {"GITHUB_STEP_SUMMARY": str(Path(directory) / "summary.md")}): + plan = self.plan_for(["pom.xml"]) + summary = (Path(directory) / "summary.md").read_text() + self.assertIn("| server_memory | yes | advisory |", summary) + self.assertNotIn("| required |", summary) + self.assertEqual([], plan["required"]) + self.assertEqual(1, summary.count("| dependency_license |")) + + def coordinator_jobs(self, plan, selection="success", java="success", result="success", cancelled=False): + workflow = Path(__file__).resolve().parents[1] / "workflows/server-ci.yml" + text = workflow.read_text().split("\njobs:\n", 1)[1] + jobs = {} + for match in re.finditer(r"^ ([\w-]+):\n(.*?)(?=^ [\w-]+:\n|\Z)", text, re.M | re.S): + name, body = match.groups() + if name == "preflight": + continue + condition = re.search(r"^ if: \$\{\{ (.+) \}\}$", body, re.M) + self.assertIsNotNone(condition, "Caller needs an explicit cancellation/selection condition: " + name) + expression = condition.group(1) + # GitHub otherwise injects success(), which would hide tests after a license failure. + self.assertIn("cancelled()", expression) + outputs = {key: str(value).lower() for key, value in plan.items() if isinstance(value, bool)} + outputs.update(selection_result=selection, java_result=java) + expression = re.sub(r"needs\.preflight\.outputs\.([\w]+)", + lambda match: repr(outputs.get(match.group(1), "")), expression) + expression = expression.replace("needs.preflight.result", repr(result)) + expression = expression.replace("!cancelled()", "not cancelled") + expression = expression.replace("&&", "and").replace("||", "or") + jobs[name] = bool(eval(expression, {"__builtins__": {}}, {"cancelled": cancelled})) + return {job for job, run in jobs.items() if run} + + def test_real_coordinator_keeps_selected_jobs_after_license_or_selftest_failure(self): + workflow = (Path(__file__).resolve().parents[1] / "workflows/server-ci.yml").read_text() + preflight = re.search(r"^ preflight:\n(.*?)(?=^ [\w-]+:|\Z)", workflow, re.M | re.S).group(1) + self.assertEqual("check-license", re.search(r"^ name: (.+)$", preflight, re.M).group(1)) + full = self.plan_for([], "workflow_dispatch", {"inputs": {"test-set": "full"}}) + expected = {"build-server", "server-rocksdb", "commons", "pd_store", "cluster", "docker", "helm", + "dependency_license", "compatibility", "codeql"} + self.assertEqual(expected, self.coordinator_jobs(full)) + self.assertEqual(expected, self.coordinator_jobs(full, result="failure")) + docs = self.plan_for(["hugegraph-pd/docs/guide.txt", "docker/README.md"]) + self.assertEqual(set(), self.coordinator_jobs(docs, result="failure")) + source = self.plan_for(["hugegraph-server/hugegraph-core/src/main/A.java"]) + self.assertEqual({"build-server", "server-rocksdb", "compatibility"}, self.coordinator_jobs(source)) + master = self.plan_for(["hugegraph-server/hugegraph-core/src/main/A.java"], "push", + {"before": "base", "ref": "refs/heads/master"}) + self.assertEqual({"docker", "compatibility"}, self.coordinator_jobs(master)) + + def test_real_coordinator_blocks_selection_runtime_failure_and_cancellation(self): + full = self.plan_for([], "schedule") + for state in ["failure", "cancelled", "skipped", ""]: + with self.subTest(selection=state): + self.assertEqual(set(), self.coordinator_jobs(full, selection=state, result="failure")) + with self.subTest(java=state): + self.assertEqual({"helm"}, self.coordinator_jobs(full, java=state, result="failure")) + self.assertEqual(set(), self.coordinator_jobs(full, cancelled=True)) + + def test_contract_scripts_reach_their_actual_inner_jobs_and_steps(self): + workflows = Path(__file__).resolve().parents[1] / "workflows" + cases = [("test-stop-hugegraph-store.sh", {"struct"}), + ("test-check-java-runtime-logs.sh", {"struct"}), + ("test-check-jacoco-report.sh", {"struct"}), + ("test-codecov-upload-config.sh", {"struct"}), + ("test-riscv64-java-runtime.sh", {"server", "riscv64"})] + for name, suites in cases: + script = policy.TRAVIS + name + with self.subTest(script=script): + plan = self.plan_for([script]) + self.assertEqual(suites, set(plan["selected"])) + callers = self.coordinator_jobs(plan) + if "struct" in suites: + self.assertEqual({"pd_store"}, callers) + root = (workflows / "server-ci.yml").read_text() + caller = re.search(r"^ pd_store:\n(.*?)(?=^ [\w-]+:|\Z)", root, re.M | re.S).group(1) + inputs = {} + for key in ["struct", "store"]: + binding = re.search(r"^ run-" + key + r": \$\{\{ (.+) \}\}$", caller, re.M).group(1) + binding = binding.replace("needs.preflight.outputs." + key, repr(str(plan[key]).lower())) + inputs[key] = eval(binding, {"__builtins__": {}}) + workflow = (workflows / "pd-store-ci.yml").read_text() + struct = workflow.split("\n struct:\n", 1)[1].split("\n pd:\n", 1)[0] + condition = re.search(r"^ if: (.+)$", struct, re.M).group(1) + condition = condition.replace("inputs.run-struct", str(inputs["struct"])) + condition = condition.replace("inputs.run-store", str(inputs["store"])) + self.assertTrue(eval(condition.replace("||", "or"), {"__builtins__": {}})) + step = next(step for step in struct.split("\n - ") if Path(script).name in step) + self.assertNotRegex(step, r"(?m)^ if:") + else: + self.assertIn("server-rocksdb", callers) + workflow = (workflows / "server-ci.yml").read_text() + # Read the backend from the actual caller rather than assuming a preset. + caller = re.search(r"^ server-rocksdb:\n(.*?)(?=^ [\w-]+:|\Z)", + workflow, re.M | re.S).group(1) + backend = re.search(r"^ backend: (\w+)$", caller, re.M).group(1) + inner = (workflows / "server-tests.yml").read_text() + step = next(step for step in inner.split("\n - ") if Path(script).name in step) + condition = re.search(r"^ if: (.+)$", step, re.M).group(1) + self.assertTrue(eval(condition.replace("inputs.backend", repr(backend)), + {"__builtins__": {}})) + mixed = self.plan_for([policy.TRAVIS + "test-stop-hugegraph-store.sh", "hugegraph-pd/src/A.java"]) + self.assertTrue({"struct", "pd", "store", "hstore", "cluster"}.issubset(mixed["selected"])) + checker = self.plan_for([policy.TRAVIS + "check-java-runtime-logs.sh"]) + self.assertEqual({"build-server", "server-rocksdb", "commons", "pd_store", "cluster", "compatibility"}, + self.coordinator_jobs(checker)) + self.assertTrue(checker["hbase"] and checker["riscv64"] and checker["macos_full"]) + master = {"ref": "refs/heads/master"} + for script in policy.CONTRACT_SUITES: + self.assertEqual([], self.plan_for([script], "push", master)["expected"]) + runtime = "hugegraph-server/hugegraph-core/src/main/A.java" + self.assertEqual(self.plan_for([runtime], "push", master)["selected"], + self.plan_for([runtime, policy.TRAVIS + "check-java-runtime-logs.sh"], + "push", master)["selected"]) + + def test_coordinator_reads_and_validates_real_java_input(self): + workflow = (Path(__file__).resolve().parents[1] / "workflows/server-ci.yml").read_text() + code = workflow.split("python3 - <<'PYTHON'\n", 1)[1].split("\n PYTHON", 1)[0] + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + java = root / ".github/workflows/.java-version" + java.parent.mkdir(parents=True) + output = root / "outputs.txt" + for version in ["17", "17.0.20", "21.0.1", "", "bad", '17\n21']: + java.write_text(version) + result = subprocess.run(["python3", "-c", textwrap.dedent(code)], cwd=root, + env={**os.environ, "GITHUB_OUTPUT": str(output)}, capture_output=True, text=True) + with self.subTest(version=version): + self.assertEqual(version in {"17", "17.0.20", "21.0.1"}, result.returncode == 0, result.stderr) + self.assertEqual(['versions=["17"]', 'versions=["17.0.20"]', 'versions=["21.0.1"]'], + output.read_text().splitlines()) + + def test_real_git_document_modes_renames_and_cli_outputs(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + fixture = GitFixture(root) + base = fixture.commit("README.md", "base", "base") + fixture.commit("README.md", "docs", "docs") + self.assertEqual([], fixture.plan({"before": base, "ref": "refs/heads/master"})["expected"]) + (root / "README.md").chmod(0o755) + fixture.git("add", ".") + fixture.git("commit", "-qm", "executable prose") + unsafe = fixture.plan({"before": base, "ref": "refs/heads/feature"}) + self.assertEqual(policy.MAINLINE, set(unsafe["selected"])) + (root / "README.md").unlink() + (root / "README.md").symlink_to("target.txt") + fixture.git("add", ".") + fixture.git("commit", "-qm", "symlink prose") + self.assertEqual(policy.MAINLINE, set(fixture.plan({"before": base})["selected"])) + before = fixture.commit("hugegraph-server/A.java", "source", "source") + (root / "docs").mkdir() + fixture.git("mv", "hugegraph-server/A.java", "docs/renamed.md") + fixture.git("commit", "-qm", "rename source to prose") + self.assertTrue(fixture.plan({"before": before})["server"]) + event = root / "event.json" + event.write_text("{}") + output = root / "outputs.txt" + subprocess.run(["python3", str(Path(policy.__file__).resolve()), "plan", "--project", "server", + "--repository", "apache/server", "--event-path", str(event), "--event-name", "schedule", + "--output", str(root / "plan.json")], cwd=root, check=True, + env={**os.environ, "GITHUB_OUTPUT": str(output)}) + self.assertIn("macos_full=true\n", output.read_text()) + self.assertIn("pd_store=true\n", output.read_text()) + self.assertNotIn("compatibility=", output.read_text()) + self.assertEqual(set(policy.MODULES["server"]), set(json.loads((root / "plan.json").read_text())["selected"])) + + def test_cumulative_pr_snapshot_keeps_source_after_document_commit_and_checks_current_head(self): + with tempfile.TemporaryDirectory() as directory: + fixture = GitFixture(Path(directory)) + base = fixture.commit("README.md", "base", "base") + fixture.git("checkout", "-qb", "feature") + fixture.commit("hugegraph-server/A.java", "source", "source") + head = fixture.commit("README.md", "docs after source", "docs") + merge = fixture.git("commit-tree", fixture.git("rev-parse", "HEAD^{tree}"), + "-p", base, "-p", head, "-m", "PR merge") + fixture.git("checkout", "--detach", "-q", merge) + live = {"number": 7, "state": "open", "head": {"sha": head, "ref": "feature", + "repo": {"full_name": "alice/server"}}, "base": {"sha": base, "repo": {"full_name": "apache/server"}}} + calls = [] + def fetch(path): + calls.append(path) + return live + plan = fixture.plan({"pull_request": live}, fetch) + self.assertEqual(["repos/apache/server/pulls/7"], calls) + self.assertTrue(plan["server"]) + self.assertFalse(plan["pd_store"]) + self.assertFalse(plan["security"]) + self.assertIn("hugegraph-server/A.java", plan["changedPaths"]) + for section, field, value in [("head", "sha", "new-head"), ("head", "ref", "other"), + ("head", "repo", {"full_name": "other/fork"}), + ("base", "repo", {"full_name": "other/target"})]: + changed = dict(live, **{section: dict(live[section], **{field: value})}) + with self.subTest(section=section, field=field), self.assertRaises(policy.StaleInputError): + fixture.plan({"pull_request": live}, lambda _: changed) + with self.assertRaises(policy.StaleInputError): + fixture.plan({"pull_request": live}, lambda _: dict(live, state="closed")) + fixture.git("checkout", "--detach", "-q", head) + with self.assertRaises(policy.StaleInputError): + fixture.plan({"pull_request": live}, fetch) + + def test_queued_merge_uses_actual_target_parent_and_api_outage_keeps_pr_identity(self): + live = {"number": 7, "state": "open", "head": {"sha": "head", "ref": "feature", + "repo": {"full_name": "alice/server"}}, "base": {"sha": "event-base", "repo": {"full_name": "apache/server"}}} + def git(*args): + if args[0] == "show": + return "current-base head" + if args[0] == "diff": + return "README.md" + return "merge" + with patch.dict(os.environ, {"GITHUB_SHA": "merge", "GITHUB_EVENT_NAME": ""}), patch.object( + policy, "git", side_effect=git), patch.object(policy, "unsafe_documentation", return_value=False): + plan = policy.create_plan("server", {"pull_request": live}, "apache/server", lambda _: live) + self.assertEqual("current-base", plan["base"]) + self.assertEqual([], plan["expected"]) + def unavailable(_): + raise subprocess.CalledProcessError(1, "gh") + fallback = policy.create_plan("server", {"pull_request": live}, "apache/server", unavailable) + self.assertEqual(policy.MAINLINE, set(fallback["selected"])) + self.assertEqual((7, "alice/server", "feature", "current-base", "head"), + tuple(fallback[key] for key in ["pr", "source", "branch", "base", "head"])) + for malformed in [{"pull_request": None}, {"pull_request": {}}, + {"pull_request": dict(live, head=dict(live["head"], repo=None))}]: + with self.subTest(event=malformed), self.assertRaises(policy.StaleInputError): + policy.create_plan("server", malformed, "apache/server", lambda _: self.fail("Invalid identity")) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/scripts/test_docker_deployment.py b/.github/scripts/test_docker_deployment.py new file mode 100644 index 0000000000..7eeca7ff8e --- /dev/null +++ b/.github/scripts/test_docker_deployment.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Reject successful service checks performed against an unrelated image.""" + +import importlib.util +import json +import os +from pathlib import Path +import unittest +from unittest.mock import patch + +spec = importlib.util.spec_from_file_location("deployment", Path(__file__).with_name("docker-deployment.py")) +deployment = importlib.util.module_from_spec(spec) +spec.loader.exec_module(deployment) + + +class ImageIdentityTest(unittest.TestCase): + def test_rejects_old_image_even_when_service_started(self): + with patch.object(deployment, "command", return_value="sha256:old"): + with self.assertRaisesRegex(RuntimeError, "uses sha256:old"): + deployment.verify_image("container", "hugegraph/server:ci-1-1", "sha256:pr") + + def test_rejects_missing_image_identity(self): + with patch.object(deployment, "command", return_value=""): + with self.assertRaises(RuntimeError): + deployment.verify_image("container", "hugegraph/server:ci-1-1", "") + + def test_accepts_exact_built_image(self): + with patch.object(deployment, "command", return_value="sha256:pr"): + deployment.verify_image("container", "hugegraph/server:ci-1-1", "sha256:pr") + + def test_identity_failure_cleans_compose_and_never_checks_http(self): + with patch.object(deployment, "command", side_effect=["sha256:pr", "container"]), \ + patch.object(deployment, "verify_image", side_effect=RuntimeError("wrong image")), \ + patch.object(deployment.subprocess, "run") as run, \ + patch.object(deployment, "response") as response: + with self.assertRaisesRegex(RuntimeError, "wrong image"): + deployment.smoke("ci-1-1", "docker-compose.yml", {"server": "hugegraph/hugegraph"}) + calls = [call.args[0] for call in run.call_args_list] + startup = next(args for args in calls if "up" in args) + self.assertEqual(startup[-1], "server") + self.assertEqual(startup[startup.index("--pull") + 1], "never") + self.assertNotIn("hubble", startup) + self.assertTrue(any("logs" in args for args in calls)) + self.assertEqual(calls[-1][-3:], ["down", "-v", "--remove-orphans"]) + response.assert_not_called() + + def test_startup_timeout_preserves_failure_and_cleans_compose(self): + timeout = deployment.subprocess.TimeoutExpired("up", 660) + cleanup = deployment.subprocess.CalledProcessError(1, "down") + with patch.object(deployment, "command", return_value="sha256:pr"), \ + patch.object(deployment.subprocess, "run", side_effect=[timeout, None, None, cleanup]) as run: + with self.assertRaises(deployment.subprocess.TimeoutExpired): + deployment.smoke("ci-1-1", "docker-compose.yml", {"server": "hugegraph/hugegraph"}) + self.assertEqual(run.call_args_list[-1].args[0][-3:], ["down", "-v", "--remove-orphans"]) + + def test_hstore_starts_only_pr_services_and_cleans_after_checks(self): + images = {"pd": "hugegraph/pd", "store": "hugegraph/store", "server": "hugegraph/server"} + identities = ["sha256:pd", "sha256:store", "sha256:server"] + containers = [value for service in images for value in (service, f"sha256:{service}", "healthy")] + with patch.object(deployment, "command", side_effect=identities + containers), \ + patch.object(deployment.subprocess, "run") as run, \ + patch.object(deployment, "verify_server") as server, \ + patch.object(deployment, "verify_storage") as storage, \ + patch.object(deployment, "verify_graph") as graph: + deployment.smoke("ci-1-1", "docker-compose-hstore.yml", images) + startup = run.call_args_list[0].args[0] + self.assertEqual(startup[-3:], ["pd", "store", "server"]) + self.assertNotIn("hubble", startup) + server.assert_called_once_with() + storage.assert_called_once_with() + graph.assert_called_once_with("hg_pr_ci_1_1_docker_compose_hstore") + self.assertEqual(run.call_args_list[-1].args[0][-3:], ["down", "-v", "--remove-orphans"]) + + +class GraphSmokeTest(unittest.TestCase): + def test_reuses_graph_write_read_and_gremlin_checks_with_auth(self): + with patch.dict(os.environ, {"PATH": "/existing/tools"}), \ + patch.object(deployment.subprocess, "run") as run: + deployment.verify_graph("ci_1_1") + invocation = run.call_args + self.assertTrue(invocation.args[0][1].endswith("run-server-e2e-smoke-test.sh")) + self.assertEqual(invocation.args[0][2:], ["http://localhost:8080", "create", "ci_1_1"]) + self.assertTrue(invocation.kwargs["check"]) + self.assertEqual(invocation.kwargs["timeout"], 960) + environment = invocation.kwargs["env"] + self.assertEqual(environment["PATH"], "/existing/tools") + self.assertEqual(environment["HUGEGRAPH_USERNAME"], "admin") + self.assertEqual(environment["HUGEGRAPH_PASSWORD"], deployment.ADMIN_PASSWORD) + + def test_graph_failure_propagates_and_cleans_both_topologies(self): + for topology, images in ( + ("docker-compose.yml", {"server": "hugegraph/hugegraph"}), + ("docker-compose-hstore.yml", {"pd": "hugegraph/pd", "store": "hugegraph/store", + "server": "hugegraph/server"})): + identities = [f"sha256:{service}" for service in images] + containers = [value for service in images for value in (service, f"sha256:{service}", "healthy")] + failure = deployment.subprocess.CalledProcessError(1, "graph smoke") + with self.subTest(topology=topology), \ + patch.object(deployment, "command", side_effect=identities + containers), \ + patch.object(deployment.subprocess, "run") as run, \ + patch.object(deployment, "verify_server"), \ + patch.object(deployment, "verify_storage"), \ + patch.object(deployment, "verify_graph", side_effect=failure) as graph: + with self.assertRaises(deployment.subprocess.CalledProcessError) as caught: + deployment.smoke("ci-1-1", topology, images) + self.assertIs(caught.exception, failure) + graph.assert_called_once() + calls = [call.args[0] for call in run.call_args_list] + self.assertTrue(any("logs" in args for args in calls)) + self.assertEqual(calls[-1][-3:], ["down", "-v", "--remove-orphans"]) + + +class PayloadTest(unittest.TestCase): + VERSIONS = json.dumps({"versions": deployment.expected_versions()}) + GRAPHS = '{"graphs":["hugegraph"]}' + + def test_accepts_public_versions_and_authenticated_graphs(self): + with patch.object(deployment, "response", side_effect=[(401, ""), (200, self.GRAPHS), + (200, self.VERSIONS)]) as response: + deployment.verify_server() + calls = response.call_args_list + self.assertIsNone(calls[0].args[1]) + self.assertEqual(calls[1].args[1], "admin:ci-compose-password") + self.assertIsNone(calls[2].args[1]) + + def test_rejects_graph_error_body_even_when_http_status_is_200(self): + for body in ('{"status":-1,"error":"Unauthorized"}', '{"graphs":"hugegraph"}', + '{"graphs":[]}'): + with self.subTest(body=body), \ + patch.object(deployment, "response", side_effect=[(401, ""), (200, body)]): + with self.assertRaisesRegex(RuntimeError, "graphs array"): + deployment.verify_server() + + def test_rejects_invalid_versions_even_when_http_status_is_200(self): + for body in ('{"status":200}', '{"versions":{"version":"v1","core":"1.7.0"}}', + '{"versions":{"version":"v1","core":"1","api":"","gremlin":"3"}}'): + with self.subTest(body=body), \ + patch.object(deployment, "response", side_effect=[(401, ""), (200, self.GRAPHS), (200, body)]): + with self.assertRaisesRegex(RuntimeError, "versions object"): + deployment.verify_server() + + def test_rejects_wrong_release_gremlin_and_protocol_versions(self): + for key, wrong in (("core", "1.7.0"), ("gremlin", "3.7.3"), ("gremlin", None), + ("api", "1.8.0"), ("version", "v2")): + payload = json.loads(self.VERSIONS) + payload["versions"][key] = wrong + with self.subTest(key=key), self.assertRaisesRegex(RuntimeError, "versions object"): + deployment.verify_versions(payload) + + def test_rejects_missing_expected_gremlin_version(self): + parse = deployment.ET.parse + + def read_pom(path): + if path == Path(deployment.__file__).resolve().parents[2] / "hugegraph-server/pom.xml": + return deployment.ET.ElementTree(deployment.ET.Element("project")) + return parse(path) + + with patch.object(deployment.ET, "parse", side_effect=read_pom): + with self.assertRaisesRegex(RuntimeError, "non-empty expected versions"): + deployment.expected_versions() + + def test_rejects_unauthenticated_server_access(self): + with patch.object(deployment, "response", return_value=(200, self.GRAPHS)): + with self.assertRaisesRegex(RuntimeError, "expected 401"): + deployment.verify_server() + + def test_accepts_pd_store_health_readiness_and_authenticated_registration(self): + responses = [(200, ""), (200, ""), (200, '{"ready":true}'), (401, ""), + (200, '{"status":0,"data":{"stores":[{"address":"store:8500"}]}}')] + with patch.object(deployment, "response", side_effect=responses) as response: + deployment.verify_storage() + self.assertEqual(response.call_args_list[-1].args[1], "hg:ci-compose-pd-secret") + + def test_rejects_pd_false_readiness_or_error_body(self): + for body in ('{"ready":false}', '{"ready":"true"}', '{"status":-1}'): + with self.subTest(body=body), \ + patch.object(deployment, "response", side_effect=[(200, ""), (200, ""), (200, body)]): + with self.assertRaisesRegex(RuntimeError, "ready=true"): + deployment.verify_storage() + + def test_rejects_health_error_body_even_when_http_status_is_200(self): + with patch.object(deployment, "response", return_value=(200, '{"error":"Unauthorized"}')): + with self.assertRaisesRegex(RuntimeError, "health response"): + deployment.verify_storage() + + def test_rejects_unregistered_store_or_pd_error(self): + for body in ('{"status":0,"data":{"stores":[]}}', + '{"status":-1,"data":{"stores":[{}]}}'): + responses = [(200, ""), (200, ""), (200, '{"ready":true}'), (401, ""), (200, body)] + with self.subTest(body=body), patch.object(deployment, "response", side_effect=responses): + with self.assertRaisesRegex(RuntimeError, "registered stores array"): + deployment.verify_storage() diff --git a/.github/workflows/.java-version b/.github/workflows/.java-version new file mode 100644 index 0000000000..98d9bcb75a --- /dev/null +++ b/.github/workflows/.java-version @@ -0,0 +1 @@ +17 diff --git a/.github/workflows/auto-pr-review.yml b/.github/workflows/auto-pr-review.yml index 6a585355f7..df4bfeb690 100644 --- a/.github/workflows/auto-pr-review.yml +++ b/.github/workflows/auto-pr-review.yml @@ -28,7 +28,7 @@ jobs: pull-requests: write steps: - name: Add review comment - uses: peter-evans/create-or-update-comment@v4 + uses: peter-evans/create-or-update-comment@v5 with: issue-number: ${{ github.event.pull_request.number }} body: | diff --git a/.github/workflows/check-dependencies.yml b/.github/workflows/check-dependencies.yml index 23efa75ddb..71da915301 100644 --- a/.github/workflows/check-dependencies.yml +++ b/.github/workflows/check-dependencies.yml @@ -2,39 +2,26 @@ name: "3rd-party" on: - push: - branches: - - 'release-*' - pull_request: - # Keep source/resources and shared build inputs; skip named documentation files. - paths: - - '**/pom.xml' - - '.mvn/**' - - '.github/configs/**' - - 'install-dist/**' - - '**/*.jar' - - '.github/workflows/check-dependencies.yml' - - '!**/README*' - - '!**/AGENTS.md' - - '!**/CLAUDE.md' + workflow_call: permissions: contents: read jobs: dependency-check: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 env: USE_STAGE: 'false' # Whether to include the stage repository. SCRIPT_DEPENDENCY: install-dist/scripts/dependency + PYTHONDONTWRITEBYTECODE: '1' # Keep generated Python caches out of source style checks. steps: - name: Checkout source - uses: actions/checkout@v4 - - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/checkout@v7 + - name: Set up project JDK + uses: actions/setup-java@v6 with: - java-version: '11' - distribution: 'adopt' + java-version-file: .github/workflows/.java-version + distribution: 'temurin' - name: use staged maven repo settings if: ${{ env.USE_STAGE == 'true' }} @@ -42,21 +29,34 @@ jobs: cp $HOME/.m2/settings.xml /tmp/settings.xml mv -vf .github/configs/settings.xml $HOME/.m2/settings.xml + - name: Test shared foundation checks + run: python3 -m unittest discover -s install-dist/scripts/shared-foundation -p 'test_*.py' + + - name: Test dependency inventory producer and checker + run: python3 -m unittest discover -s install-dist/scripts/dependency -p 'test_*.py' + - name: mvn install run: | mvn install -Dmaven.test.skip=true -ntp --fail-at-end + - name: Check shared foundation boundaries and shipped classes + run: | + REVISION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + python3 install-dist/scripts/shared-foundation/check_shared_foundation.py --revision "$REVISION" + - name: generate current dependencies run: | - bash $SCRIPT_DEPENDENCY/regenerate_known_dependencies.sh current-dependencies.txt + bash $SCRIPT_DEPENDENCY/regenerate_known_dependencies.sh "$PWD/target/dependency/current-dependencies.txt" - name: check third dependencies run: | - bash $SCRIPT_DEPENDENCY/check_dependencies.sh + bash $SCRIPT_DEPENDENCY/check_dependencies.sh \ + "$SCRIPT_DEPENDENCY/known-dependencies.txt" "$PWD/target/dependency/current-dependencies.txt" dependency-review: - runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + runs-on: ubuntu-24.04 steps: - name: 'Checkout Repository' - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: 'Dependency Review' uses: actions/dependency-review-action@v5 # Refer: https://github.com/actions/dependency-review-action diff --git a/.github/workflows/cluster-test-ci.yml b/.github/workflows/cluster-test-ci.yml index c2972cb9f9..85c4ef8881 100644 --- a/.github/workflows/cluster-test-ci.yml +++ b/.github/workflows/cluster-test-ci.yml @@ -1,56 +1,32 @@ name: "Cluster Test CI" on: - push: - branches: - - master - - 'release-*' - - 'test-*' - pull_request: - # Keep source/resources and shared build inputs; skip named documentation files. - paths: - - 'pom.xml' - - '.mvn/**' - - 'style/**' - - '.editorconfig' - - '.github/configs/**' - - 'hugegraph-commons/**' - - 'hugegraph-struct/**' - - 'hugegraph-pd/**' - - 'hugegraph-store/**' - - 'hugegraph-server/**' - - 'hugegraph-cluster-test/**' - - 'install-dist/**' - - '.github/workflows/cluster-test-ci.yml' - - '!**/README*' - - '!**/AGENTS.md' - - '!**/CLAUDE.md' + workflow_call: + +permissions: + contents: read jobs: cluster-test: - runs-on: ubuntu-latest + timeout-minutes: 120 + runs-on: ubuntu-24.04 env: USE_STAGE: 'false' # Whether to include the stage repository. steps: - - name: Install JDK 11 - uses: actions/setup-java@v3 - with: - java-version: '11' - distribution: 'zulu' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 5 + - name: Install project JDK + uses: actions/setup-java@v6 + with: + java-version-file: .github/workflows/.java-version + distribution: 'zulu' + cache: maven + cache-jdk: false + - name: use staged maven repo settings if: ${{ env.USE_STAGE == 'true' }} run: | @@ -63,8 +39,33 @@ jobs: - name: Run simple cluster test run: | - mvn test -pl hugegraph-cluster-test/hugegraph-clustertest-test -am -P simple-cluster-test + set -o pipefail + suite_log="$RUNNER_TEMP/simple-cluster-test.log" + timeout 45m mvn test -pl hugegraph-cluster-test/hugegraph-clustertest-test \ + -am -P simple-cluster-test 2>&1 | tee "$suite_log" + hugegraph-server/hugegraph-dist/src/assembly/travis/check-java-runtime-logs.sh "$suite_log" - name: Run multi cluster test run: | - mvn test -pl hugegraph-cluster-test/hugegraph-clustertest-test -am -P multi-cluster-test + set -o pipefail + suite_log="$RUNNER_TEMP/multi-cluster-test.log" + timeout 45m mvn test -pl hugegraph-cluster-test/hugegraph-clustertest-test \ + -am -P multi-cluster-test 2>&1 | tee "$suite_log" + hugegraph-server/hugegraph-dist/src/assembly/travis/check-java-runtime-logs.sh "$suite_log" + + - name: Show cluster diagnostics on failure + if: failure() + run: | + echo "[ci] java processes:" + ps -ef | grep -E "HugeGraph|hg-|java" | grep -v grep || true + echo "[ci] listening tcp ports:" + (ss -ltnp || netstat -ltnp || true) 2>&1 + find hugegraph-cluster-test -path '*/logs/*' -type f | sort | while read -r log; do + echo "--- tail -n 200 $log ---" + tail -n 200 "$log" || true + done + find . -path '*/surefire-reports/*' -type f \ + \( -name '*.txt' -o -name '*.xml' \) | sort | while read -r report; do + echo "--- tail -n 120 $report ---" + tail -n 120 "$report" || true + done diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d66dc8cee9..3f9b906933 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,51 +1,54 @@ name: "CodeQL" on: - push: - branches: [ master, release-* ] - pull_request: - # The branches below must be a subset of the branches above - # branches: [ master ] # enable in all PRs - schedule: - - cron: '33 0 * * 5' + workflow_call: + inputs: + enabled: + type: boolean + default: true + +permissions: + contents: read + actions: read + security-events: write jobs: analyze: + if: inputs.enabled env: USE_STAGE: 'false' # Whether to include the stage repository. - name: Analyze - runs-on: ubuntu-latest + # Preserve the master scan identity when invoked by a reusable-workflow caller. + CODEQL_ACTION_ANALYSIS_KEY: .github/workflows/codeql-analysis.yml:analyze + name: Analyze (java) + runs-on: ubuntu-24.04 permissions: actions: read contents: read security-events: write - strategy: - fail-fast: false - matrix: - language: [ 'java' ] - steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + persist-credentials: false - name: Setup Java JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v6 with: distribution: 'zulu' - java-version: '11' + java-version-file: .github/workflows/.java-version - name: use staged maven repo settings if: ${{ env.USE_STAGE == 'true' }} run: | - cp $HOME/.m2/settings.xml /tmp/settings.xml - mv -vf .github/configs/settings.xml $HOME/.m2/settings.xml + cp "$HOME/.m2/settings.xml" /tmp/settings.xml + mv -vf .github/configs/settings.xml "$HOME/.m2/settings.xml" # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: - languages: ${{ matrix.language }} + languages: java # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. @@ -54,7 +57,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -68,4 +71,6 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 + with: + category: .github/workflows/codeql-analysis.yml:analyze/language:java diff --git a/.github/workflows/commons-ci.yml b/.github/workflows/commons-ci.yml index fd01df1825..8246a8cc1d 100644 --- a/.github/workflows/commons-ci.yml +++ b/.github/workflows/commons-ci.yml @@ -1,66 +1,75 @@ name: "HugeGraph-Commons CI" on: - workflow_dispatch: - push: - branches: - - master - - 'release-*' - - 'test-*' - pull_request: + workflow_call: + inputs: + java-versions: + type: string + required: true + +permissions: + contents: read jobs: build-commons: - runs-on: ubuntu-latest + timeout-minutes: 20 + name: Commons tests (Java ${{ matrix.JAVA_VERSION }}) + runs-on: ubuntu-24.04 env: USE_STAGE: 'false' # Whether to include the stage repository. + TRAVIS_DIR: hugegraph-server/hugegraph-dist/src/assembly/travis strategy: fail-fast: false matrix: - JAVA_VERSION: ['11'] + JAVA_VERSION: ${{ fromJSON(inputs.java-versions) }} steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 2 + - name: Install JDK ${{ matrix.JAVA_VERSION }} - uses: actions/setup-java@v3 + uses: actions/setup-java@v6 with: java-version: ${{ matrix.JAVA_VERSION }} distribution: 'zulu' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 2 + cache: maven + cache-jdk: false - name: Use staged maven repo settings if: ${{ env.USE_STAGE == 'true' }} run: | - cp $HOME/.m2/settings.xml /tmp/settings.xml - cp -vf .github/configs/settings.xml $HOME/.m2/settings.xml && cat $HOME/.m2/settings.xml + cp "$HOME/.m2/settings.xml" /tmp/settings.xml + cp -vf .github/configs/settings.xml "$HOME/.m2/settings.xml" && cat "$HOME/.m2/settings.xml" - name: Install run: | - mvn install -Dmaven.javadoc.skip=true -ntp -Dmaven.test.skip=true + mvn install -pl hugegraph-commons/hugegraph-common,hugegraph-commons/hugegraph-rpc -am \ + -Dmaven.javadoc.skip=true -ntp -Dmaven.test.skip=true - name: Run common test run: | - mvn test -pl hugegraph-commons/hugegraph-common -Dtest=UnitTestSuite -DskipCommonsTests=false + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + mvn test -pl hugegraph-commons/hugegraph-common -Dtest=UnitTestSuite \ + -DskipCommonsTests=false -Dsurefire.failIfNoSpecifiedTests=true 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" - name: Run rpc test run: | - mvn test -pl hugegraph-commons/hugegraph-rpc -Dtest=UnitTestSuite -DskipCommonsTests=false + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + mvn test -pl hugegraph-commons/hugegraph-rpc -Dtest=UnitTestSuite \ + -DskipCommonsTests=false -Dsurefire.failIfNoSpecifiedTests=true 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} files: hugegraph-commons/target/jacoco.xml + flags: commons disable_search: true fail_ci_if_error: false diff --git a/.github/workflows/docker-build-ci.yml b/.github/workflows/docker-build-ci.yml index 97e2d37a41..5ed8fd5acc 100644 --- a/.github/workflows/docker-build-ci.yml +++ b/.github/workflows/docker-build-ci.yml @@ -18,40 +18,38 @@ name: "Docker Build CI" on: - push: - branches: - - master - - 'release-*' - pull_request: - # Keep source/resources and shared build inputs; skip named documentation files. - paths: - - 'pom.xml' - - '.mvn/**' - - 'style/**' - - '.editorconfig' - - '.github/configs/**' - - 'hugegraph-commons/**' - - 'hugegraph-struct/**' - - 'hugegraph-pd/**' - - 'hugegraph-store/**' - - 'hugegraph-server/**' - - 'hugegraph-cluster-test/**' - - 'install-dist/**' - - 'docker/**' - - '.dockerignore' - - '.github/workflows/docker-build-ci.yml' - - '!**/README*' - - '!**/AGENTS.md' - - '!**/CLAUDE.md' + workflow_call: + inputs: + smoke-only: + type: boolean + default: false +permissions: + contents: read jobs: - docker-bake-check: - runs-on: ubuntu-latest + docker-build: + runs-on: ubuntu-24.04 + timeout-minutes: 90 + env: + IMAGE_TAG: ci-${{ github.run_id }}-${{ github.run_attempt }} + SOURCE_REVISION: ${{ github.sha }} steps: - name: Checkout - uses: actions/checkout@v4 + id: checkout + uses: actions/checkout@v7 + + - name: Install project JDK for deployment contracts + id: java + if: ${{ !cancelled() && steps.checkout.outcome == 'success' && !inputs.smoke-only }} + uses: actions/setup-java@v6 + with: + java-version-file: .github/workflows/.java-version + distribution: 'zulu' + cache: maven + cache-jdk: false - name: Verify shared Maven stages stay identical + if: ${{ !cancelled() && steps.checkout.outcome == 'success' && !inputs.smoke-only }} shell: bash run: | set -euo pipefail @@ -86,6 +84,7 @@ jobs: done - name: Validate shared image build graph + if: ${{ !cancelled() && steps.checkout.outcome == 'success' && !inputs.smoke-only }} run: | set -euo pipefail @@ -108,83 +107,60 @@ jobs: all(. == "docker")) ' /tmp/hugegraph-bake.json - docker-build: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - dockerfile: - - hugegraph-pd/Dockerfile - - hugegraph-store/Dockerfile - - hugegraph-server/Dockerfile - - hugegraph-server/Dockerfile-hstore - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Build ${{ matrix.dockerfile }} + - name: Verify deployment shell and Compose contracts + if: ${{ !cancelled() && steps.checkout.outcome == 'success' && steps.java.outcome == 'success' }} + timeout-minutes: 25 run: | - IMAGE_ID=$(docker build -q --build-arg SOURCE_REVISION="$GITHUB_SHA" \ - -f ${{ matrix.dockerfile }} .) - echo "Built: $IMAGE_ID" - echo "IMAGE_ID=$IMAGE_ID" >> "$GITHUB_ENV" - HC=$(docker inspect --format='{{json .Config.Healthcheck}}' "$IMAGE_ID") - echo "Healthcheck: $HC" - [[ "$HC" != "null" ]] || { echo "ERROR: HEALTHCHECK missing in ${{ matrix.dockerfile }}"; exit 1; } - - - name: Test server entrypoint property mapping - if: matrix.dockerfile == 'hugegraph-server/Dockerfile' - run: bash hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh - - # The startup preflight needs a socket-table tool, and the base image - # ships none of its own. Without one every start reports "unknown" and - # a duplicate start is no longer refused, so assert the image can - # actually answer. Only the server images run check_port. - # TODO(docker-ci): this pins the probe dependency, not the behaviour it - # protects. A full duplicate-start/stop check needs a booted server with - # a backend, which belongs with the e2e job rather than the image build. - - name: Port preflight can answer inside ${{ matrix.dockerfile }} - if: ${{ startsWith(matrix.dockerfile, 'hugegraph-server/') }} - run: | - STATE=$(docker run --rm "$IMAGE_ID" bash -c \ - 'source /hugegraph-server/bin/util.sh && port_listen_state 8080') - echo "port_listen_state 8080 -> $STATE" - # Assert a positive answer rather than "not unknown": an empty $STATE - # would otherwise satisfy the check. The step's default `bash -e` - # already aborts on a failed `docker run`, so this only states intent. - [[ "$STATE" == "free" || "$STATE" == "busy" ]] || { - echo "ERROR: no usable socket-table tool (ss/netstat) in ${{ matrix.dockerfile }}" - exit 1 - } + set -euo pipefail + status=0 + if mvn -B -ntp -pl hugegraph-commons/hugegraph-common -am clean compile dependency:build-classpath \ + -Dmdep.outputFile=target/config-readback-classpath.txt -Dmaven.javadoc.skip=true -DskipTests; then + HG_CONFIG_TEST_CLASSPATH="$PWD/hugegraph-commons/hugegraph-common/target/classes" + HG_CONFIG_TEST_CLASSPATH+=":$(cat hugegraph-commons/hugegraph-common/target/config-readback-classpath.txt)" + export HG_CONFIG_TEST_CLASSPATH + if ! bash hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh; then + status=1 + fi + else + status=1 + fi + if ! bash hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh; then + status=1 + fi + if ! bash hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-port.sh \ + hugegraph-server/hugegraph-dist/src/assembly/static \ + hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh; then + status=1 + fi + if ! bash docker/test-compose.sh render; then + status=1 + fi + exit "$status" - - name: Server image API versions match source - if: ${{ startsWith(matrix.dockerfile, 'hugegraph-server/') }} + - name: Build current images and verify authentication, readiness and graph read/write + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + timeout-minutes: 45 + env: + SMOKE_ONLY: ${{ inputs.smoke-only }} run: | - CHECK_DIR=$(mktemp -d) - trap 'rm -rf "$CHECK_DIR"' EXIT - docker run --rm --entrypoint bash \ - -v "$CHECK_DIR:/check" "$IMAGE_ID" -c \ - 'cp /hugegraph-server/lib/hugegraph-api-*.jar \ - /hugegraph-server/lib/hugegraph-common-*.jar /check/' - - API_JAR=$(find "$CHECK_DIR" -name 'hugegraph-api-*.jar' -print -quit) - COMMON_JAR=$(find "$CHECK_DIR" -name 'hugegraph-common-*.jar' -print -quit) - EXPECTED_MANIFEST=$(sed -n \ - 's|.*\([^<]*\).*|\1|p' \ - hugegraph-server/hugegraph-api/pom.xml) - ACTUAL_MANIFEST=$(unzip -p "$API_JAR" META-INF/MANIFEST.MF | - sed -n 's/^Implementation-Version: *//p' | tr -d '\r') - EXPECTED_PROPERTY=$(sed -n 's/^ApiVersion=//p' \ - hugegraph-commons/hugegraph-common/src/main/resources/version.properties) - ACTUAL_PROPERTY=$(unzip -p "$COMMON_JAR" version.properties | - sed -n 's/^ApiVersion=//p' | tr -d '\r') - - [[ "$ACTUAL_MANIFEST" == "$EXPECTED_MANIFEST" ]] || { - echo "ERROR: API manifest is $ACTUAL_MANIFEST; expected $EXPECTED_MANIFEST" - exit 1 - } - [[ "$ACTUAL_PROPERTY" == "$EXPECTED_PROPERTY" ]] || { - echo "ERROR: API property is $ACTUAL_PROPERTY; expected $EXPECTED_PROPERTY" - exit 1 - } + set -euo pipefail + docker buildx create --driver docker-container --use + docker buildx bake --file docker/bake.hcl --progress=plain \ + --set '*.platform=linux/amd64' \ + pd store server-standalone server-hstore + + # Each runtime check uses successfully built images. Preserve a failed + # check while allowing the independent deployment check to collect results. + status=0 + if ! bash .github/scripts/check-docker-images.sh; then + status=1 + fi + if [[ "$SMOKE_ONLY" != true ]]; then + if ! python3 -m unittest discover -s .github/scripts -p 'test_docker_deployment.py'; then + status=1 + fi + fi + if ! python3 .github/scripts/docker-deployment.py "$IMAGE_TAG"; then + status=1 + fi + exit "$status" diff --git a/.github/workflows/helm-chart-ci.yml b/.github/workflows/helm-chart-ci.yml new file mode 100644 index 0000000000..d60495fbf6 --- /dev/null +++ b/.github/workflows/helm-chart-ci.yml @@ -0,0 +1,536 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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: "Helm Chart CI" + +on: + workflow_call: + +permissions: + contents: read + +jobs: + lint-and-render: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + # azure/setup-helm is not on the ASF-approved actions allowlist, which + # fails the workflow at startup; install the pinned release directly. + - name: install helm + run: | + curl -fsSLo /tmp/helm.tgz https://get.helm.sh/helm-v3.16.2-linux-amd64.tar.gz + echo "9318379b847e333460d33d291d4c088156299a26cd93d570a7f5d0c36e50b5bb /tmp/helm.tgz" | sha256sum -c - + tar -xzf /tmp/helm.tgz -C /tmp + sudo install -m 0755 /tmp/linux-amd64/helm /usr/local/bin/helm + helm version + + - name: helm lint + run: | + helm lint helm/hugegraph + helm lint helm/hugegraph -f helm/hugegraph/values-single.yaml + helm lint helm/hugegraph -f helm/hugegraph/values-cluster.yaml + helm lint helm/hugegraph -f helm/hugegraph/values-strict.yaml + + - name: packaged chart regression + run: python3 -m unittest discover -s helm -p 'test_helm_package.py' -v + + - name: helm unittest + run: | + # v1.0.0 is the newest helm-unittest whose plugin.yaml Helm 3.16.2 can + # parse (v1.1.x adds a platformHooks field that 3.16.2 rejects). + # Pinned by the tag's commit so a moved tag cannot change what runs. + helm plugin install https://github.com/helm-unittest/helm-unittest.git \ + --version ddd5feaa9465e7a3591792524ce7f441d4c5157e + helm unittest helm/hugegraph + + - name: helm template + run: | + for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml" "-f helm/hugegraph/values-strict.yaml"; do + # shellcheck disable=SC2086 # $preset intentionally splits into flags + helm template ci helm/hugegraph $preset > /dev/null + done + # Positive coverage for every hubble wrapper branch: pd mode with + # auth, direct mode with a custom port, and a TLS-less ingress with + # ordinary HTTP configuration, without acknowledgement flags. + helm template ci helm/hugegraph \ + --set hubble.enabled=true \ + --set server.auth.enabled=true \ + --set server.auth.admin.existingSecret=ci-auth > /dev/null + helm template ci helm/hugegraph \ + --set hubble.enabled=true \ + --set hubble.allowWithoutServerAuth=true \ + --set hubble.mode=direct \ + --set hubble.port=9090 \ + --set hubble.persistence.enabled=true \ + --set hubble.ingress.enabled=true \ + --set hubble.ingress.allowPlainHttp=true > /dev/null + # PD sharding knobs rendered as -D system properties, including + # the values-file shape where the count arrives as a numeric string + helm template ci helm/hugegraph \ + --set pd.partition.defaultShardCount=3 \ + --set pd.partition.storeMaxShardCount=12 > /dev/null + helm template ci helm/hugegraph \ + --set-string pd.partition.defaultShardCount=3 > /dev/null + # The derived shard count and the raft whitelist and RPC-timeout flags must land in + # the PD JAVA_OPTS verbatim: shard count 3 on the default topology, + # 1 on the single-node preset, whitelist disabled in both. + helm template ci helm/hugegraph \ + | grep -qF 'value: "-Dpartition.default-shard-count=3 -Draft.ip-whitelist.enabled=false -Draft.rpc-timeout=3000"' + helm template ci helm/hugegraph -f helm/hugegraph/values-single.yaml \ + | grep -qF 'value: "-Dpartition.default-shard-count=1 -Draft.ip-whitelist.enabled=false -Draft.rpc-timeout=3000"' + # The stock distributed install must put every Server replica on + # the shared PD graph catalog, even without Hubble. Auth is on by + # default, so the chart-managed admin Secret must render too. + DEFAULT=$(helm template ci helm/hugegraph) + grep -qF "printf 'usePD=true\\n'" <<<"$DEFAULT" + grep -qF "printf 'pd.peers=%s\\n' \"\${HG_SERVER_PD_PEERS}\"" \ + <<<"$DEFAULT" + grep -qF 'value: "ci-hugegraph-pd-0.ci-hugegraph-pd.default.svc:8686,ci-hugegraph-pd-1.ci-hugegraph-pd.default.svc:8686,ci-hugegraph-pd-2.ci-hugegraph-pd.default.svc:8686"' \ + <<<"$DEFAULT" + grep -qE '^kind: Secret$' <<<"$DEFAULT" + grep -qF 'name: ci-admin' <<<"$DEFAULT" + grep -qF 'name: ci-auth-token' <<<"$DEFAULT" + # A YAML-coercible Secret key must render quoted, or the API + # server stores the key as a boolean and the pods cannot find it. + helm template ci helm/hugegraph --set-string server.auth.admin.key=on \ + | grep -qF '"on":' + # The install render must hash the generated credentials it writes: + # the rollout checksums equal sha256(name|key|sha256(value)) over + # the rendered Secret data, so the first upgrade, which reads the + # same values back through lookup, renders the same checksums and + # rolls nothing. + python3 - <<'PY' + import hashlib, base64, subprocess, yaml + out = subprocess.check_output(['helm', 'template', 'ci', 'helm/hugegraph']) + docs = [d for d in yaml.safe_load_all(out) if d] + h = lambda s: hashlib.sha256(s.encode()).hexdigest() + sec = {d['metadata']['name']: d['data'] for d in docs if d['kind'] == 'Secret'} + val = lambda n, k: base64.b64decode(sec[n][k]).decode() + pd = next(d for d in docs if d['kind'] == 'StatefulSet' and d['metadata']['name'] == 'ci-hugegraph-pd') + srv = next(d for d in docs if d['kind'] == 'Deployment' and d['metadata']['name'] == 'ci-hugegraph-server') + ann = lambda d, k: d['spec']['template']['metadata']['annotations'][k] + assert ann(pd, 'checksum/pd-auth') == h('ci-pd-auth|secret-key|' + h(val('ci-pd-auth', 'secret-key'))) + assert ann(srv, 'checksum/pd-auth') == ann(pd, 'checksum/pd-auth') + assert ann(srv, 'checksum/auth') == h('ci-admin|password|ci-auth-token|token_secret|' + + h(val('ci-admin', 'password')) + '|' + h(val('ci-auth-token', 'token_secret'))) + print('install-time checksums match the generated Secret values') + services = {d['metadata']['name']: d['spec'] for d in docs if d['kind'] == 'Service'} + assert services['ci-hugegraph-server']['type'] == 'NodePort' + assert services['ci-hugegraph-hubble']['type'] == 'NodePort' + assert services['ci-hugegraph-pd-client']['type'] == 'ClusterIP' + for name in ('ci-hugegraph-server', 'ci-hugegraph-hubble'): + assert 'nodePort' not in services[name]['ports'][0] + sts = [d for d in docs if d['kind'] == 'StatefulSet'] + assert sum(int(d['spec']['replicas']) * len(d['spec'].get('volumeClaimTemplates', [])) + for d in sts) + sum(d['kind'] == 'PersistentVolumeClaim' for d in docs) == 7 + assert not any(d['kind'] == 'NetworkPolicy' for d in docs) + assert not any('curlimages/' in str(d) for d in docs) + print('easy defaults: three-member groups, seven volumes and direct HTTP NodePorts') + PY + + - name: reject invalid values + run: | + # Each case must fail to render; the schema and helpers are the + # contract. `! cmd` alone is NOT enforced under `set -e` (errexit + # ignores inverted commands), so every case checks explicitly. + must_fail() { + if helm template ci helm/hugegraph "$@" >/dev/null 2>&1; then + echo "expected render failure for: $*" >&2 + exit 1 + fi + } + # Kubernetes permits successThreshold > 1 only for readiness. + for component in pd store server hubble; do + for probe in startup liveness; do + must_fail --set "${component}.probes.${probe}.successThreshold=2" + done + helm template ci helm/hugegraph \ + --set "${component}.probes.readiness.successThreshold=2" > /dev/null + done + # These deployment inputs must serialize unchanged through the properties reader. + must_fail --set-string server.serviceId=café + must_fail --set-string server.serviceId=😀 + must_fail --set-string "$(printf 'server.serviceId=ops\tteam')" + # Match the Server API graph-space names, including its 48-character limit. + must_fail --set-string server.graphSpace=TeamA + must_fail --set-string server.graphSpace=team-a + must_fail --set-string server.graphSpace=1team + graph_space_max=$(printf 'a%.0s' {1..48}) + helm template ci helm/hugegraph --set-string "server.graphSpace=${graph_space_max}" > /dev/null + must_fail --set-string "server.graphSpace=${graph_space_max}a" + # The hook queries an explicit, fully qualified existing graph binding. + must_fail --set-string server.testGraphAlias= + must_fail --set-string server.testGraphAlias=team_a + must_fail --set-string server.testGraphAlias=team_a-demo-name + must_fail --set-string "server.testGraphAlias=team_a-demo'" + graph_name_max=$(printf 'g%.0s' {1..48}) + helm template ci helm/hugegraph \ + --set-string "server.testGraphAlias=${graph_space_max}-${graph_name_max}" > /dev/null + must_fail --set-string "server.testGraphAlias=${graph_space_max}a-demo" + must_fail --set-string "server.testGraphAlias=team_a-${graph_name_max}g" + must_fail --set pd.replicas=100 + must_fail --set pd.pdb.minAvailable=3 + must_fail --set server.hpa.enabled=true + must_fail \ + --set server.hpa.enabled=true \ + --set server.hpa.minReplicas=2 \ + --set server.resources.requests.cpu=100m \ + --set server.pdb.enabled=true \ + --set server.pdb.minAvailable=2 + must_fail --set server.auth.enabled=true --set server.auth.admin.autoGenerate=false --set server.auth.token.autoGenerate=false + # pd.auth with every source disabled fails the schema anyOf; the + # unit framework at v1.0.0 cannot match schema aborts, so the case + # lives here. + must_fail --set pd.auth.autoGenerate=false + # Commons Configuration trims a properties value, so a credential + # with surrounding whitespace would be stored in the Secret and + # applied to the account without it. The schema rejects both ends; + # these are schema aborts, so they live here rather than in the + # unit suite. --set-string keeps the value a string. + must_fail --set-string "server.auth.admin.password=review-secret " + must_fail --set-string "$(printf 'server.auth.admin.password=review-secret\t')" + must_fail --set-string "pd.auth.value=review-secret " + # An inner space in the admin password fails too: the image + # entrypoint stores it with a backslash before each space, so the + # account would answer 401 to the configured value. The PD secret + # is not written by that encoder, so its inner space stays legal. + must_fail --set-string "server.auth.admin.password=review secret" + helm template ci helm/hugegraph \ + --set-string "pd.auth.value=review secret" >/dev/null + # The Server decodes Basic-auth credentials as ASCII, so a non-ASCII + # admin password would be applied and then answer 401 to every + # request; the schema refuses it before install, as it already + # does for the PD REST secret. Backslashes and tabs go the same way. + must_fail --set-string "server.auth.admin.password=密码abc" + must_fail --set-string "server.auth.admin.password=café" + # --set unescapes a single backslash, so the literal needs two. + must_fail --set-string 'server.auth.admin.password=review\\secret' + must_fail --set-string "$(printf 'server.auth.admin.password=review\tsecret')" + must_fail --set-string "pd.auth.value=密码abc" + # The Server splits the decoded credential on every colon (400). + must_fail --set-string 'server.auth.admin.password=review:secret' + helm template ci helm/hugegraph \ + --set-string "server.auth.admin.password=review-secret_42!" >/dev/null + # Auth defaults to on, so Hubble alone is valid; refuse Hubble only + # when authentication is explicitly disabled. + must_fail --set hubble.enabled=true --set server.auth.enabled=false + A=(--set hubble.enabled=true --set hubble.allowWithoutServerAuth=true) + must_fail "${A[@]}" --set hubble.port=0 + must_fail "${A[@]}" \ + --set hubble.persistence.enabled=true \ + --set hubble.persistence.size="" + must_fail "${A[@]}" --set hubble.service.type=ClusterIP --set hubble.service.nodePort=30080 + # HTTP is supported directly in both modes. Component choices take + # precedence over mode defaults and require no acknowledgement. + for mode in easy strict; do + for component in pd server hubble; do + for kind in NodePort LoadBalancer; do + helm template ci helm/hugegraph --set "mode=$mode" \ + --set "${component}.service.type=$kind" > /dev/null + done + done + helm template ci helm/hugegraph --set "mode=$mode" \ + --set server.ingress.enabled=true \ + --set hubble.ingress.enabled=true > /dev/null + done + must_fail "${A[@]}" --set hubble.mode=bogus + must_fail "${A[@]}" --set hubble.image.tag="" + # When UI is explicitly disabled, stale credentials may remain unused. + helm template ci helm/hugegraph --set hubble.enabled=false \ + --set server.auth.enabled=false \ + --set server.auth.admin.password=unused-secret \ + --set server.auth.token.value=unused-token-that-is-at-least-32-bytes > /dev/null + # Respect custom API-server NodePort ranges rather than assuming 30000+. + helm template ci helm/hugegraph \ + --set server.service.nodePort=8802 \ + --set hubble.service.nodePort=8808 > /dev/null + must_fail --set server.service.nodePort=65536 + must_fail --set server.probes.startup.failureThreshold=1 + # Kubernetes refuses a StatefulSet that carries rollingUpdate + # options with updateStrategy OnDelete; the schema rejects the + # combination up front (a schema abort, so it lives here). + for component in pd store; do + must_fail \ + --set "${component}.updateStrategy.type=OnDelete" \ + --set "${component}.updateStrategy.rollingUpdate.partition=1" + helm template ci helm/hugegraph \ + --set "${component}.updateStrategy.type=OnDelete" > /dev/null + done + # maxUnavailable above 1 lets the controller disrupt two members + # of a three-member raft group or shard at once, and PDBs do not + # constrain controller rollouts; the schema accepts integer 1 only. + for component in pd store; do + must_fail --set "${component}.updateStrategy.rollingUpdate.maxUnavailable=2" + must_fail --set "${component}.updateStrategy.rollingUpdate.maxUnavailable=0" + must_fail --set "${component}.updateStrategy.rollingUpdate.maxUnavailable=-1" + must_fail --set-string "${component}.updateStrategy.rollingUpdate.maxUnavailable=67%" + must_fail --set-string "${component}.updateStrategy.rollingUpdate.maxUnavailable=foo" + must_fail --set-string "${component}.updateStrategy.rollingUpdate.maxUnavailable=1" + helm template ci helm/hugegraph \ + --set "${component}.updateStrategy.rollingUpdate.maxUnavailable=1" > /dev/null + done + # PD PDB must keep the Raft majority: floor(replicas/2)+1 + must_fail --set pd.replicas=5 --set pd.pdb.minAvailable=2 + must_fail --set pd.replicas=4 --set pd.pdb.minAvailable=2 + helm template ci helm/hugegraph \ + --set pd.replicas=5 --set pd.pdb.minAvailable=3 > /dev/null + helm template ci helm/hugegraph \ + --set pd.replicas=5 --set pd.pdb.minAvailable=4 > /dev/null + # PD sharding knobs: empty or a positive integer; an explicit + # shard count must be odd and stay within the store count + must_fail --set pd.partition.defaultShardCount=0 + must_fail --set pd.partition.defaultShardCount=-1 + must_fail --set-string pd.partition.defaultShardCount=abc + must_fail --set pd.partition.defaultShardCount=2 + must_fail --set pd.partition.defaultShardCount=5 + must_fail --set pd.partition.storeMaxShardCount=0 + # extraEnv must not override chart-managed variables + must_fail --set 'server.extraEnv[0].name=HG_SERVER_INIT_STORE_ENABLED' \ + --set 'server.extraEnv[0].value=true' + must_fail --set 'pd.extraEnv[0].name=HG_PD_RAFT_PEERS_LIST' \ + --set 'pd.extraEnv[0].value=x' + must_fail --set 'store.extraEnv[0].name=HG_STORE_PD_ADDRESS' \ + --set 'store.extraEnv[0].value=x' + # JAVA_OPTIONS is reserved: a preset value makes the start scripts + # skip auto heap sizing and drop the chart's JAVA_OPTS entirely + must_fail --set 'pd.extraEnv[0].name=JAVA_OPTIONS' \ + --set 'pd.extraEnv[0].value=-Xmx1g' + + - name: server credential wrapper behaviour + shell: bash + run: | + # An existingSecret is only seen when the container starts, so run + # the rendered Server wrapper against Secret values. A valid value + # passes the checks and stops at the missing properties file; an + # unsupported one stops earlier with its own message. + set -euo pipefail + helm template ci helm/hugegraph -s templates/server-deployment.yaml \ + --set server.auth.admin.existingSecret=ci-byo > /tmp/srv.yaml + python3 - <<'PY' + import yaml + d = yaml.safe_load(open('/tmp/srv.yaml')) + open('/tmp/srv.sh', 'w').write(d['spec']['template']['spec']['containers'][0]['args'][0]) + PY + mkdir -p /tmp/srv-run + run() { + (cd /tmp/srv-run && PASSWORD="$1" bash /tmp/srv.sh 2>&1) || true + } + expect() { + local out + out=$(run "$1") + if ! grep -qF -- "$2" <<<"$out"; then + printf 'password %q: expected "%s", got:\n%s\n' "$1" "$2" "$out" >&2 + exit 1 + fi + } + expect 'review-secret_42!' 'missing or unreadable ./conf/rest-server.properties' + expect 'ASCII inner space 123!?' 'must not contain spaces' + expect ' leading' 'must not contain spaces' + expect 'trailing ' 'must not contain spaces' + expect 'review:secret' 'must not contain a colon' + expect 'review\secret' 'must not contain backslashes' + expect "$(printf 'review\tsecret')" 'must be printable ASCII' + expect 'café' 'must be printable ASCII' + + - name: helm test query assertions and shell quoting + run: | + # Execute the rendered query with the same Groovy version as Server. + GROOVY_VERSION=$(python3 - <<'PY_VERSION' + import xml.etree.ElementTree as ET + pom = ET.parse('pom.xml') + print(pom.find('{*}properties/{*}groovy.version').text) + PY_VERSION + ) + GROOVY_URL="https://repo.maven.apache.org/maven2/org/apache/groovy/groovy/${GROOVY_VERSION}/groovy-${GROOVY_VERSION}.jar" + curl -fsSLo /tmp/groovy.jar "${GROOVY_URL}" + curl -fsSLo /tmp/groovy.jar.sha1 "${GROOVY_URL}.sha1" + printf '%s /tmp/groovy.jar\n' "$(cat /tmp/groovy.jar.sha1)" | sha1sum -c - + GROOVY_JAR=/tmp/groovy.jar python3 helm/test_helm_hook.py + + - name: helm test hook behaviour + shell: bash + run: | + # The hook must fail when any one Server Pod cannot answer a + # graph-bound Gremlin query, even while /versions and /graphs + # answer 200 through the Service: a Server that started during a + # PD roll stays in exactly that state for its whole life. Run the + # rendered hook script against loopback fixtures. + set -euo pipefail + sudo sh -c 'echo "127.0.0.1 ci-hugegraph-server ci-hugegraph-server-headless" >> /etc/hosts' + helm template ci helm/hugegraph -s templates/tests/test-connection.yaml > /tmp/hook.yaml + python3 - <<'PY' + import yaml + d = yaml.safe_load(open('/tmp/hook.yaml')) + open('/tmp/hook.sh', 'w').write(d['spec']['containers'][0]['command'][2]) + PY + grep -q 'DEFAULT-hugegraph' /tmp/hook.sh + # getent stub: the headless lookup answers with the fixture IPs. + mkdir -p /tmp/stub + printf '#!/bin/sh\nfor ip in ${STUB_IPS}; do printf "%%s STREAM host\\n" "$ip"; done\n' > /tmp/stub/getent + chmod +x /tmp/stub/getent + cat > /tmp/fixture.py <<'PY' + import http.server, json, os, sys, threading, time + calls = {} + class H(http.server.BaseHTTPRequestHandler): + def _send(self, code, body): + self.send_response(code) + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', str(len(body))) + self.end_headers() + self.wfile.write(body) + def do_GET(self): + self._send(200, b'{"versions":{}}') + def do_POST(self): + body = json.loads(self.rfile.read(int(self.headers.get('Content-Length', 0)))) + expected_alias = ('team_a-demo' if os.path.exists('/tmp/selected-graph') + else 'DEFAULT-hugegraph') + ip = self.server.server_address[0] + # broken-: every Gremlin call fails. flip-odd- / + # flip-even-: the odd or even calls fail, counted from + # the first call made while the file exists, so two Pods + # on opposite phases are never healthy in the same sweep. + odd = os.path.exists('/tmp/flip-odd-' + ip) + even = os.path.exists('/tmp/flip-even-' + ip) + n = calls[ip] = (calls.get(ip, 0) + 1) if (odd or even) else 0 + broken = (os.path.exists('/tmp/broken-' + ip) + or body.get('aliases', {}).get('graph') != expected_alias + or (odd and n % 2 == 1) + or (even and n % 2 == 0)) + if broken: + self._send(400, b'{"exception":"Could not rebind [graph]"}') + else: + self._send(200, b'{"result":{"data":[0]}}') + def log_message(self, *a): + pass + for ip in sys.argv[1:]: + s = http.server.HTTPServer((ip, 8080), H) + threading.Thread(target=s.serve_forever, daemon=True).start() + time.sleep(3600) + PY + python3 /tmp/fixture.py 127.0.0.1 127.0.0.2 127.0.0.3 & + sleep 1 + run_hook() { + PATH="/tmp/stub:${PATH}" PASSWORD=ci-password HG_TEST_DEADLINE_S=8 \ + STUB_IPS="$1" sh "${2:-/tmp/hook.sh}" + } + # Healthy: three Pods, all Gremlin-bound. + run_hook "127.0.0.1 127.0.0.2 127.0.0.3" + # One Pod answers REST but not Gremlin: the hook must fail. + touch /tmp/broken-127.0.0.3 + if run_hook "127.0.0.1 127.0.0.2 127.0.0.3"; then + echo "expected the hook to fail on a broken-binding Pod" >&2 + exit 1 + fi + rm /tmp/broken-127.0.0.3 + # Fewer Pods than the replica floor: the hook must fail. + if run_hook "127.0.0.1"; then + echo "expected the hook to fail below the replica floor" >&2 + exit 1 + fi + # Alternating health: sweep 1 sees A ok, B broken; sweep 2 sees A + # broken, B ok; and so on. No single sweep is clean, so the hook + # must fail. A hook that remembered earlier successes per Pod would + # pass here after the second sweep. + touch /tmp/flip-even-127.0.0.1 /tmp/flip-odd-127.0.0.2 + if run_hook "127.0.0.1 127.0.0.2 127.0.0.3"; then + echo "expected the hook to fail when no sweep is completely healthy" >&2 + exit 1 + fi + rm /tmp/flip-even-127.0.0.1 /tmp/flip-odd-127.0.0.2 + # Dual-stack: each Pod publishes an IPv4 and an IPv6 address. Two + # Pods give four addresses, which must not satisfy a floor of + # three; three Pods must pass without the IPv6 addresses being + # queried (nothing listens on them, so a query would fail). + if run_hook "127.0.0.1 127.0.0.2 ::1 ::2"; then + echo "expected the hook to count Pods, not addresses, on dual-stack" >&2 + exit 1 + fi + run_hook "127.0.0.1 127.0.0.2 127.0.0.3 ::1 ::2 ::3" + # A non-default graph space must query the explicitly selected graph. + # A missing alias still fails rather than weakening the per-Pod sweep. + touch /tmp/selected-graph + for alias in team_a-demo team_a-missing; do + helm template ci helm/hugegraph -s templates/tests/test-connection.yaml \ + --set-string server.graphSpace=team_a \ + --set-string "server.testGraphAlias=${alias}" > "/tmp/hook-${alias}.yaml" + done + python3 - <<'PY' + import yaml + for alias in ('team_a-demo', 'team_a-missing'): + d = yaml.safe_load(open('/tmp/hook-' + alias + '.yaml')) + open('/tmp/hook-' + alias + '.sh', 'w').write(d['spec']['containers'][0]['command'][2]) + PY + run_hook "127.0.0.1 127.0.0.2 127.0.0.3" /tmp/hook-team_a-demo.sh + if run_hook "127.0.0.1 127.0.0.2 127.0.0.3" /tmp/hook-team_a-missing.sh; then + echo "expected the hook to fail on a missing selected graph" >&2 + exit 1 + fi + rm /tmp/selected-graph + # The auth-off render must carry no credentials. + if helm template ci helm/hugegraph -s templates/tests/test-connection.yaml \ + --set hubble.enabled=false --set server.auth.enabled=false | grep -q 'Authorization'; then + echo "the auth-off hook render must not carry credentials" >&2 + exit 1 + fi + + - name: kubeconform + shell: bash + run: | + set -o pipefail + curl -sSLo /tmp/kc.tar.gz https://github.com/yannh/kubeconform/releases/download/v0.6.7/kubeconform-linux-amd64.tar.gz + echo "95f14e87aa28c09d5941f11bd024c1d02fdc0303ccaa23f61cef67bc92619d73 /tmp/kc.tar.gz" | sha256sum -c - + tar -xzf /tmp/kc.tar.gz -C /tmp + for preset in "" "-f helm/hugegraph/values-single.yaml" "-f helm/hugegraph/values-cluster.yaml" "-f helm/hugegraph/values-strict.yaml"; do + # shellcheck disable=SC2086 # $preset intentionally splits into flags + helm template ci helm/hugegraph $preset | /tmp/kubeconform -strict -summary -kubernetes-version 1.23.0 + done + helm template ci helm/hugegraph \ + --set hubble.enabled=true \ + --set hubble.allowWithoutServerAuth=true \ + --set hubble.mode=direct \ + --set hubble.port=9090 \ + --set hubble.persistence.enabled=true \ + --set hubble.ingress.enabled=true \ + --set hubble.ingress.allowPlainHttp=true \ + | /tmp/kubeconform -strict -summary -kubernetes-version 1.23.0 + helm template ci helm/hugegraph \ + --set hubble.enabled=true \ + --set server.auth.enabled=true \ + --set server.auth.admin.existingSecret=ci-auth \ + | /tmp/kubeconform -strict -summary -kubernetes-version 1.23.0 + helm template ci helm/hugegraph \ + --set pd.partition.defaultShardCount=3 \ + --set pd.partition.storeMaxShardCount=12 \ + | /tmp/kubeconform -strict -summary -kubernetes-version 1.23.0 + + - name: legacy --reuse-values compatibility + run: | + # A release created before a field existed must still render. This has + # regressed five times, so it is guarded here rather than by review. + # `-f` is NOT equivalent: it merges over the new defaults, whereas + # --reuse-values discards them, so the fixture must become values.yaml. + LEGACY_ROOT=$(mktemp -d) + cp -R helm/hugegraph "$LEGACY_ROOT/chart" + cp helm/hugegraph/testdata/values-pre-hardening.yaml "$LEGACY_ROOT/chart/values.yaml" + helm template legacy "$LEGACY_ROOT/chart" > /dev/null + helm template legacy "$LEGACY_ROOT/chart" --is-upgrade > /dev/null + + - name: helm package + run: helm package helm/hugegraph -d /tmp/chart diff --git a/.github/workflows/licence-checker.yml b/.github/workflows/licence-checker.yml deleted file mode 100644 index a6e6990a64..0000000000 --- a/.github/workflows/licence-checker.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: "License Checker" - -on: - push: - branches: - - master - - 'release-*' - pull_request: - -jobs: - check-license: - runs-on: ubuntu-latest - env: - USE_STAGE: 'false' # Whether to include the stage repository. - steps: - - uses: actions/checkout@v4 - - - name: Check License Header - uses: apache/skywalking-eyes@main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - log: info - token: ${{ github.token }} - config: .licenserc.yaml - - - name: License Check (RAT) - run: | - mvn apache-rat:check -ntp - find ./ -name rat.txt -print0 | xargs -0 -I file cat file > merged-rat.txt - grep "Binaries" merged-rat.txt -C 3 && cat merged-rat.txt diff --git a/.github/workflows/pd-store-ci.yml b/.github/workflows/pd-store-ci.yml index 79217cfc58..ceb1d1121c 100644 --- a/.github/workflows/pd-store-ci.yml +++ b/.github/workflows/pd-store-ci.yml @@ -1,63 +1,117 @@ name: "HugeGraph-PD & Store & Hstore CI" on: - push: - branches: - - master - - 'release-*' - - 'test-*' - pull_request: - # Keep source/resources and shared build inputs; skip named documentation files. - paths: - - 'pom.xml' - - '.mvn/**' - - 'style/**' - - '.editorconfig' - - '.github/configs/**' - - 'hugegraph-commons/**' - - 'hugegraph-struct/**' - - 'hugegraph-pd/**' - - 'hugegraph-store/**' - - 'hugegraph-server/**' - - '.github/workflows/pd-store-ci.yml' - - 'docker/**' - - 'install-dist/**' - - '!**/README*' - - '!**/AGENTS.md' - - '!**/CLAUDE.md' - -# TODO: consider merge to one ci.yml file + workflow_call: + inputs: + run-struct: + type: boolean + default: true + run-pd: + type: boolean + default: true + run-store: + type: boolean + default: true + run-hstore: + type: boolean + default: true +permissions: + contents: read + jobs: - struct: - runs-on: ubuntu-latest + rocksdb-compatibility: + if: inputs.run-pd || inputs.run-store || inputs.run-hstore + permissions: + contents: read + runs-on: ubuntu-24.04 env: - USE_STAGE: 'false' + BEFORE_SHA: ${{ github.event.pull_request.base.sha || github.event.before || github.sha }} + AFTER_SHA: ${{ github.event.pull_request.head.sha || github.event.after || github.sha }} steps: - - name: Install JDK 11 - uses: actions/setup-java@v3 + - name: Require exact comparison revisions + run: | + for revision in "$BEFORE_SHA" "$AFTER_SHA"; do + if [[ ! "$revision" =~ ^[0-9a-f]{40}$ || "$revision" =~ ^0+$ ]]; then + echo "Error: cannot determine the before/after revision for RocksDB compatibility" >&2 + exit 1 + fi + done + + - name: Checkout before + uses: actions/checkout@v7 + with: + ref: ${{ env.BEFORE_SHA }} + path: before + persist-credentials: false + + - name: Checkout after + uses: actions/checkout@v7 with: - java-version: '11' + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ env.AFTER_SHA }} + path: after + persist-credentials: false + + - name: Install project JDK + uses: actions/setup-java@v6 + with: + java-version-file: after/.github/workflows/.java-version distribution: 'zulu' - - name: Cache Maven packages - uses: actions/cache@v3 + - name: Test only changed RocksDB version pairs + run: | + bash after/hugegraph-server/hugegraph-test/src/test/rocksdb-compatibility/run.sh \ + "$GITHUB_WORKSPACE/before" "$GITHUB_WORKSPACE/after" \ + "$RUNNER_TEMP/rocksdb-compatibility" + + - name: Preserve comparison and synthetic-data failure evidence + if: always() + uses: actions/upload-artifact@v7 with: - path: ~/.m2/repository - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + name: rocksdb-compatibility-${{ github.sha }} + path: | + ${{ runner.temp }}/rocksdb-compatibility/run.log + ${{ runner.temp }}/rocksdb-compatibility/models/ + ${{ runner.temp }}/rocksdb-compatibility/pairs.tsv + ${{ runner.temp }}/rocksdb-compatibility/cases/ + struct: + if: inputs.run-struct || inputs.run-store + runs-on: ubuntu-24.04 + env: + USE_STAGE: 'false' + TRAVIS_DIR: hugegraph-server/hugegraph-dist/src/assembly/travis + steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 5 + - name: Install project JDK + uses: actions/setup-java@v6 + with: + java-version-file: .github/workflows/.java-version + distribution: 'zulu' + cache: maven + cache-jdk: false + + - name: Verify Store stop failure propagation and PID retention + run: bash hugegraph-server/hugegraph-dist/src/assembly/travis/test-stop-hugegraph-store.sh + + - name: Run Java runtime log checker tests + run: bash "$TRAVIS_DIR/test-check-java-runtime-logs.sh" + - name: Run JaCoCo report validator tests run: hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh - name: Run Codecov upload configuration tests run: hugegraph-server/hugegraph-dist/src/assembly/travis/test-codecov-upload-config.sh + - name: Check TinkerPop branch and report gates + run: bash hugegraph-server/hugegraph-dist/src/assembly/travis/test-tinkerpop-reports.sh + - name: Use staged maven repo settings + if: ${{ env.USE_STAGE == 'true' }} run: | cp $HOME/.m2/settings.xml /tmp/settings.xml || true mv -vf .github/configs/settings.xml $HOME/.m2/settings.xml @@ -79,11 +133,15 @@ jobs: - name: Run hugegraph-struct test if: ${{ hashFiles('hugegraph-struct/pom.xml') != '' }} run: | - mvn -U -ntp -pl hugegraph-struct test + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + mvn -U -ntp -pl hugegraph-struct test 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" pd: - needs: struct - runs-on: ubuntu-latest + if: inputs.run-pd + timeout-minutes: 30 + runs-on: ubuntu-24.04 env: # TODO: avoid duplicated env setup in pd & store USE_STAGE: 'false' # Whether to include the stage repository. @@ -93,24 +151,19 @@ jobs: TEST_REPORT_DIR: hugegraph-pd/hg-pd-test/target/surefire-reports steps: - - name: Install JDK 11 - uses: actions/setup-java@v3 - with: - java-version: '11' - distribution: 'zulu' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 5 + - name: Install project JDK + uses: actions/setup-java@v6 + with: + java-version-file: .github/workflows/.java-version + distribution: 'zulu' + cache: maven + cache-jdk: false + - name: use staged maven repo settings if: ${{ env.USE_STAGE == 'true' }} run: | @@ -130,13 +183,47 @@ jobs: # clean package so their JaCoCo data survives until the final report. - name: Run common test run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") mvn test -pl hugegraph-pd/hg-pd-test -am \ - -P pd-common-test -Djacoco.sessionId=pd-common-test + -P pd-common-test -Djacoco.sessionId=pd-common-test 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" - name: Run core test run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") mvn test -pl hugegraph-pd/hg-pd-test -am \ - -P pd-core-test -Djacoco.sessionId=pd-core-test + -P pd-core-test -Djacoco.sessionId=pd-core-test 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" + + - name: Verify Surefire single-class routing + run: | + SUREFIRE_DIR=hugegraph-pd/hg-pd-test/target/surefire-reports + REPORT="$SUREFIRE_DIR/TEST-org.apache.hugegraph.pd.core.PDConfigTest.xml" + rm -f "$REPORT" + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + mvn test -pl hugegraph-pd/hg-pd-test -am \ + -P pd-core-test -Dtest=PDConfigTest -ntp 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" + test -s "$REPORT" || { + echo "::error::PD single-class test produced no report" + exit 1 + } + NEGATIVE_LOG=$(mktemp) + trap 'rm -f "$NEGATIVE_LOG"' EXIT + if mvn test -pl hugegraph-pd/hg-pd-test -am \ + -P pd-core-test -Dtest=NoSuchJava17ContractTest -ntp \ + >"$NEGATIVE_LOG" 2>&1; then + cat "$NEGATIVE_LOG" + echo "::error::Surefire accepted a missing PD test class" + exit 1 + fi + cat "$NEGATIVE_LOG" + grep -Fq 'No tests matching pattern "NoSuchJava17ContractTest"' \ + "$NEGATIVE_LOG" + grep -Fq '(pd-core-test) @ hg-pd-test' "$NEGATIVE_LOG" - name: Check startup test prerequisites (PD) id: pd-preflight @@ -147,11 +234,16 @@ jobs: if ! command -v "$tool" >/dev/null 2>&1; then echo "can_run=false" >> "$GITHUB_OUTPUT" echo "skip_reason=missing tool: $tool" >> "$GITHUB_OUTPUT" - exit 0 + exit 1 fi done echo "can_run=true" >> "$GITHUB_OUTPUT" + - name: Check explicit Topling launcher selection + run: | + bash $TRAVIS_DIR/test-topling-selection.sh + bash hugegraph-store/hg-store-dist/src/assembly/travis/test-store-preloads.sh + - name: Run PD docker entrypoint secret override tests run: | $TRAVIS_DIR/test-pd-docker-entrypoint.sh @@ -175,16 +267,24 @@ jobs: - name: Prepare env and service run: | $TRAVIS_DIR/start-pd.sh + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "PD_DIR=hugegraph-pd/apache-hugegraph-pd-$VERSION" >> "$GITHUB_ENV" - name: Run client test run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") mvn test -pl hugegraph-pd/hg-pd-test -am \ - -P pd-client-test -Djacoco.sessionId=pd-client-test + -P pd-client-test -Djacoco.sessionId=pd-client-test 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" - name: Run rest test run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") mvn test -pl hugegraph-pd/hg-pd-test -am \ - -P pd-rest-test -Djacoco.sessionId=pd-rest-test + -P pd-rest-test -Djacoco.sessionId=pd-rest-test 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" - name: Generate aggregate coverage report run: | @@ -212,17 +312,36 @@ jobs: "$REPORT_FILE" \ hg-pd-grpc hg-pd-common hg-pd-client hg-pd-core hg-pd-service hg-pd-dist + - name: Show PD diagnostics on failure + if: failure() + run: | + VERSION=$(grep -E '^VersionInBash=' \ + hugegraph-commons/hugegraph-common/src/main/resources/version.properties | cut -d'=' -f2-) + [[ "$VERSION" =~ ^[0-9A-Za-z._-]+$ ]] || { + echo "Invalid VersionInBash: $VERSION" + exit 1 + } + PD_DIR=hugegraph-pd/apache-hugegraph-pd-$VERSION + bash $TRAVIS_DIR/ci-service-utils.sh dump "$PD_DIR" HugeGraphPD || true + find . -path '*/surefire-reports/*' -type f \ + \( -name '*.txt' -o -name '*.xml' \) | sort | while read -r report; do + echo "--- tail -n 120 $report ---" + tail -n 120 "$report" || true + done + - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} files: ${{ env.REPORT_FILE }} + flags: pd disable_search: true fail_ci_if_error: false store: - needs: struct - runs-on: ubuntu-latest + if: inputs.run-store + timeout-minutes: 45 + runs-on: ubuntu-24.04 env: USE_STAGE: 'false' # Whether to include the stage repository. # TODO: remove outdated env @@ -231,24 +350,19 @@ jobs: TEST_REPORT_DIR: hugegraph-store/hg-store-test/target/surefire-reports steps: - - name: Install JDK 11 - uses: actions/setup-java@v3 - with: - java-version: '11' - distribution: 'zulu' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 5 + - name: Install project JDK + uses: actions/setup-java@v6 + with: + java-version-file: .github/workflows/.java-version + distribution: 'zulu' + cache: maven + cache-jdk: false + - name: use staged maven repo settings if: ${{ env.USE_STAGE == 'true' }} run: | @@ -260,6 +374,40 @@ jobs: run: | mvn clean package -U -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -ntp --fail-at-end + - name: Verify Store Arthas local CLI attachment + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + STORE_DIR=hugegraph-store/apache-hugegraph-store-$VERSION + bash "$TRAVIS_DIR/test-arthas-cli.sh" "$STORE_DIR" + + - name: Verify Surefire single-class routing + run: | + SUREFIRE_DIR=hugegraph-store/hg-store-test/target/surefire-reports + REPORT="$SUREFIRE_DIR/TEST-org.apache.hugegraph.store.raftcore.ZeroByteStringHelperTest.xml" + rm -f "$REPORT" + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + mvn test -pl hugegraph-store/hg-store-test -am \ + -P store-raftcore-test -Dtest=ZeroByteStringHelperTest -ntp 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" + test -s "$REPORT" || { + echo "::error::Store single-class test produced no report" + exit 1 + } + NEGATIVE_LOG=$(mktemp) + trap 'rm -f "$NEGATIVE_LOG"' EXIT + if mvn test -pl hugegraph-store/hg-store-test -am \ + -P store-raftcore-test -Dtest=NoSuchJava17ContractTest -ntp \ + >"$NEGATIVE_LOG" 2>&1; then + cat "$NEGATIVE_LOG" + echo "::error::Surefire accepted a missing Store test class" + exit 1 + fi + cat "$NEGATIVE_LOG" + grep -Fq 'No tests matching pattern "NoSuchJava17ContractTest"' \ + "$NEGATIVE_LOG" + grep -Fq '(store-raftcore-test) @ hg-store-test' "$NEGATIVE_LOG" + - name: Check source formatting run: | mvn editorconfig:check -pl hugegraph-store/hg-store-test -am -ntp @@ -273,14 +421,14 @@ jobs: if ! command -v "$tool" >/dev/null 2>&1; then echo "can_run=false" >> "$GITHUB_OUTPUT" echo "skip_reason=missing tool: $tool" >> "$GITHUB_OUTPUT" - exit 0 + exit 1 fi done LIMIT_N=$(ulimit -n) if [[ "$LIMIT_N" != "unlimited" ]] && (( LIMIT_N < 1024 )); then echo "can_run=false" >> "$GITHUB_OUTPUT" echo "skip_reason=ulimit -n is $LIMIT_N (store requires >= 1024)" >> "$GITHUB_OUTPUT" - exit 0 + exit 1 fi echo "can_run=true" >> "$GITHUB_OUTPUT" @@ -300,31 +448,67 @@ jobs: run: | $TRAVIS_DIR/start-pd.sh $TRAVIS_DIR/start-store.sh + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "PD_DIR=hugegraph-pd/apache-hugegraph-pd-$VERSION" >> "$GITHUB_ENV" + echo "STORE_DIR=hugegraph-store/apache-hugegraph-store-$VERSION" >> "$GITHUB_ENV" + + - name: Run Store ingress and Arthas security tests + run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + mvn test -pl hugegraph-store/hg-store-node -am \ + -Dtest=StoredRowIngressTest,ArthasSecurityTest -Dsurefire.failIfNoSpecifiedTests=false \ + -Djacoco.sessionId=store-node-test \ + -Djacoco.destFile="$PWD/hugegraph-store/hg-store-test/target/store-node-test.exec" 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" - name: Run common test run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") mvn test -pl hugegraph-store/hg-store-test -am \ - -P store-common-test -Djacoco.sessionId=store-common-test + -P store-common-test -Djacoco.sessionId=store-common-test 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" - name: Run client test run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") mvn test -pl hugegraph-store/hg-store-test -am \ - -P store-client-test -Djacoco.sessionId=store-client-test + -P store-client-test -Djacoco.sessionId=store-client-test 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" - name: Run rocksdb test run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") mvn test -pl hugegraph-store/hg-store-test -am \ - -P store-rocksdb-test -Djacoco.sessionId=store-rocksdb-test + -P store-rocksdb-test -Djacoco.sessionId=store-rocksdb-test 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" - name: Run raft-core test run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") mvn test -pl hugegraph-store/hg-store-test -am \ - -P store-raftcore-test -Djacoco.sessionId=store-raftcore-test + -P store-raftcore-test -Djacoco.sessionId=store-raftcore-test 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" - name: Run core test run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + mvn test -pl hugegraph-store/hg-store-test -am \ + -P store-core-test -Djacoco.sessionId=store-core-test 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" + + - name: Run server lifecycle and shutdown tests + run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") mvn test -pl hugegraph-store/hg-store-test -am \ - -P store-core-test -Djacoco.sessionId=store-core-test + -P store-server-test -Djacoco.sessionId=store-server-test 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" - name: Generate aggregate coverage report run: | @@ -344,6 +528,12 @@ jobs: "$TEST_REPORT_DIR/TEST-org.apache.hugegraph.store.raftcore.RaftSuiteTest.xml" \ --require-test-report \ "$TEST_REPORT_DIR/TEST-org.apache.hugegraph.store.core.CoreSuiteTest.xml" \ + --require-test-report \ + "$TEST_REPORT_DIR/TEST-org.apache.hugegraph.store.service.ServerSuiteTest.xml" \ + --require-test-report \ + "hugegraph-store/hg-store-node/target/surefire-reports/TEST-org.apache.hugegraph.store.business.StoredRowIngressTest.xml" \ + --require-test-report \ + "hugegraph-store/hg-store-node/target/surefire-reports/TEST-org.apache.hugegraph.store.node.controller.ArthasSecurityTest.xml" \ --require-covered-group hg-store-common \ --require-covered-group hg-store-client \ --require-covered-group hg-store-rocksdb \ @@ -353,46 +543,75 @@ jobs: --require-session store-rocksdb-test \ --require-session store-raftcore-test \ --require-session store-core-test \ + --require-session store-server-test \ + --require-session store-node-test \ "$REPORT_FILE" \ hg-store-grpc hg-store-common hg-store-client hg-store-rocksdb hg-store-core + - name: Show Store diagnostics on failure + if: failure() + run: | + VERSION=$(grep -E '^VersionInBash=' \ + hugegraph-commons/hugegraph-common/src/main/resources/version.properties | cut -d'=' -f2-) + [[ "$VERSION" =~ ^[0-9A-Za-z._-]+$ ]] || { + echo "Invalid VersionInBash: $VERSION" + exit 1 + } + PD_DIR=hugegraph-pd/apache-hugegraph-pd-$VERSION + STORE_DIR=hugegraph-store/apache-hugegraph-store-$VERSION + bash $TRAVIS_DIR/ci-service-utils.sh dump "$PD_DIR" HugeGraphPD || true + bash $TRAVIS_DIR/ci-service-utils.sh dump "$STORE_DIR" HugeGraphStore || true + find . -path '*/surefire-reports/*' -type f \ + \( -name '*.txt' -o -name '*.xml' \) | sort | while read -r report; do + echo "--- tail -n 120 $report ---" + tail -n 120 "$report" || true + done + - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} files: ${{ env.REPORT_FILE }} + flags: store disable_search: true fail_ci_if_error: false hstore: - needs: struct - runs-on: ubuntu-latest + if: inputs.run-hstore + runs-on: ubuntu-24.04 env: USE_STAGE: 'false' # Whether to include the stage repository. TRAVIS_DIR: hugegraph-server/hugegraph-dist/src/assembly/travis REPORT_DIR: target/site/jacoco BACKEND: hstore - RELEASE_BRANCH: ${{ startsWith(github.ref_name, 'release-') || startsWith(github.ref_name, 'test-') || startsWith(github.base_ref, 'release-') }} + # Long TP suites run only on dedicated validation branches. + RUN_TINKERPOP_TESTS: >- + ${{ + startsWith(github.ref_name, 'release-') || + startsWith(github.ref_name, 'test-') || + startsWith(github.ref_name, 'tinkerpop-') || + startsWith(github.head_ref, 'release-') || + startsWith(github.head_ref, 'test-') || + startsWith(github.head_ref, 'tinkerpop-') || + startsWith(github.base_ref, 'release-') || + startsWith(github.base_ref, 'test-') || + startsWith(github.base_ref, 'tinkerpop-') + }} steps: - - name: Install JDK 11 - uses: actions/setup-java@v3 - with: - java-version: '11' - distribution: 'zulu' - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v7 with: fetch-depth: 2 + - name: Install project JDK + uses: actions/setup-java@v6 + with: + java-version-file: .github/workflows/.java-version + distribution: 'zulu' + cache: maven + cache-jdk: false + - name: use staged maven repo settings if: ${{ env.USE_STAGE == 'true' }} run: | @@ -406,36 +625,301 @@ jobs: - name: Prepare env and service run: | + VERSION=$(grep -E '^VersionInBash=' \ + hugegraph-commons/hugegraph-common/src/main/resources/version.properties | cut -d'=' -f2-) + [[ "$VERSION" =~ ^[0-9A-Za-z._-]+$ ]] || { + echo "Invalid VersionInBash: $VERSION" + exit 1 + } + STORE_CONFIG=hugegraph-store/apache-hugegraph-store-$VERSION/conf/application-pd.yml + [[ -f "$STORE_CONFIG" ]] || { + echo "Store config not found: $STORE_CONFIG" + exit 1 + } + [[ $(grep -Ec '^[[:space:]]*min_write_buffer_number_to_merge:' \ + "$STORE_CONFIG") -eq 1 ]] || { + echo "Unable to locate RocksDB config insertion point: $STORE_CONFIG" + exit 1 + } + # Bound file-handle caching for this CI topology without changing + # the released Store default for deployments with different limits. + sed -i \ + -e '/^[[:space:]]*max_open_files:/d' \ + -e '/^[[:space:]]*min_write_buffer_number_to_merge:/a\ max_open_files: 4096' \ + "$STORE_CONFIG" + grep -n 'max_open_files' "$STORE_CONFIG" + + SOFT_OPEN_FILES=$(ulimit -Sn) + echo "[ci] open-file limit before HStore startup:" \ + "soft=$SOFT_OPEN_FILES, hard=$(ulimit -Hn)" + if [[ "$SOFT_OPEN_FILES" != "unlimited" ]] && + (( SOFT_OPEN_FILES < 65535 )); then + ulimit -Sn 65535 + fi + echo "[ci] open-file limit for HStore:" \ + "soft=$(ulimit -Sn), hard=$(ulimit -Hn)" $TRAVIS_DIR/install-backend.sh $BACKEND + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "PD_DIR=hugegraph-pd/apache-hugegraph-pd-$VERSION" >> "$GITHUB_ENV" + echo "STORE_DIR=hugegraph-store/apache-hugegraph-store-$VERSION" >> "$GITHUB_ENV" - name: Run unit test run: | - $TRAVIS_DIR/run-unit-test.sh $BACKEND + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + $TRAVIS_DIR/run-unit-test.sh $BACKEND 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" - name: Run core test + # Bound stuck HStore scans so the diagnostic steps below can run + # before the GitHub-hosted job reaches its global execution limit. + timeout-minutes: 45 env: HUGEGRAPH_TEST_TASK_LOG_LEVEL: WARN run: | - $TRAVIS_DIR/run-core-test.sh $BACKEND + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + $TRAVIS_DIR/run-core-test.sh $BACKEND 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" - name: Run api test run: | - $TRAVIS_DIR/run-api-test.sh $BACKEND $REPORT_DIR + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + $TRAVIS_DIR/run-api-test.sh $BACKEND $REPORT_DIR 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" - name: Run raft test if: ${{ env.BACKEND == 'rocksdb' }} run: | - $TRAVIS_DIR/run-api-test-for-raft.sh $BACKEND $REPORT_DIR - - - name: Run TinkerPop test - if: ${{ env.RELEASE_BRANCH == 'true' }} + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + $TRAVIS_DIR/run-api-test-for-raft.sh $BACKEND $REPORT_DIR 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" + + # Include HStore in the upgrade compatibility gate. The dedicated HStore + # service setup above makes its structure/process suites exercise the + # distributed backend rather than only the memory and RocksDB paths. + - name: Run TinkerPop structure test + if: ${{ env.RUN_TINKERPOP_TESTS == 'true' }} + # The HStore suite needs close to one hour before global cleanup, so + # keep enough headroom to finish cleanup and emit Surefire results. + timeout-minutes: 120 + env: + HUGEGRAPH_TEST_TASK_LOG_LEVEL: WARN run: | - $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND tinkerpop + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + # Emit lightweight samples during execution: a lost runner cannot + # upload the failure-only diagnostics below. + python3 - "$TRAVIS_DIR/run-tinkerpop-test.sh" "$BACKEND" structure 2>&1 <<'PYTHON' | tee "$RUNTIME_LOG" + import datetime + import os + import pathlib + import subprocess + import sys + + with subprocess.Popen(sys.argv[1:]) as test: + while test.poll() is None: + try: + print("[hstore-resources] " + datetime.datetime.now( + datetime.timezone.utc).isoformat(), flush=True) + subprocess.run(["free", "-m"], check=False) + processes = subprocess.run( + ["ps", "-eo", "pid,comm,rss,pcpu", "--sort=-rss"], + capture_output=True, text=True, check=False) + print("\n".join(processes.stdout.splitlines()[:12]), flush=True) + for name in ("/proc/pressure/memory", "/sys/fs/cgroup/memory.events"): + path = pathlib.Path(name) + if path.is_file(): + try: + print(f"{name}: {path.read_text().strip()}", flush=True) + except OSError: + pass + java = subprocess.run(["pgrep", "-x", "java"], + capture_output=True, text=True, check=False) + for pid in java.stdout.split(): + try: + with os.scandir(f"/proc/{pid}/fd") as entries: + count = sum(1 for _ in entries) + print(f"java pid={pid} open_fds={count}", flush=True) + except OSError: + pass + except (OSError, UnicodeError) as error: + print(f"[hstore-resources] sampling unavailable: {error}", flush=True) + try: + result = test.wait(timeout=60) + sys.exit(result if result >= 0 else 128 - result) + except subprocess.TimeoutExpired: + pass + result = test.returncode + sys.exit(result if result >= 0 else 128 - result) + PYTHON + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" + + - name: Run TinkerPop process standard test + if: ${{ env.RUN_TINKERPOP_TESTS == 'true' }} + timeout-minutes: 120 + run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND process-standard 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" + + - name: Run TinkerPop process feature test + if: ${{ env.RUN_TINKERPOP_TESTS == 'true' }} + timeout-minutes: 120 + run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND process-feature 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs" + + - name: Collect HStore diagnostics on failure or timeout + if: ${{ failure() || cancelled() }} + timeout-minutes: 10 + run: | + set +e + DIAGNOSTICS_DIR=hstore-diagnostics + mkdir -p "$DIAGNOSTICS_DIR/threads" \ + "$DIAGNOSTICS_DIR/surefire" \ + "$DIAGNOSTICS_DIR/services" + exec > >(tee -a "$DIAGNOSTICS_DIR/summary.log") 2>&1 + + VERSION=$(grep -E '^VersionInBash=' \ + hugegraph-commons/hugegraph-common/src/main/resources/version.properties | cut -d'=' -f2-) + [[ "$VERSION" =~ ^[0-9A-Za-z._-]+$ ]] || { + echo "Invalid VersionInBash: $VERSION" + exit 0 + } + PD_DIR=hugegraph-pd/apache-hugegraph-pd-$VERSION + STORE_DIR=hugegraph-store/apache-hugegraph-store-$VERSION + SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION + + echo "::group::HugeGraphStore file descriptors" + STORE_PID_FILE="$STORE_DIR/bin/pid" + if [[ -f "$STORE_PID_FILE" ]]; then + STORE_PID=$(tr -d '[:space:]' < "$STORE_PID_FILE") + if [[ "$STORE_PID" =~ ^[0-9]+$ ]] && + [[ -r "/proc/$STORE_PID/limits" ]] && + [[ -d "/proc/$STORE_PID/fd" ]]; then + cat "/proc/$STORE_PID/limits" | + tee "$DIAGNOSTICS_DIR/services/store-process-limits.txt" || true + + FD_TARGETS="$DIAGNOSTICS_DIR/services/store-fd-targets.txt" + find "/proc/$STORE_PID/fd" -mindepth 1 -maxdepth 1 -type l \ + -printf '%l\n' >"$FD_TARGETS" \ + 2>"$DIAGNOSTICS_DIR/services/store-fd-errors.txt" || true + ls -l "/proc/$STORE_PID/fd" \ + >"$DIAGNOSTICS_DIR/services/store-fds.txt" 2>&1 || true + + STORE_FD_COUNT=$(wc -l < "$FD_TARGETS" | tr -d '[:space:]') + echo "[ci] Store PID $STORE_PID open fd count: $STORE_FD_COUNT" | + tee "$DIAGNOSTICS_DIR/services/store-fd-count.txt" || true + + awk ' + /\.sst/ { types["rocksdb-sst"]++; next } + /^socket:/ { types["socket"]++; next } + /^pipe:/ { types["pipe"]++; next } + /^anon_inode:/ { types["anon-inode"]++; next } + /\/storage\// { types["rocksdb-other"]++; next } + { types["other"]++ } + END { + for (type in types) { + print types[type], type + } + } + ' "$FD_TARGETS" | sort -nr | + tee "$DIAGNOSTICS_DIR/services/store-fd-types.txt" || true + + sort "$FD_TARGETS" | uniq -c | sort -nr \ + >"$DIAGNOSTICS_DIR/services/store-fd-target-counts.txt" || true + echo "[ci] Store top fd targets:" + head -n 50 \ + "$DIAGNOSTICS_DIR/services/store-fd-target-counts.txt" || true + else + echo "[ci] Store PID is unavailable for fd diagnostics: $STORE_PID" + fi + else + echo "[ci] Store pid file not found: $STORE_PID_FILE" + fi + echo "::endgroup::" + + echo "::group::Runner process snapshot" + ps -eo pid,ppid,stat,etime,%cpu,%mem,args --sort=pid | + tee "$DIAGNOSTICS_DIR/processes.txt" || true + (ss -ltnp || netstat -ltnp || true) 2>&1 | + tee "$DIAGNOSTICS_DIR/listening-ports.txt" || true + echo "::endgroup::" + + echo "::group::Java thread dumps" + if command -v jcmd >/dev/null 2>&1; then + jcmd -l | tee "$DIAGNOSTICS_DIR/jcmd-list.txt" || true + while read -r pid _; do + [[ "$pid" =~ ^[0-9]+$ ]] || continue + kill -0 "$pid" >/dev/null 2>&1 || continue + thread_dump="$DIAGNOSTICS_DIR/threads/java-$pid.txt" + timeout 30s jcmd "$pid" Thread.print -l >"$thread_dump" 2>&1 + if [[ $? -ne 0 ]] && command -v jstack >/dev/null 2>&1; then + timeout 30s jstack -l "$pid" >"$thread_dump" 2>&1 || true + fi + echo "[ci] captured Java thread dump: $thread_dump" + done < "$DIAGNOSTICS_DIR/jcmd-list.txt" + else + echo "[ci] jcmd is unavailable" + fi + echo "::endgroup::" + + bash $TRAVIS_DIR/ci-service-utils.sh dump "$PD_DIR" HugeGraphPD || true + bash $TRAVIS_DIR/ci-service-utils.sh dump "$STORE_DIR" HugeGraphStore || true + bash $TRAVIS_DIR/ci-service-utils.sh dump \ + "$SERVER_DIR" HugeGraphServer || true + + for service in \ + "pd:$PD_DIR/logs" \ + "store:$STORE_DIR/logs" \ + "server:$SERVER_DIR/logs"; do + service_name="${service%%:*}" + log_dir="${service#*:}" + if [[ -d "$log_dir" ]]; then + mkdir -p "$DIAGNOSTICS_DIR/services/$service_name" + # Skip heap dumps (logs/heapdump_*/): they can be gigabytes. + tar -C "$log_dir" --exclude='*.hprof*' -cf - . | \ + tar -C "$DIAGNOSTICS_DIR/services/$service_name" -xf - || true + fi + done + + echo "::group::Surefire reports" + while read -r report; do + destination="$DIAGNOSTICS_DIR/surefire/${report#./}" + mkdir -p "$(dirname "$destination")" + cp "$report" "$destination" || true + echo "--- tail -n 200 $report ---" + tail -n 200 "$report" || true + done < <( + find . -path "./$DIAGNOSTICS_DIR" -prune -o \ + -path '*/surefire-reports/*' -type f \ + \( -name '*.txt' -o -name '*.xml' -o \ + -name '*.dump' -o -name '*.dumpstream' \) \ + -print | sort + ) + echo "::endgroup::" + exit 0 + + - name: Upload HStore diagnostics + if: ${{ failure() || cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: hstore-diagnostics-${{ github.run_id }}-${{ github.run_attempt }} + path: hstore-diagnostics + if-no-files-found: warn + retention-days: 14 - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} files: ${{ env.REPORT_DIR }}/*.xml + flags: hstore disable_search: true fail_ci_if_error: false diff --git a/.github/workflows/rerun-ci.yml b/.github/workflows/rerun-ci.yml deleted file mode 100644 index 3fdd4c0f56..0000000000 --- a/.github/workflows/rerun-ci.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: "Rerun CI" - -on: - workflow_run: - workflows: - - "HugeGraph-Server CI" - - "HugeGraph-Commons CI" - - "HugeGraph-PD & Store & Hstore CI" - - "Cluster Test CI" - types: - - completed - -permissions: {} - -env: - MAX_RERUNS: '2' - RETRY_DELAY_SECONDS: '180' - -jobs: - decide-rerun-action: - if: ${{ github.event.workflow_run.conclusion == 'failure' }} - runs-on: ubuntu-latest - outputs: - action: ${{ steps.decision.outputs.action }} - steps: - - name: Decide rerun action - id: decision - env: - WORKFLOW_NAME: ${{ github.event.workflow_run.name }} - RUN_ID: ${{ github.event.workflow_run.id }} - RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} - EVENT_NAME: ${{ github.event.workflow_run.event }} - HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} - run: | - set -euo pipefail - - action="skip" - reason="unsupported event: $EVENT_NAME" - - if [[ "$EVENT_NAME" == "push" || "$EVENT_NAME" == "pull_request" ]]; then - if (( RUN_ATTEMPT > MAX_RERUNS )); then - reason="retry limit reached" - else - action="rerun" - reason="within retry limit" - fi - fi - - { - echo "action=$action" - echo "reason=$reason" - } >> "$GITHUB_OUTPUT" - - { - echo "### Rerun CI decision" - echo "" - echo "- Workflow: $WORKFLOW_NAME" - echo "- Source event: $EVENT_NAME" - echo "- Head branch: $HEAD_BRANCH" - echo "- Run ID: $RUN_ID" - echo "- Current attempt: $RUN_ATTEMPT" - echo "- Max automatic reruns: $MAX_RERUNS" - echo "- Delay seconds: $RETRY_DELAY_SECONDS" - echo "- Action: $action" - echo "- Reason: $reason" - } >> "$GITHUB_STEP_SUMMARY" - - rerun-failed-jobs: - needs: decide-rerun-action - if: needs.decide-rerun-action.outputs.action == 'rerun' - permissions: - actions: write - contents: read - runs-on: ubuntu-latest - steps: - - name: Wait before rerun - run: | - sleep "$RETRY_DELAY_SECONDS" - - - name: Rerun failed jobs - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: | - gh run rerun ${{ github.event.workflow_run.id }} --failed diff --git a/.github/workflows/riscv64-ci.yml b/.github/workflows/riscv64-ci.yml index 9f8ca12057..9b8bce79b0 100644 --- a/.github/workflows/riscv64-ci.yml +++ b/.github/workflows/riscv64-ci.yml @@ -28,12 +28,16 @@ env: tonistiigi/binfmt@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0 RISCV64_BASE_IMAGE: >- ubuntu@sha256:4edded5722eb644868b7b976033d241d2ab3fff0a170924df69b200a59a2b994 - DRAGONWELL_RISCV64_ARCHIVE: >- - Alibaba_Dragonwell_Extended_11.0.31.28.11_riscv64_linux.tar.gz - DRAGONWELL_RISCV64_SHA256: >- - 7df2d308f0dca7a779d2854e6da19214f99cd77aa6d4982c3bf981a77266a79b - DRAGONWELL_RISCV64_URL: >- - https://github.com/dragonwell-project/dragonwell11/releases/download/dragonwell-extended-11.0.31.28_jdk-11.0.31-ga/Alibaba_Dragonwell_Extended_11.0.31.28.11_riscv64_linux.tar.gz + TEMURIN_RISCV64_ARCHIVE: >- + OpenJDK17U-jdk_riscv64_linux_hotspot_17.0.20_8.tar.gz + TEMURIN_RISCV64_SHA256: >- + bc36e8044c88df9f4ec2967c5277d1c8fae572378c12c0bac44dcae15d3af2f7 + TEMURIN_RISCV64_BASE_URL: >- + https://github.com/adoptium/temurin17-binaries/releases/download + TEMURIN_RISCV64_RELEASE: jdk-17.0.20%2B8 + EXPECTED_JAVA_MAJOR: '17' + EXPECTED_RISCV64_JAVA_VERSION: '17.0.20' + EXPECTED_RISCV64_JAVA_VENDOR: Eclipse Adoptium RISCV64_CONTAINER: >- hugegraph-riscv64-ci-${{ github.run_id }}-${{ github.run_attempt }} @@ -44,7 +48,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: persist-credentials: false @@ -55,24 +59,28 @@ jobs: --name "$RISCV64_CONTAINER" "$RISCV64_BASE_IMAGE" sleep infinity test "$(docker exec "$RISCV64_CONTAINER" uname -m)" = riscv64 - - name: Prepare source and Java 11 + - name: Prepare source and Java 17 run: | - ARCHIVE_PATH="$RUNNER_TEMP/$DRAGONWELL_RISCV64_ARCHIVE" + ARCHIVE_PATH="$RUNNER_TEMP/$TEMURIN_RISCV64_ARCHIVE" curl --fail --location --retry 3 --show-error \ - "$DRAGONWELL_RISCV64_URL" --output "$ARCHIVE_PATH" - echo "$DRAGONWELL_RISCV64_SHA256 $ARCHIVE_PATH" | sha256sum -c - + "$TEMURIN_RISCV64_BASE_URL/$TEMURIN_RISCV64_RELEASE/$TEMURIN_RISCV64_ARCHIVE" \ + --output "$ARCHIVE_PATH" + echo "$TEMURIN_RISCV64_SHA256 $ARCHIVE_PATH" | sha256sum -c - - docker exec "$RISCV64_CONTAINER" mkdir -p /workspace /opt/dragonwell + docker exec "$RISCV64_CONTAINER" mkdir -p /workspace /opt/temurin git ls-files -z | tar --null --files-from=- -cf - | \ docker exec -i "$RISCV64_CONTAINER" tar -xf - -C /workspace docker cp "$ARCHIVE_PATH" \ - "$RISCV64_CONTAINER:/tmp/$DRAGONWELL_RISCV64_ARCHIVE" + "$RISCV64_CONTAINER:/tmp/$TEMURIN_RISCV64_ARCHIVE" - name: Build and smoke test on RISC-V run: | docker exec \ - --env DRAGONWELL_RISCV64_ARCHIVE="$DRAGONWELL_RISCV64_ARCHIVE" \ - --env DRAGONWELL_RISCV64_SHA256="$DRAGONWELL_RISCV64_SHA256" \ + --env TEMURIN_RISCV64_ARCHIVE="$TEMURIN_RISCV64_ARCHIVE" \ + --env TEMURIN_RISCV64_SHA256="$TEMURIN_RISCV64_SHA256" \ + --env EXPECTED_JAVA_MAJOR="$EXPECTED_JAVA_MAJOR" \ + --env EXPECTED_RISCV64_JAVA_VERSION="$EXPECTED_RISCV64_JAVA_VERSION" \ + --env EXPECTED_RISCV64_JAVA_VENDOR="$EXPECTED_RISCV64_JAVA_VENDOR" \ "$RISCV64_CONTAINER" bash -euo pipefail -c ' test "$(uname -m)" = riscv64 apt-get -q update @@ -81,11 +89,11 @@ jobs: lsof maven procps \ protobuf-compiler protobuf-compiler-grpc-java-plugin - ARCHIVE_PATH="/tmp/$DRAGONWELL_RISCV64_ARCHIVE" - echo "$DRAGONWELL_RISCV64_SHA256 $ARCHIVE_PATH" | sha256sum -c - - tar -xzf "$ARCHIVE_PATH" --strip-components=1 -C /opt/dragonwell + ARCHIVE_PATH="/tmp/$TEMURIN_RISCV64_ARCHIVE" + echo "$TEMURIN_RISCV64_SHA256 $ARCHIVE_PATH" | sha256sum -c - + tar -xzf "$ARCHIVE_PATH" --strip-components=1 -C /opt/temurin rm "$ARCHIVE_PATH" - export JAVA_HOME=/opt/dragonwell + export JAVA_HOME=/opt/temurin export PATH="$JAVA_HOME/bin:$PATH" java -XshowSettings:vm -version diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml index b5f5efc270..f21f1f73a1 100644 --- a/.github/workflows/server-ci.yml +++ b/.github/workflows/server-ci.yml @@ -2,262 +2,195 @@ name: "HugeGraph-Server CI" on: push: - branches: - - master - - 'release-*' - - 'test-*' + branches: [master, 'release-*', 'test-*', 'tinkerpop-*'] pull_request: + schedule: + - cron: '0 6 * * 1' + timezone: Asia/Singapore + workflow_dispatch: + inputs: + test-set: + description: Tests to run on the selected branch + type: choice + default: mainline + options: [mainline, distributed, macos-arm, hbase, riscv64, codeql, full] + +# A new PR head replaces its old run; scheduled and manual runs stay independent. +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}-${{ github.run_attempt == 1 && 'head' || format('rerun-{0}', github.run_id) }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + pull-requests: read jobs: - wait-storage-shell-test: - permissions: - contents: read - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Run wait-storage.sh peer failover tests - run: hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh - - build-server: + preflight: + name: check-license runs-on: ubuntu-24.04 + timeout-minutes: 15 env: - USE_STAGE: 'false' # Whether to include the stage repository. - TRAVIS_DIR: hugegraph-server/hugegraph-dist/src/assembly/travis - REPORT_DIR: target/site/jacoco - BACKEND: ${{ matrix.BACKEND }} - TRIGGER_BRANCH_NAME: ${{ github.ref_name }} - HEAD_BRANCH_NAME: ${{ github.head_ref }} - BASE_BRANCH_NAME: ${{ github.base_ref }} - TARGET_BRANCH_NAME: ${{ github.base_ref != '' && github.base_ref || github.ref_name }} - RELEASE_BRANCH: ${{ startsWith(github.ref_name, 'release-') || startsWith(github.ref_name, 'test-') }} - RAFT_MODE: ${{ startsWith(github.head_ref, 'test') || startsWith(github.head_ref, 'raft') }} - - strategy: - fail-fast: false - matrix: - BACKEND: [ memory, rocksdb, hbase ] - JAVA_VERSION: [ '11' ] - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 5 - - # TODO: Remove this step after install-backend.sh updated - - name: Install Java8 for backend - uses: actions/setup-java@v4 - with: - java-version: '8' - distribution: 'zulu' - - - name: Prepare backend environment - run: | - $TRAVIS_DIR/install-backend.sh $BACKEND && jps -l - - - name: Install Java ${{ matrix.JAVA_VERSION }} - uses: actions/setup-java@v4 - with: - java-version: ${{ matrix.JAVA_VERSION }} - distribution: 'zulu' - - - name: Cache Maven packages - uses: actions/cache@v4 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - - name: Use staged maven repo settings - if: ${{ env.USE_STAGE == 'true' }} - run: | - cp $HOME/.m2/settings.xml /tmp/settings.xml - cp -vf .github/configs/settings.xml $HOME/.m2/settings.xml && cat $HOME/.m2/settings.xml - - - name: Compile - run: | - mvn clean compile -U -Dmaven.javadoc.skip=true -ntp - - - name: Validate Docker integration - if: ${{ env.BACKEND == 'rocksdb' }} - run: | - bash hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh - bash docker/test-compose.sh render - - - name: Run Compose auth-on smoke tests - if: ${{ env.BACKEND == 'rocksdb' }} - run: bash docker/test-compose.sh smoke - - - name: Run check_port unit tests - if: ${{ env.BACKEND == 'rocksdb' }} - run: | - # Validates the ss/netstat port preflight that replaced lsof - $TRAVIS_DIR/test-check-port.sh \ - hugegraph-server/hugegraph-dist/src/assembly/static \ - hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh - - - name: Run Java security properties tests - if: ${{ env.BACKEND == 'rocksdb' }} - run: | - mvn package -Dmaven.test.skip=true -pl hugegraph-server/hugegraph-dist -am -ntp - VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) - SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION/ - $TRAVIS_DIR/test-java-security-properties.sh $SERVER_DIR - - - name: Check startup test prerequisites - id: server-preflight - if: ${{ env.BACKEND == 'rocksdb' }} - run: | - # Note: lsof removed from prerequisites; check_port now uses ss, falling back - # to netstat, and warns without blocking when neither can answer. - # This job always runs on Linux (ubuntu-24.04), which uses fuser for port cleanup. - for tool in crontab curl java; do - if ! command -v "$tool" >/dev/null 2>&1; then - echo "can_run=false" >> "$GITHUB_OUTPUT" - echo "skip_reason=missing tool: $tool" >> "$GITHUB_OUTPUT" - exit 0 - fi - done - if ! command -v fuser >/dev/null 2>&1; then - echo "can_run=false" >> "$GITHUB_OUTPUT" - echo "skip_reason=missing tool: fuser (required for Linux port cleanup)" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "can_run=true" >> "$GITHUB_OUTPUT" - - - name: Run start-hugegraph.sh foreground mode tests - if: ${{ env.BACKEND == 'rocksdb' && steps.server-preflight.outputs.can_run == 'true' }} - run: | - VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) - SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION/ - $TRAVIS_DIR/test-start-hugegraph.sh $SERVER_DIR - - - name: Startup tests skipped (missing prerequisites) - if: ${{ env.BACKEND == 'rocksdb' && steps.server-preflight.outputs.can_run == 'false' }} - run: | - echo "::notice::Server startup tests skipped — ${{ steps.server-preflight.outputs.skip_reason }}" - - - name: Run unit test - run: | - $TRAVIS_DIR/run-unit-test.sh $BACKEND - - - name: Run core test - run: | - $TRAVIS_DIR/run-core-test.sh $BACKEND - - - name: Run api test - run: | - if [ "$BACKEND" = "rocksdb" ]; then - $TRAVIS_DIR/run-api-test.sh $BACKEND $REPORT_DIR true - else - $TRAVIS_DIR/run-api-test.sh $BACKEND $REPORT_DIR - fi - - # TODO: disable raft test in normal PR due to the always timeout problem - - name: Run raft test - if: ${{ env.RAFT_MODE == 'true' && env.BACKEND == 'rocksdb' }} - run: | - $TRAVIS_DIR/run-api-test-for-raft.sh $BACKEND $REPORT_DIR - - - name: Run TinkerPop test - if: ${{ env.RELEASE_BRANCH == 'true' }} - run: | - echo "[WARNING] Enter Tinkerpop Test, current 'github.ref_name' is ${{ github.ref_name }}" - $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND tinkerpop - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ${{ env.REPORT_DIR }}/*.xml - disable_search: true - fail_ci_if_error: false - - build-server-macos-rocksdb: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: macos-15-intel - server_java_options: '' - - os: macos-15 - server_java_options: '-Xms512m -Xmx2g' - env: - USE_STAGE: 'false' # Whether to include the stage repository. - TRAVIS_DIR: hugegraph-server/hugegraph-dist/src/assembly/travis - REPORT_DIR: target/site/jacoco - BACKEND: rocksdb - JAVA_VERSION: '11' - SERVER_JAVA_OPTIONS: ${{ matrix.server_java_options }} - + PYTHONDONTWRITEBYTECODE: '1' + outputs: + selection_result: ${{ steps.select.outcome }} + java_result: ${{ steps.java.outcome }} + java_versions: ${{ steps.java.outputs.versions }} + server: ${{ steps.select.outputs.server }} + commons: ${{ steps.select.outputs.commons }} + struct: ${{ steps.select.outputs.struct }} + pd: ${{ steps.select.outputs.pd }} + store: ${{ steps.select.outputs.store }} + hstore: ${{ steps.select.outputs.hstore }} + pd_store: ${{ steps.select.outputs.pd_store }} + cluster: ${{ steps.select.outputs.cluster }} + docker: ${{ steps.select.outputs.docker }} + helm: ${{ steps.select.outputs.helm }} + dependency_license: ${{ steps.select.outputs.dependency_license }} + macos: ${{ steps.select.outputs.macos }} + macos_full: ${{ steps.select.outputs.macos_full }} + hbase: ${{ steps.select.outputs.hbase }} + riscv64: ${{ steps.select.outputs.riscv64 }} + security: ${{ steps.select.outputs.security }} + distributed_smoke: ${{ steps.select.outputs.distributed_smoke }} steps: - name: Checkout - uses: actions/checkout@v4 + id: checkout + uses: actions/checkout@v7 with: - fetch-depth: 5 + fetch-depth: 0 + persist-credentials: false - - name: Install Java ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4 + - name: Select affected tests + id: select + env: + GH_TOKEN: ${{ github.token }} + run: >- + python3 .github/scripts/ci-policy.py plan --project server + --event-name "$GITHUB_EVENT_NAME" --event-path "$GITHUB_EVENT_PATH" + --repository "$GITHUB_REPOSITORY" --output "$RUNNER_TEMP/ci-plan.json" + + - name: Read project Java runtime + id: java + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + run: | + python3 - <<'PYTHON' + import json + import os + import re + from pathlib import Path + + runtime = Path('.github/workflows/.java-version').read_text().strip() + if not re.fullmatch(r'[1-9][0-9]*(\.[0-9]+)*', runtime): + raise ValueError('Project Java runtime must be a non-empty numeric version') + with open(os.environ['GITHUB_OUTPUT'], 'a') as output: + print('versions=' + json.dumps([runtime]), file=output) + PYTHON + + - name: Record selection + if: ${{ !cancelled() && steps.select.outcome == 'success' }} + uses: actions/upload-artifact@v4 with: - java-version: ${{ env.JAVA_VERSION }} - distribution: 'zulu' - - - name: Cache Maven packages - uses: actions/cache@v4 + name: ci-plan + path: ${{ runner.temp }}/ci-plan.json + retention-days: 7 + overwrite: true + + - name: Test CI policy and deployment checks + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + run: python3 -m unittest discover -s .github/scripts -p 'test_*.py' + + - name: Check license headers + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} + uses: apache/skywalking-eyes@main with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + log: info + token: ${{ github.token }} + config: .licenserc.yaml - - name: Use staged maven repo settings - if: ${{ env.USE_STAGE == 'true' }} + - name: License Check (RAT) + if: ${{ !cancelled() && steps.checkout.outcome == 'success' }} run: | - cp $HOME/.m2/settings.xml /tmp/settings.xml - cp -vf .github/configs/settings.xml $HOME/.m2/settings.xml && cat $HOME/.m2/settings.xml + mvn apache-rat:check -ntp + find . -name rat.txt -print0 | xargs -0 -I file cat file > "$RUNNER_TEMP/merged-rat.txt" + cat "$RUNNER_TEMP/merged-rat.txt" - - name: Compile - run: | - mvn clean compile -pl hugegraph-server/hugegraph-test -am -U -Dmaven.javadoc.skip=true -ntp - - - name: Run check_port unit tests - # Validates the ss/netstat port preflight that replaced lsof - run: | - $TRAVIS_DIR/test-check-port.sh \ - hugegraph-server/hugegraph-dist/src/assembly/static \ - hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh - - - name: Run RocksDB core test - run: | - $TRAVIS_DIR/run-core-test.sh $BACKEND - - - name: Run RocksDB API test - run: | - $TRAVIS_DIR/run-api-test.sh $BACKEND $REPORT_DIR true - - - name: Show server log on failure - if: failure() - run: | - VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) - SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION/ - if [ -f "$SERVER_DIR/logs/hugegraph-server.log" ]; then - tail -n 200 "$SERVER_DIR/logs/hugegraph-server.log" - fi - - - name: Stop RocksDB server - if: always() - run: | - VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) - SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION/ - if [ -f "$SERVER_DIR/bin/pid" ]; then - $TRAVIS_DIR/stop-server.sh $SERVER_DIR || true - fi - - build-server-riscv64: - uses: ./.github/workflows/riscv64-ci.yml + build-server: + needs: preflight + if: ${{ !cancelled() && needs.preflight.outputs.selection_result == 'success' && needs.preflight.outputs.java_result == 'success' && needs.preflight.outputs.server == 'true' }} + uses: ./.github/workflows/server-tests.yml + with: + backend: memory + java-versions: ${{ needs.preflight.outputs.java_versions }} + secrets: inherit + + server-rocksdb: + needs: preflight + if: ${{ !cancelled() && needs.preflight.outputs.selection_result == 'success' && needs.preflight.outputs.java_result == 'success' && needs.preflight.outputs.server == 'true' }} + uses: ./.github/workflows/server-tests.yml + with: + backend: rocksdb + java-versions: ${{ needs.preflight.outputs.java_versions }} + secrets: inherit + + commons: + needs: preflight + if: ${{ !cancelled() && needs.preflight.outputs.selection_result == 'success' && needs.preflight.outputs.java_result == 'success' && needs.preflight.outputs.commons == 'true' }} + uses: ./.github/workflows/commons-ci.yml + with: + java-versions: ${{ needs.preflight.outputs.java_versions }} + secrets: inherit + + pd_store: + needs: preflight + if: ${{ !cancelled() && needs.preflight.outputs.selection_result == 'success' && needs.preflight.outputs.java_result == 'success' && needs.preflight.outputs.pd_store == 'true' }} + uses: ./.github/workflows/pd-store-ci.yml + with: + run-struct: ${{ needs.preflight.outputs.struct == 'true' }} + run-pd: ${{ needs.preflight.outputs.pd == 'true' }} + run-store: ${{ needs.preflight.outputs.store == 'true' }} + run-hstore: ${{ needs.preflight.outputs.hstore == 'true' }} + secrets: inherit + + cluster: + needs: preflight + if: ${{ !cancelled() && needs.preflight.outputs.selection_result == 'success' && needs.preflight.outputs.java_result == 'success' && needs.preflight.outputs.cluster == 'true' }} + uses: ./.github/workflows/cluster-test-ci.yml + secrets: inherit + + docker: + needs: preflight + if: ${{ !cancelled() && needs.preflight.outputs.selection_result == 'success' && needs.preflight.outputs.java_result == 'success' && (needs.preflight.outputs.docker == 'true' || needs.preflight.outputs.distributed_smoke == 'true') }} + uses: ./.github/workflows/docker-build-ci.yml + with: + smoke-only: ${{ needs.preflight.outputs.distributed_smoke == 'true' && needs.preflight.outputs.docker != 'true' }} + + helm: + needs: preflight + if: ${{ !cancelled() && needs.preflight.outputs.selection_result == 'success' && needs.preflight.outputs.helm == 'true' }} + uses: ./.github/workflows/helm-chart-ci.yml + + dependency_license: + needs: preflight + if: ${{ !cancelled() && needs.preflight.outputs.selection_result == 'success' && needs.preflight.outputs.java_result == 'success' && needs.preflight.outputs.dependency_license == 'true' }} + uses: ./.github/workflows/check-dependencies.yml + + compatibility: + needs: preflight + if: ${{ !cancelled() && needs.preflight.outputs.selection_result == 'success' && needs.preflight.outputs.java_result == 'success' && (needs.preflight.outputs.macos == 'true' || needs.preflight.outputs.hbase == 'true' || needs.preflight.outputs.riscv64 == 'true') }} + uses: ./.github/workflows/server-compatibility-ci.yml + with: + java-versions: ${{ needs.preflight.outputs.java_versions }} + macos: ${{ needs.preflight.outputs.macos == 'true' }} + macos-full: ${{ needs.preflight.outputs.macos_full == 'true' }} + hbase: ${{ needs.preflight.outputs.hbase == 'true' }} + riscv64: ${{ needs.preflight.outputs.riscv64 == 'true' }} + secrets: inherit + + codeql: + needs: preflight + if: ${{ !cancelled() && needs.preflight.outputs.selection_result == 'success' && needs.preflight.outputs.java_result == 'success' && needs.preflight.outputs.security == 'true' }} + uses: ./.github/workflows/codeql-analysis.yml permissions: contents: read + actions: read + security-events: write diff --git a/.github/workflows/server-compatibility-ci.yml b/.github/workflows/server-compatibility-ci.yml new file mode 100644 index 0000000000..422182fdd5 --- /dev/null +++ b/.github/workflows/server-compatibility-ci.yml @@ -0,0 +1,125 @@ +name: "Server Compatibility CI" + +on: + workflow_call: + inputs: + java-versions: + type: string + required: true + hbase: + type: boolean + default: false + riscv64: + type: boolean + default: false + macos: + type: boolean + default: false + macos-full: + type: boolean + default: false + +permissions: + contents: read + +jobs: + build-server-backends: + if: inputs.hbase + uses: ./.github/workflows/server-tests.yml + with: + backend: hbase + java-versions: ${{ inputs.java-versions }} + secrets: inherit + + build-server-macos-rocksdb: + name: macOS ARM (${{ inputs.macos-full && 'full' || 'smoke' }}) + if: inputs.macos + runs-on: macos-15 + timeout-minutes: 60 + env: + USE_STAGE: 'false' # Whether to include the stage repository. + TRAVIS_DIR: hugegraph-server/hugegraph-dist/src/assembly/travis + REPORT_DIR: target/site/jacoco + BACKEND: rocksdb + EXPECTED_ARCH: arm64 + EXPECTED_JAVA_MAJOR: ${{ fromJSON(inputs.java-versions)[0] }} + SERVER_JAVA_OPTIONS: '-Xms512m -Xmx2g' + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 5 + persist-credentials: false + + - name: Install project JDK + uses: actions/setup-java@v6 + with: + java-version-file: .github/workflows/.java-version + distribution: 'zulu' + cache: maven + cache-jdk: false + + - name: Use staged maven repo settings + if: ${{ env.USE_STAGE == 'true' }} + run: | + cp "$HOME/.m2/settings.xml" /tmp/settings.xml + cp -vf .github/configs/settings.xml "$HOME/.m2/settings.xml" && cat "$HOME/.m2/settings.xml" + + - name: Prepare bounded runtime commands + run: | + if ! command -v timeout >/dev/null 2>&1 && ! command -v gtimeout >/dev/null 2>&1; then + brew install coreutils + fi + + - name: Package RocksDB distribution + run: | + mvn clean package -pl hugegraph-server/hugegraph-dist -am -U -Drocksdb-only \ + -Dmaven.test.skip=true -Dmaven.javadoc.skip=true -ntp + + - name: Run check_port unit tests + # Validates the ss/netstat port preflight that replaced lsof + run: | + "$TRAVIS_DIR/test-check-port.sh" \ + hugegraph-server/hugegraph-dist/src/assembly/static \ + hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh + + - name: Run native startup, JNI and restart smoke + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + SERVER_DIR="hugegraph-server/apache-hugegraph-server-$VERSION" + "$TRAVIS_DIR/run-native-runtime-smoke-test.sh" "$SERVER_DIR" + + - name: Run RocksDB core test + if: inputs.macos-full + run: | + "$TRAVIS_DIR/run-core-test.sh" "$BACKEND" + + - name: Run RocksDB API test + if: inputs.macos-full + run: | + "$TRAVIS_DIR/run-api-test.sh" "$BACKEND" "$REPORT_DIR" true + + - name: Show server log on failure + if: failure() + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + SERVER_DIR="hugegraph-server/apache-hugegraph-server-$VERSION" + if [ -f "$SERVER_DIR/logs/hugegraph-server.log" ]; then + tail -n 200 "$SERVER_DIR/logs/hugegraph-server.log" + fi + + - name: Stop RocksDB server + if: always() + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + SERVER_DIR="hugegraph-server/apache-hugegraph-server-$VERSION" + if [ -f "$SERVER_DIR/bin/pid" ]; then + "$TRAVIS_DIR/stop-server.sh" "$SERVER_DIR" || true + fi + + build-server-riscv64: + if: inputs.riscv64 + uses: ./.github/workflows/riscv64-ci.yml + permissions: + contents: read diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml new file mode 100644 index 0000000000..1f707604ea --- /dev/null +++ b/.github/workflows/server-tests.yml @@ -0,0 +1,247 @@ +name: Server backend tests + +on: + workflow_call: + inputs: + backend: + type: string + required: true + java-versions: + type: string + required: true + +permissions: + contents: read + +jobs: + build-server: + name: Server ${{ inputs.backend }} tests (Java ${{ matrix.JAVA_VERSION }}) + runs-on: ubuntu-24.04 + env: + USE_STAGE: 'false' # Whether to include the stage repository. + TRAVIS_DIR: hugegraph-server/hugegraph-dist/src/assembly/travis + REPORT_DIR: target/site/jacoco + BACKEND: ${{ inputs.backend }} + TRIGGER_BRANCH_NAME: ${{ github.ref_name }} + HEAD_BRANCH_NAME: ${{ github.head_ref }} + BASE_BRANCH_NAME: ${{ github.base_ref }} + TARGET_BRANCH_NAME: ${{ github.base_ref != '' && github.base_ref || github.ref_name }} + # Long TP suites run only on dedicated validation branches. + RUN_TINKERPOP_TESTS: >- + ${{ + startsWith(github.ref_name, 'release-') || + startsWith(github.ref_name, 'test-') || + startsWith(github.ref_name, 'tinkerpop-') || + startsWith(github.head_ref, 'release-') || + startsWith(github.head_ref, 'test-') || + startsWith(github.head_ref, 'tinkerpop-') || + startsWith(github.base_ref, 'release-') || + startsWith(github.base_ref, 'test-') || + startsWith(github.base_ref, 'tinkerpop-') + }} + RAFT_MODE: ${{ startsWith(github.ref_name, 'raft-') || startsWith(github.head_ref, 'raft-') }} + + strategy: + fail-fast: false + matrix: + JAVA_VERSION: ${{ fromJSON(inputs.java-versions) }} + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 5 + + - name: Prepare HBase container + if: inputs.backend == 'hbase' + run: $TRAVIS_DIR/install-backend.sh hbase + + - name: Install Java ${{ matrix.JAVA_VERSION }} + uses: actions/setup-java@v6 + with: + java-version: ${{ matrix.JAVA_VERSION }} + distribution: 'zulu' + cache: maven + cache-jdk: false + + - name: Use staged maven repo settings + if: ${{ env.USE_STAGE == 'true' }} + run: | + cp $HOME/.m2/settings.xml /tmp/settings.xml + cp -vf .github/configs/settings.xml $HOME/.m2/settings.xml && cat $HOME/.m2/settings.xml + + - name: Compile + run: | + mvn clean compile -pl hugegraph-server/hugegraph-test -am -U -Dmaven.javadoc.skip=true -ntp + + - name: Check startup and native runtime contracts + if: inputs.backend == 'rocksdb' + run: | + "$TRAVIS_DIR/test-wait-storage.sh" + "$TRAVIS_DIR/test-start-server.sh" + "$TRAVIS_DIR/test-riscv64-java-runtime.sh" + bash "$TRAVIS_DIR/test-tinkerpop-reports.sh" + + - name: Validate Docker integration + if: ${{ env.BACKEND == 'rocksdb' }} + run: | + bash hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh + bash docker/test-compose.sh render + + - name: Run check_port unit tests + if: ${{ env.BACKEND == 'rocksdb' }} + run: | + # Validates the ss/netstat port preflight that replaced lsof + $TRAVIS_DIR/test-check-port.sh \ + hugegraph-server/hugegraph-dist/src/assembly/static \ + hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh + + - name: Run Java security properties tests + if: ${{ env.BACKEND == 'rocksdb' }} + run: | + mvn package -Dmaven.test.skip=true -pl hugegraph-server/hugegraph-dist -am -ntp + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION/ + $TRAVIS_DIR/test-java-security-properties.sh \ + "$SERVER_DIR" "$GITHUB_WORKSPACE" + + - name: Check bundled Arthas local CLI + if: ${{ env.BACKEND == 'rocksdb' }} + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION + bash "$TRAVIS_DIR/test-arthas-cli.sh" "$SERVER_DIR" + + - name: Verify Surefire single-class routing + if: ${{ env.BACKEND == 'rocksdb' }} + run: | + SUREFIRE_DIR=hugegraph-server/hugegraph-test/target/surefire-reports + REPORT="$SUREFIRE_DIR/TEST-org.apache.hugegraph.unit.auth.HugeGraphAuthProxyTest.xml" + rm -f "$REPORT" + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + mvn test -pl hugegraph-server/hugegraph-test -am \ + -P unit-test -Dtest=HugeGraphAuthProxyTest -ntp 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" + test -s "$REPORT" || { + echo "::error::Server single-class test produced no report" + exit 1 + } + NEGATIVE_LOG=$(mktemp) + trap 'rm -f "$NEGATIVE_LOG"' EXIT + if mvn test -pl hugegraph-server/hugegraph-test -am \ + -P unit-test -Dtest=NoSuchJava17ContractTest -ntp \ + >"$NEGATIVE_LOG" 2>&1; then + cat "$NEGATIVE_LOG" + echo "::error::Surefire accepted a missing Server test class" + exit 1 + fi + cat "$NEGATIVE_LOG" + grep -Fq 'No tests matching pattern "NoSuchJava17ContractTest"' \ + "$NEGATIVE_LOG" + grep -Fq '(unit-test) @ hugegraph-test' "$NEGATIVE_LOG" + + - name: Check startup test prerequisites + id: server-preflight + if: ${{ env.BACKEND == 'rocksdb' }} + run: | + # Note: lsof removed from prerequisites; check_port now uses ss, falling back + # to netstat, and warns without blocking when neither can answer. + # This job always runs on Linux (ubuntu-24.04), which uses fuser for port cleanup. + for tool in crontab curl java; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "can_run=false" >> "$GITHUB_OUTPUT" + echo "skip_reason=missing tool: $tool" >> "$GITHUB_OUTPUT" + exit 1 + fi + done + if ! command -v fuser >/dev/null 2>&1; then + echo "can_run=false" >> "$GITHUB_OUTPUT" + echo "skip_reason=missing tool: fuser (required for Linux port cleanup)" >> "$GITHUB_OUTPUT" + exit 1 + fi + echo "can_run=true" >> "$GITHUB_OUTPUT" + + - name: Run start-hugegraph.sh foreground mode tests + if: ${{ env.BACKEND == 'rocksdb' && steps.server-preflight.outputs.can_run == 'true' }} + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION/ + bash $TRAVIS_DIR/test-ci-service-utils.sh "$TRAVIS_DIR/ci-service-utils.sh" + bash $TRAVIS_DIR/test-launcher-gc.sh "$GITHUB_WORKSPACE" + bash $TRAVIS_DIR/test-cgroup-memory.sh "$SERVER_DIR/bin/util.sh" + bash $TRAVIS_DIR/test-start-hugegraph-signal.sh "$SERVER_DIR/bin/start-hugegraph.sh" + $TRAVIS_DIR/test-start-hugegraph.sh $SERVER_DIR + + - name: Startup tests skipped (missing prerequisites) + if: ${{ env.BACKEND == 'rocksdb' && steps.server-preflight.outputs.can_run == 'false' }} + run: | + echo "::notice::Server startup tests skipped — ${{ steps.server-preflight.outputs.skip_reason }}" + + - name: Run unit test + run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + $TRAVIS_DIR/run-unit-test.sh $BACKEND 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" + + - name: Run core test + run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + $TRAVIS_DIR/run-core-test.sh $BACKEND 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" + + - name: Run api test + run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + { + if [ "$BACKEND" = "rocksdb" ]; then + $TRAVIS_DIR/run-api-test.sh $BACKEND $REPORT_DIR true + else + $TRAVIS_DIR/run-api-test.sh $BACKEND $REPORT_DIR + fi + } 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" + + # TODO: disable raft test in normal PR due to the always timeout problem + - name: Run raft test + if: ${{ env.RAFT_MODE == 'true' && env.BACKEND == 'rocksdb' }} + timeout-minutes: 45 + run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + $TRAVIS_DIR/run-api-test-for-raft.sh $BACKEND $REPORT_DIR 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" + + - name: Run TinkerPop structure test + if: ${{ env.RUN_TINKERPOP_TESTS == 'true' && (env.BACKEND == 'memory' || env.BACKEND == 'rocksdb') }} + timeout-minutes: 60 + run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND structure 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" + + - name: Run TinkerPop process test + if: ${{ env.RUN_TINKERPOP_TESTS == 'true' && (env.BACKEND == 'memory' || env.BACKEND == 'rocksdb') }} + timeout-minutes: 180 + run: | + set -o pipefail + RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX") + $TRAVIS_DIR/run-tinkerpop-test.sh $BACKEND process 2>&1 | tee "$RUNTIME_LOG" + bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v7 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ${{ env.REPORT_DIR }}/*.xml,hugegraph-server/target/site/jacoco/jacoco.xml + flags: server_${{ inputs.backend }} + disable_search: true + fail_ci_if_error: false + + - name: Stop HBase container + if: ${{ always() && inputs.backend == 'hbase' }} + run: | + "$TRAVIS_DIR/install-hbase.sh" stop diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 4f8484ce69..5c50204aef 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -13,9 +13,11 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v3 + - uses: actions/stale@v11 with: repo-token: ${{ secrets.GITHUB_TOKEN }} + # Preserve the old issue-close classification across the action upgrade. + close-issue-reason: completed stale-issue-message: 'Due to the lack of activity, the current issue is marked as stale and will be closed after 20 days, any update will remove the stale label' stale-pr-message: 'Due to the lack of activity, the current pr is marked as stale and will be closed after 180 days, any update will remove the stale label' stale-issue-label: 'inactive' diff --git a/.serena/memories/code_style_and_conventions.md b/.serena/memories/code_style_and_conventions.md index 701757a1b6..ce3809cdf4 100644 --- a/.serena/memories/code_style_and_conventions.md +++ b/.serena/memories/code_style_and_conventions.md @@ -24,5 +24,5 @@ - Validate: `mvn apache-rat:check -ntp` + `mvn editorconfig:check` ## Build -- Java 11 target except Commons (Java 8 target); compiler/dependency versions come from module POMs. +- Java 17 release target, `-Xlint:unchecked`, Lombok 1.18.30 (provided/optional) - Swagger: `io.swagger.core.v3:swagger-jaxrs2-jakarta` for REST API docs diff --git a/.serena/memories/ecosystem_and_related_projects.md b/.serena/memories/ecosystem_and_related_projects.md index 5f405287d9..b5bd9c1eeb 100644 --- a/.serena/memories/ecosystem_and_related_projects.md +++ b/.serena/memories/ecosystem_and_related_projects.md @@ -17,7 +17,7 @@ Sources → hugegraph-loader → hugegraph-server → Hubble / Computer / AI ## Integrations - Big Data: Flink, Spark, HDFS -- Queries: Gremlin (TinkerPop 3.5.1), OpenCypher, REST API + Swagger UI +- Queries: Gremlin (TinkerPop 3.8.1), OpenCypher, REST API + Swagger UI - Storage: RocksDB (default), HStore (distributed) ## Versions diff --git a/.serena/memories/implementation_patterns_and_guidelines.md b/.serena/memories/implementation_patterns_and_guidelines.md index 26d19ac26f..ce83957f48 100644 --- a/.serena/memories/implementation_patterns_and_guidelines.md +++ b/.serena/memories/implementation_patterns_and_guidelines.md @@ -23,7 +23,7 @@ - After `.proto` changes: `mvn clean compile`; PD/Store gRPC POMs output to each module's `src/main/java/`. ## Query Languages -- **Gremlin**: Native TinkerPop 3.5.1 +- **Gremlin**: Native TinkerPop 3.8.1 - **OpenCypher**: `hugegraph-api/opencypher/` - TinkerPop exceptions are passed through in Gremlin responses @@ -35,7 +35,7 @@ - **Profiles**: `unit-test`, `core-test`, `api-test`, `tinkerpop-structure-test`, `tinkerpop-process-test` - **Backends in CI**: memory, rocksdb, hbase (matrix) - **Single test class**: `mvn test -pl hugegraph-server/hugegraph-test -am -P core-test,memory -Dtest=ClassName` -- TinkerPop tests: only on `release-*`/`test-*` branches +- TinkerPop tests: `release-*`/`test-*` and upgrade branches selected in `server-ci.yml` - Server Raft API tests are branch-gated in `server-ci.yml`; Store raft-core tests run in normal `pd-store-ci.yml` CI. ## Docker @@ -44,6 +44,6 @@ - Container logs: stdout-based ## CI Pipelines -- `server-ci.yml`: compile + unit/core/API tests (memory/rocksdb/hbase × Java 11) +- `server-ci.yml`: compile + unit/core/API tests (memory/rocksdb/hbase × Java 17) - `rerun-ci.yml`: auto-rerun flaky failures (max 2 reruns, 180s delay) - `auto-pr-review.yml`: auto-comment on new PRs diff --git a/.serena/memories/key_file_locations.md b/.serena/memories/key_file_locations.md index ac10c668e8..bde5926a3c 100644 --- a/.serena/memories/key_file_locations.md +++ b/.serena/memories/key_file_locations.md @@ -29,7 +29,7 @@ - Dist: `hugegraph-store/hg-store-dist/src/assembly/static/` ## CI Workflows (.github/workflows/) -- `server-ci.yml` — Server tests (matrix: memory/rocksdb/hbase × Java 11) +- `server-ci.yml` — Server tests (matrix: memory/rocksdb/hbase × Java 17) - `pd-store-ci.yml` — PD, Store & HStore tests - `commons-ci.yml` — Commons tests - `cluster-test-ci.yml` — Cluster integration diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md index a08aea9ec6..deac1e6821 100644 --- a/.serena/memories/project_overview.md +++ b/.serena/memories/project_overview.md @@ -14,9 +14,9 @@ Apache HugeGraph is a fast-speed, highly-scalable graph database supporting 10+ - Integration with Flink/Spark/HDFS ## Technology Stack -- **Language**: Java 11+ (required) -- **Build**: Maven 3.5+ -- **Graph Framework**: Apache TinkerPop 3.5.1 +- **Language**: Java 17 (required; currently supported release) +- **Build**: Maven 3.6.3+ +- **Graph Framework**: Apache TinkerPop 3.8.1 - **RPC**: gRPC + Protocol Buffers - **API Docs**: Swagger (io.swagger.core.v3) - **Storage**: RocksDB (default/embedded), HStore (distributed/production), HBase (deprecated; planned for removal in 2.0) diff --git a/.serena/memories/task_completion_checklist.md b/.serena/memories/task_completion_checklist.md index 22a12b4db7..58a32fb0e4 100644 --- a/.serena/memories/task_completion_checklist.md +++ b/.serena/memories/task_completion_checklist.md @@ -22,7 +22,7 @@ mvn clean compile -Dmaven.javadoc.skip=true # Compile warnings 4. Run `./install-dist/scripts/dependency/regenerate_known_dependencies.sh` ## 4. CI Awareness -- `server-ci.yml`: memory/rocksdb/hbase × Java 11 +- `server-ci.yml`: memory/rocksdb/hbase × Java 17 - `rerun-ci.yml`: auto-retries flaky failures - `licence-checker.yml`: header validation - Server Raft API tests are branch-gated; Store raft-core and core tests run in normal PD/Store CI. diff --git a/AGENTS.md b/AGENTS.md index a38801710f..e7768ad5eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ Commons: shared utilities; independent compiler and test settings ## Build and validation -Commands run from the repository root. Java 11+ and Maven 3.5+; +Commands run from the repository root. Java 17 and Maven 3.6.3+; versions come from [pom.xml](pom.xml), including `${revision}`. ```bash diff --git a/README.md b/README.md index 0e48ced340..2237a5e2da 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,14 @@ [![HugeGraph-Server CI](https://github.com/apache/hugegraph/actions/workflows/server-ci.yml/badge.svg)](https://github.com/apache/hugegraph/actions/workflows/server-ci.yml) [![HugeGraph-PD & Store CI](https://github.com/apache/hugegraph/actions/workflows/pd-store-ci.yml/badge.svg)](https://github.com/apache/hugegraph/actions/workflows/pd-store-ci.yml) [![GitHub Releases Downloads](https://img.shields.io/github/downloads/apache/hugegraph/total.svg)](https://github.com/apache/hugegraph/releases) -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/apache/hugegraph) +[![Artifact Hub](https://img.shields.io/endpoint?url=https%3A%2F%2Fartifacthub.io%2Fbadge%2Frepository%2Fhugegraph-helm)](https://artifacthub.io/packages/helm/hugegraph-helm/hugegraph-helm) +[![Ask DeepWiki](https://img.shields.io/badge/Ask-DeepWiki-blue)](https://deepwiki.com/apache/hugegraph) --- -**Quick Navigation:** [Architecture](#architecture) • [Quick Start](#quick-start) • [Module Map](#module-map) • [Ecosystem](#ecosystem) • [For Contributors](#for-contributors) • [Community](#community) +**Quick Navigation:** [Architecture](#architecture) • [Quick Start](#quick-start) • [Module Map](#module-map) • [Ecosystem](#ecosystem) • [For Contributors](#contributing) • [Community](#community) --- @@ -66,7 +67,7 @@ HugeGraph supports both **standalone** and **distributed** deployments: │ HugeGraph Server (:8080) │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ │ REST API │ │ Gremlin │ │ Cypher Engine │ │ - │ │(Jersey 3)│ │ (TP 3.5) │ │ (OpenCypher) │ │ + │ │(Jersey 3)│ │(TP 3.8.1)│ │ (OpenCypher) │ │ │ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │ │ └─────────────┼─────────────────┘ │ │ ┌────────▼────────┐ │ @@ -100,6 +101,8 @@ HugeGraph supports both **standalone** and **distributed** deployments: See the [backend evolution guide](hugegraph-server/README.md#backend-evolution-and-compatibility) for lifecycle and historical compatibility guidance. +To use ToplingDB with the RocksDB backend, follow the [three-step switching guide](docs/storage-lifecycle.md#storage-provider-selection-and-toplingdb). + ### Deployment Mode Comparison | Mode | Components | Use Case | Data Scale | High Availability | @@ -115,6 +118,9 @@ See the [backend evolution guide](hugegraph-server/README.md#backend-evolution-a | [hugegraph-pd](hugegraph-pd/README.md) | Placement Driver for distributed mode - handles meta storage, partition management and cluster scheduling | | [hugegraph-store](hugegraph-store/README.md) | Distributed storage with Raft consensus for high availability and horizontal scaling | | [hugegraph-commons](hugegraph-commons) | Shared utilities, RPC framework and common components | +| [hugegraph-struct](hugegraph-struct/README.md) | Shared schema, IDs, base elements, queries and encoding used by Server and Store | + +See the [1.8.0 shared-foundation migration guide](docs/shared-foundation-migration.md) for Java API changes, module ownership and coordinated upgrade requirements.
📊 Click to view detailed architecture diagram (Mermaid) @@ -125,7 +131,7 @@ flowchart TB subgraph Server["HugeGraph Server :8080"] API[REST API
Jersey 3] - GS[Gremlin Server
TinkerPop 3.5] + GS[Gremlin Server
TinkerPop 3.8.1] CS[Cypher Engine
OpenCypher] CORE[Graph Engine
hugegraph-core] @@ -157,6 +163,8 @@ flowchart TB ## Quick Start + + ### 5 Minutes Quick Start ```bash @@ -176,8 +184,15 @@ curl -X POST http://localhost:8080/gremlin \ ### Prerequisites -- **Java 11+** (required) -- **Maven 3.5+** (for building from source) +- **Java 17** (required and currently the only supported Java release) +- **Maven 3.6.3+** (for building from source) + +The launch scripts reject Java versions older than 17. That minimum-version +check does not qualify later Java releases; use Java 17 unless another release +is explicitly listed as supported. + +Read the [Server runtime guide](docs/server-runtime.md#runtime-and-client-configuration) +for client configuration and query compatibility checks. ### Option 1: Docker (Fastest) @@ -207,9 +222,16 @@ For advanced Docker configurations, see: > > **Version Tags**: Use release tags (e.g., `1.7.0`) for stable deployments. The `latest` tag should only be used for testing or development. +### Option 2: Kubernetes with Helm + +The HStore Helm chart deploys HugeGraph PD, Store, and Server as a distributed +Kubernetes cluster. See the [chart documentation](helm/hugegraph/README.md) for +single-node and highly available presets, configuration, and +upgrade guidance. +
-Option 2: Download Binary Package +Option 3: Download Binary Package Download pre-built packages from the [Download Page](https://hugegraph.apache.org/docs/download/download/): @@ -242,7 +264,7 @@ For detailed instructions, see the [Binary Installation Guide](https://hugegraph
-Option 3: Build from Source +Option 4: Build from Source Build from source for development or customization: @@ -281,7 +303,7 @@ curl http://localhost:8080/versions # "versions": { # "version": "v1", # "core": "1.7.0", -# "gremlin": "3.5.1", +# "gremlin": "3.8.1", # "api": "1.7.0" # } # } @@ -296,6 +318,8 @@ gremlin> :> g.V().limit(5) For comprehensive documentation, visit the [HugeGraph Documentation](https://hugegraph.apache.org/docs/). +Read the [storage lifecycle and recovery guide](docs/storage-lifecycle.md) before restoring data, mounting store directories or upgrading the storage runtime. +
## Module Map @@ -327,7 +351,7 @@ For repository constraints and module guidance, see [AGENTS.md](AGENTS.md). - Review the [Architecture Diagram](#architecture) above 2. **Set Up Your Environment** - - Install Java 11+ and Maven 3.5+ + - Install Java 17 and Maven 3.6.3+ - Follow [BUILDING.md](docs/BUILDING.md) for build instructions - Configure your IDE to use `.editorconfig` for code style and `style/checkstyle.xml` for Checkstyle rules diff --git a/docker/README.md b/docker/README.md index a9d43dcb1f..de89b98605 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,5 +1,7 @@ # HugeGraph Docker Compose + + ## Users ### Choose a topology @@ -48,6 +50,27 @@ A non-empty `HUGEGRAPH_ADMIN_PASSWORD` enables Server authentication, and Hubble `HUGEGRAPH_ADMIN_PASSWORD` initializes the built-in `admin` account on its first authenticated startup. Changing `.env` does not rotate an existing administrator password; use the HugeGraph user API for credential changes. +The Server image writes `PASSWORD` to `auth.admin_pa` in the REST configuration. +It writes `HG_SERVER_AUTH_TOKEN_SECRET` to `auth.token_secret` in both REST and +graph configuration. An explicit signing key must be at least 32 bytes. Authentication +string values retain spaces, tabs, backslashes and Unicode through the Server's +properties reader; keep the same signing key on every replica. Use passwords +compatible with the existing authentication protocol and the chart's documented +password constraints. When the signing-key environment variable is omitted, +the image reuses the configured key instead of rotating it. + +Older images could change signing keys containing spaces or Unicode, and a +trailing space could hide the following configuration property. On upgrade, +explicitly provide the intended key for those deployments. Tokens signed with +the previous interpreted key require a new login after the correction. + +An older image could also initialize a Unicode administrator password incorrectly +when the initializer inherited the C locale. Upgrading or setting `PASSWORD` +again does not repair an existing account's stored password. A working +administrator must rotate it through the user API. This fix provides no automatic +recovery when no administrator identity is usable; preserve the existing data and +arrange account recovery before upgrading. + For the verification commands below, load `.env` into your current shell and set the password: ```bash @@ -69,6 +92,22 @@ To regenerate Hubble configuration after modifying `.env`: ./set-hubble-pd-password.sh hstore # or hstore-ha ``` +### Server heap sizing + +The Server launcher sizes its default heap from the smaller of host available memory +and remaining memory under visible cgroup hard limits. It supports cgroup v1/v2, +ancestor limits and custom controller mounts. Charged inactive file cache is counted +as reclaimable when controller statistics are available and consistent. The default heap +uses half that budget, with a 512 MiB minimum and a 32 GiB maximum; startup fails if the budget is below +512 MiB. Native allocations, thread stacks and file caches also consume container +memory, so the heap limit does not bound the process's total memory use. + +Explicit heap options still take precedence. For example, use `JAVA_OPTS=-Xmx2g` +for a fixed Server heap in a Docker container, leaving room for other allocations +within its memory limit. Replacing `JAVA_OPTS` also replaces the image's default +options; repeat any defaults you need. JVM container-awareness flags cannot reduce +an explicit `-Xmx` that exceeds the container's limit. + ### Standalone This is the recommended quickstart. @@ -133,6 +172,22 @@ curl -fsS http://localhost:8088/about
+### PD health and readiness + +PD's `/v1/ready` returns HTTP 503 while it cannot serve through a Raft quorum. +`/v1/health` remains HTTP 200 during an election or temporary quorum loss, but +returns HTTP 503 after a fatal Raft error or state-machine shutdown. Both health +responses retain the empty text body. Probes read cached state without waiting +for the Raft node lock. + +A failed snapshot, for example after the PD disk fills, leaves JRaft in a terminal +error state. Free disk space before restarting the affected PD; freeing space +alone does not reinitialize its Raft node. Configure liveness against `/v1/health` +with an image containing this fix and gate traffic on `/v1/ready` using an external +readiness gate. The shipped Compose `service_healthy` dependencies still use +`/v1/health`; they do not wait for PD quorum readiness. Older images +still need the single-PD readiness-based liveness workaround supplied by the Helm chart. + ### Stop or remove a deployment `stop` keeps containers and data; `down` removes containers and the network but keeps data. **`down -v` also deletes all topology data.** @@ -230,6 +285,95 @@ Standalone stores RocksDB data at `/hugegraph-server/rocksdb-data`. The HStore t `docker compose down` keeps named-volume data. `docker compose down -v` intentionally deletes it. +### Server service identity + +Server image builds containing this configuration mapping accept +`HG_SERVER_SERVICE_ID` and `HG_SERVER_GRAPHSPACE`, which write `server.service_id` and `server.graphspace` +in `conf/rest-server.properties`. A property keeps its existing value when both +its `HG_SERVER_*` variable and its older name are unset or empty; the shipped +defaults are both `DEFAULT`. +The older `SERVICE_ID` and `GRAPH_SPACE` variables emitted by the Kubernetes driver +are also accepted. A non-empty `HG_SERVER_*` value takes precedence over its older name. +These container identity inputs accept printable ASCII (bytes 0x20–0x7E); +spaces and backslashes in service IDs round-trip unchanged. Non-ASCII and control +characters are rejected before the entrypoint writes configuration, because the +Server reads properties using ISO-8859-1. This is the container input's +serialization limit; it does not change the core service-name contract. +The full service ID, including `/` and `-`, is retained when service metadata is +reloaded or its graph space is cleared. +`HG_SERVER_GRAPHSPACE` and a migrated `GRAPH_SPACE` must also match the Server +API graph-space rule: `DEFAULT`, or up to 48 lowercase letters, digits and +underscores, starting with a letter. Invalid environment values are rejected +before writing configuration; existing mounted properties are preserved. + +For a PD-backed Server, add the variables to the Server container's environment, +for example `HG_SERVER_SERVICE_ID=analytics` and `HG_SERVER_GRAPHSPACE=team_a`. +Create `team_a` in the existing cluster before starting the assigned Server; +these settings do not create graph spaces. The Compose files do not pass these +variables through from the host, so add explicit `environment:` entries to a +local Compose override. The Helm chart exposes the same settings as +[`server.serviceId` and `server.graphSpace`](../helm/hugegraph/README.md#server-service-identity). + +Use a chart and Server image from the same source version containing these +mappings. Older release images without them do not apply these environment +values; select the matching image build explicitly. +The Server's existing `SERVICE_ID` normalization (`GraphManager.serviceId()`) is +unchanged and can map different raw names to the same identifier. Select +`GRAPHSPACE` and the complete raw `SERVICE_NAME` in discovery queries; +identifier-only lookup does not provide a uniqueness or isolation guarantee. + +This selects the Server's PD service metadata. It does not change +`server.path_graphspace` for REST paths that omit `/graphspaces/`, or +configure Hubble's chosen service. Actual non-default service discovery, +authentication and graph-space isolation still require verification with a +running Linux cluster. + +### Server logs and crash files + +The Server container owns its foreground launcher and returns the JVM's exit status after startup, +including 0 for a normal exit and 137 when the JVM is killed with SIGKILL. TERM and INT request +shutdown through the launcher using TERM, which shuts down and reaps the JVM; the entrypoint returns +143 and 130 respectively. +The startup REST check still uses `HG_SERVER_STARTUP_TIMEOUT_S`, and HStore partition checks still +run after readiness. Shutdown interrupts a stalled readiness probe or pending partition check and +reaps its polling processes. +The partition poll and its final `/v1/partitions` diagnostic each have a separate +`WAIT_PARTITION_TIMEOUT_S` budget (120 seconds by default). If the JVM exits while the +final diagnostic is stalled, the entrypoint observes its exit after that request finishes +or reaches this bound; it does not monitor the JVM immediately during the diagnostic. +A startup timeout requests termination and returns 1 without waiting for an +unresponsive JVM beyond the startup deadline; container termination cleans up remaining processes. +Graceful shutdown requires the JVM to respond to TERM; forceful OOM and orchestrator shutdown testing +needs a Linux environment. + +This section describes images built from current master. Images from 1.7.0 and earlier do not set `STDOUT_MODE`: `docker logs` shows only the entrypoint's output, and the service logs stay in `logs/` inside the container. + +Both Server images (`hugegraph/hugegraph` and `hugegraph/server`) write the HugeGraph log to container stdout. WARN and above from Hadoop, ZooKeeper, SOFA, Netty and Commons also reaches stdout. INFO from Hadoop, Netty and Commons, the audit log and the slow-query log stay in files only; ZooKeeper and SOFA log nothing below WARN. Errors that `hugegraph-server.sh` reports before Java starts, such as an unsupported JDK or too little free memory, and fatal bootstrap errors go to stderr and, when `logs/` can be written, also to `hugegraph-server.log`. Earlier entrypoint steps, such as `init-store.sh` in the HStore image, report to stderr only. Either way, when a Server exits during startup, `docker logs` or `kubectl logs --previous` shows why. When the JVM starts, it also prints `Picked up JAVA_TOOL_OPTIONS: ...` with the crash-file defaults below; a start that fails its preflight checks exits before that. + +`/hugegraph-server/logs` holds `hugegraph-server.log`, JVM crash logs (`hs_err_pid__.log`) and out-of-memory heap dumps (`heapdump__/java_pid.hprof`), where `` is the container's local time as `YYYYMMDD-HHMMSS`. Each start gets its own `heapdump_*` directory, so the Server and the JVMs it starts, such as computer jobs, write separate dumps. HotSpot names each dump and crash log after the JVM's PID, so a JVM that reuses the PID of an earlier one from the same start cannot write its dump, and its crash log replaces the earlier one's. The host name (on Kubernetes, the pod name, or `spec.hostname` when that is set) and a `-1`, `-2`, ... counter keep names from clashing; the counter also separates pods that share a host name. If the directory cannot be created, for example on a full volume, the Server still starts and dumps go to `logs/` itself as `java_pid.hprof`. That is best effort: a full volume may have no room for the dump either, and a restarted container that reuses a PID cannot write over an earlier dump with the same name. If the logs path contains a double quote or `%`, the launcher warns and heap dumps go to `logs/` itself, with no per-launch directory and no crash log default; the `JAVA_TOOL_OPTIONS` opt-out below still applies. A path that contains both quote characters gets no heap dump default at all. + +Both images declare `VOLUME /hugegraph-server`, so `docker restart` and a Compose recreate keep the files in that anonymous volume. `docker compose down` leaves the volume behind but the next `up` does not attach it, so the files become hard to reach and keep using disk. A later `docker compose down -v` does not remove that orphaned volume either, since it only removes anonymous volumes of the containers it removes; note the volume name with `docker inspect` before `down` and delete it with `docker volume rm `, or stop with `docker compose down -v` in the first place. Avoid `docker volume prune` for this: it deletes every unused anonymous volume on the Docker host, including other projects' data. Mount a named volume or a bind mount at `/hugegraph-server/logs` to keep the files reachable. Kubernetes starts a new container on every restart and does not turn the image's `VOLUME` into a pod volume, so mount an `emptyDir` or a PersistentVolumeClaim at `/hugegraph-server/logs`. Give each pod its own volume, or a per-pod directory on a shared PVC: crash files get distinct names, but `hugegraph-server.log`, the audit log and the slow-query log have fixed names, and pods sharing them would roll them over for each other. For a per-pod directory, pass the pod name in through the Downward API and use it in `subPathExpr` (the Helm chart sets neither): + +```yaml +env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name +volumeMounts: + - name: server-logs + mountPath: /hugegraph-server/logs + subPathExpr: $(POD_NAME) +``` + +The Helm chart in [`helm/`](../helm/hugegraph) does not mount a logs volume yet, so with it these files are lost when a container restarts. + +A heap dump can be as large as the JVM heap. Every start uses a new directory and the JVMs a Server starts can dump too, so a Server that keeps running out of memory fills the volume with heap-sized files. Size the volume for the dumps you want to keep plus the logs, and keep it within any `ephemeral-storage` limit or `emptyDir` `sizeLimit`: eviction deletes an `emptyDir` together with the dump in it. Do not use an `emptyDir` with `medium: Memory`, which counts against the memory limit. A liveness probe that restarts the pod while a large dump is being written leaves a truncated file. + +The launcher never deletes dumps or `heapdump_*` directories. Each start that gets as far as starting Java leaves a directory behind, empty unless something ran out of memory; the launcher keeps it because a computer-job JVM can go on using it after its Server exits. Move or delete old directories once no HugeGraph JVM from that launch is running, but never the newest one of a running Server: without its directory, the next dump is written as a file with that name and later dumps from that launch fail. + +To turn heap dumps off for every JVM, set `JAVA_TOOL_OPTIONS=-XX:-HeapDumpOnOutOfMemoryError`, which also keeps the image's default `JAVA_OPTS`. Set it in the container's environment, not in the host shell: `docker run -e JAVA_TOOL_OPTIONS=...`, an `environment:` entry on the Server service in Compose (the shipped Compose files do not pass it through), or `env` on the container in Kubernetes. Putting the flag in `JAVA_OPTS` turns dumps off for the Server JVM only, because the JVMs it starts do not see its command line, and setting `JAVA_OPTS` replaces the image default (`-XX:+UseContainerSupport -XX:MaxRAMPercentage=50 ...`), so repeat those flags. Either way, each start still creates its empty `heapdump_*` directory, unless creating it failed or the logs path contains a double quote or `%`. + ## Developers ### Images and Compose files diff --git a/docs/BUILDING.md b/docs/BUILDING.md index d4c807c748..d111820fd7 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -3,11 +3,47 @@ Building HugeGraph Required: -* Java 11 -* Maven 3.5+ +* Java 17 (currently the only supported Java release) +* Maven 3.6.3+ + +The launch scripts enforce Java 17 as the minimum runtime. This check does not +qualify later Java releases; build HugeGraph with Java 17 unless another +release is explicitly listed as supported. + +The published `hugegraph-common` and `hugegraph-rpc` libraries use Java 11 +bytecode for downstream compatibility. Their build and tests still run on +Java 17; this library bytecode target does not lower HugeGraph's runtime +requirement. + +See the [Server runtime guide](server-runtime.md#runtime-and-client-configuration) for the +upgraded runtime, client configuration and compatibility checks. To build without executing tests: `mvn clean package -Dmaven.test.skip=true` +## Optional word analyzer + +The standard distribution does not include `org.apdplat:word:1.3`. It is a +provided dependency used to compile and test the optional `word` analyzer. +The default analyzer remains `ikanalyzer`; the shipped graph configurations +use `jieba`, and neither requires the word library. + +Obtain the GPLv3-licensed external component from +[Maven Central](https://repo.maven.apache.org/maven2/org/apdplat/word/1.3/word-1.3.jar). + +To use `word`, supply `word-1.3.jar` separately in the Server's `lib/` directory +and restart the Server. Select it in the graph configuration with a supported +mode, for example: + +```properties +search.text_analyzer=word +search.text_analyzer_mode=PureEnglish +``` + +Selecting `word` without its external library fails with a configuration error +identifying the missing optional dependency. Other analyzers remain available. +Applications using Struct directly must add the same dependency to their +runtime classpath when selecting `word`. + ## Building in IDEA To build without executing tests: @@ -38,4 +74,3 @@ To build without executing tests: To find the Java binary in your environment, run the appropriate command for your operating system: * Linux/macOS: `which java` * Windows: `for %i in (java.exe) do @echo. %~$PATH:i` - diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index bfa9d7e66f..58bf79c315 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,187 +1,90 @@ -# How to Contribute to HugeGraph +# Contributing to HugeGraph -> Refer [website-doc](https://hugegraph.apache.org/docs/contribution-guidelines/) for the latest information. +Keep each contribution focused on one problem, with enough context and validation for review. +Report reproducible bugs and proposed features through [GitHub issues](https://github.com/apache/hugegraph/issues). -Thanks for taking the time to contribute! -As an open source project, HugeGraph is looking forward to being contributed from everyone, and we are also grateful to all the contributors. +## Prepare a change -The following is a contribution guide for HugeGraph: +- Read [AGENTS.md](../AGENTS.md) and the guidance for the modules you will change. + The same repository rules apply to changes made by people and coding agents. +- Work on a topic branch or isolated worktree based on the intended target branch. + Preserve existing uncommitted work; stage only files belonging to your change. +- Check existing implementations and dependencies before adding new abstractions or libraries. + Keep unrelated cleanup out of the PR. +- Follow [.editorconfig](../.editorconfig) and [Checkstyle](../style/checkstyle.xml): + four-space indentation, 120-column Java lines, and no wildcard imports. -image +## Build and validate -## 1. Preparation +Use Java 17 and Maven 3.6.3+. Compiler settings and project versions are defined in +[pom.xml](../pom.xml); module guidance describes test profiles and service prerequisites. +Run commands from the repository root. -**Recommended**: You can use [GitHub desktop](https://desktop.github.com/) to greatly simplify the PR process. +```bash +# Build the server and its dependencies without running tests +mvn clean install -pl hugegraph-server -am -DskipTests -We can contribute by reporting issues, submitting code patches or any other feedback. - -Before submitting the code, we need to do some preparation: - -1. Sign up or login to GitHub: [https://github.com](https://github.com) - -2. Fork HugeGraph repo from GitHub: [https://github.com/apache/hugegraph/fork](https://github.com/apache/hugegraph/fork) - -3. Clone code from fork repo to local: [https://github.com/${GITHUB_USER_NAME}/hugegraph](https://github.com/${GITHUB_USER_NAME}/hugegraph) - - ```shell - # clone code from remote to local repo - git clone https://github.com/${GITHUB_USER_NAME}/hugegraph.git hugegraph - ``` - -4. Configure local HugeGraph repo - - ```shell - cd hugegraph - - # add upstream to synchronize the latest code - git remote add hugegraph https://github.com/apache/hugegraph - - # set name and email to push code to github - git config user.name "{full-name}" # like "Jermy Li" - git config user.email "{email-address-of-github}" # like "jermy@apache.org" - ``` - -## 2. Create an Issue on GitHub - -If you encounter bugs or have any questions, please go to [GitHub Issues](https://github.com/apache/hugegraph/issues) to report them and feel free to [create an issue](https://github.com/apache/hugegraph/issues/new). - -## 3. Make changes of code locally - -#### 3.1 Create a new branch - -Please don't use master branch for development. Instead, we should create a new branch: - -```shell -# checkout master branch -git checkout master -# pull the latest code from official hugegraph -git pull hugegraph -# create new branch: bugfix-branch -git checkout -b bugfix-branch -``` - -#### 3.2 Change the code - -Assume that we need to modify some files like "HugeGraph.java" and "HugeFactory.java": - -```shell -# modify code to fix a bug -vim hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java -vim hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeFactory.java -# run test locally (optional) -mvn test -Pcore-test,memory -``` -Note: Code style is defined by the `.editorconfig` file at the repository root. Checkstyle rules are defined in `style/checkstyle.xml`. Configure your IDE accordingly. - -##### 3.2.1 Check licenses -If we want to add new third-party dependencies to the `HugeGraph` project, we need to do the following things: -1. Find the third-party dependent repository, put the dependent `license` file into [./install-dist/release-docs/licenses/](https://github.com/apache/hugegraph/tree/master/install-dist/release-docs/licenses) path. -2. Declare the dependency in [./install-dist/release-docs/LICENSE](https://github.com/apache/hugegraph/blob/master/install-dist/release-docs/LICENSE) `LICENSE` information. -3. Find the NOTICE file in the repository and append it to [./install-dist/release-docs/NOTICE](https://github.com/apache/hugegraph/blob/master/install-dist/release-docs/NOTICE) file (skip this step if there is no NOTICE file). -4. Execute locally [./install-dist/scripts/dependency/regenerate_known_dependencies.sh](https://github.com/apache/hugegraph/blob/master/install-dist/scripts/dependency/regenerate_known_dependencies.sh) to update the dependency list [known-dependencies.txt](https://github.com/apache/hugegraph/blob/master/install-dist/scripts/dependency/known-dependencies.txt) (or manually update). - -**Example**: A new third-party dependency is introduced into the project -> `ant-1.9.1.jar` -- The project source code is located at: https://github.com/apache/ant/tree/rel/1.9.1 -- LICENSE file: https://github.com/apache/ant/blob/rel/1.9.1/LICENSE -- NOTICE file: https://github.com/apache/ant/blob/rel/1.9.1/NOTICE - -The license information of `ant-1.9.1.jar` needs to be specified in the LICENSE file, and the notice information needs to be specified in the NOTICE file. The detailed LICENSE file corresponding to ant-1.9.1.jar needs to be copied to our licenses/ directory. Finally, update the known-dependencies.txt file. - -#### 3.3 Commit changes to git repo - -After the code has been completed, we submit them to the local git repo: - -```shell -# add files to local git index -git add hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java -git add hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeFactory.java -# commit to local git repo -git commit -``` - -Please edit the commit message after running `git commit`, we can explain what and how to fix a bug or implement a feature, the following is an example: - -```sh -Fix bug: run deploy multiple times - -fix #ISSUE_ID +# Before pushing code, format and compile +mvn editorconfig:format +mvn clean compile -Dmaven.javadoc.skip=true ``` -> Please remember to fill in the issue id, which GitHub generated after issue creation. - -#### 3.4 Push commit to GitHub fork repo - -Push the local commit to GitHub fork repo: - -```shell -# push the local commit to fork repo -git push origin bugfix-branch:bugfix-branch +The root POM and child modules flatten CI-friendly versions during `process-resources`, +so installed and deployed POMs use a concrete version instead of `${revision}`. Build the +reactor with `install` before building modules separately; keep Maven Resolver validation +enabled, including on Maven 3.10+. + +Run the affected module tests and add regression coverage when behavior changes. +A successful build with skipped tests does not validate behavior; Commons tests require +`-DskipCommonsTests=false`. For documentation-only changes, verify links and paths and run +`git diff --check`. Inspect formatter output before staging to avoid unrelated edits. + +See [CI policy](ci.md) for affected-module selection, required checks, result reuse, +and automatic cancellation and retries. Report what you actually validated and any limitations. + +## Documentation and dependencies + +- User-visible features, configuration and deployment changes need matching repository + documentation in the same PR. When website documentation is affected, link a paired + [apache/hugegraph-doc](https://github.com/apache/hugegraph-doc) PR and coordinate both merges. + A follow-up issue does not replace the required documentation. +- In the PR template, use `Doc - TODO` while documentation is pending, `Doc - Done` with + its location when ready, or `Doc - No Need` for internal-only changes. +- New or updated dependencies need appropriate license and notice updates in the root + and [release documentation](../install-dist/release-docs/), including bundled license files. + Update the [dependency inventory](../install-dist/scripts/dependency/known-dependencies.txt) + using [the regeneration script](../install-dist/scripts/dependency/regenerate_known_dependencies.sh). + +Install/package the current reactor before regenerating the inventory: + +```bash +mvn clean install -DskipTests -Dmaven.javadoc.skip=true +bash install-dist/scripts/dependency/regenerate_known_dependencies.sh ``` -Note that since GitHub requires submitting code through `username + token` (instead of using `username + password` directly), you need to create a GitHub token from https://github.com/settings/tokens: -image - -## 4. Create a Pull Request - -Go to the web page of GitHub fork repo, there would be a chance to create a Pull Request after pushing to a new branch, click the button "Compare & pull request" to do it. Then edit the description for proposed changes, which can just be copied from the commit message. - -User-visible feature, configuration or deployment behavior changes must ship with matching -documentation. Include repository documentation in the same PR. When website documentation -is affected, link a paired [hugegraph-doc](https://github.com/apache/hugegraph-doc) PR and -coordinate both merges. A follow-up issue alone does not replace required documentation. -In the PR template, mark pending documentation as `Doc - TODO` until it is ready; -use `Doc - Done` with the documentation files or PR link, or `Doc - No Need` for internal-only changes. - -Note: please make sure the email address you used to submit the code is bound to the GitHub account. For how to bind the email address, please refer to https://github.com/settings/emails: -image - -## 5. Code review - - Maintainers will start the code review after all the **automatic** checks are passed: - -- Check: Contributor License Agreement is signed -- Check: Travis CI builds are passed (automatically Test and Deploy) - -The commit will be accepted and merged if there is no problem after review. - -Please click on "Details" to find the problem if any check does not pass. - -If there are checks not passed or changes requested, then continue to modify the code and push again. - -## 6. Further changes after review - -If we have not passed the review, don't be discouraged. Usually a commit needs to be reviewed several times before being accepted! Please follow the review comments and make further changes. - -After the further changes, we submit them to the local repo: - -```shell -# commit all updated files in a new commit, -# please feel free to enter any appropriate commit message, note that -# we will squash all commits in the pull request as one commit when -# merging into the master branch. -git commit -a -``` - -> If there are conflicts that prevent the code from being merged, we need to rebase on master branch: -> -> ```shell -> # synchronize the latest code -> git checkout master -> git pull hugegraph -> # rebase on master -> git checkout bugfix-branch -> git rebase -i master -> ``` - -And push it to GitHub fork repo again: - -```shell -# force push the local commit to fork repo -git push -f origin bugfix-branch:bugfix-branch -``` - -GitHub will automatically update the Pull Request after we push it, wait for code review. - -For Any question, please contact us through [dev@hugegraph.apache.org](mailto:dev@hugegraph.apache.org) ([subscriber](https://hugegraph.apache.org/docs/contribution-guidelines/subscribe/) only) - - +Clean rebuilds the current revision's distributions to avoid stale dependency jars. +It also removes root-level `*.tar.gz` distribution archives; other revision directories remain. +The build must produce Server, PD and Store distributions for the same current revision. +It skips tests and is not validation evidence. The inventory combines Maven runtime +dependencies with flat and nested distribution jars, including Spring Boot `BOOT-INF/lib` +dependencies inserted by repackaging. Missing distributions fail collection; source or POM +inspection alone cannot establish the full shipped inventory. Review all additions and +removals and their license/NOTICE coverage. The dependency check compares the exact inventory; +repeat it for release platform/profile variants as needed. + +## Submit and review + +Use `type(scope): description` for commits; omit the scope when it is unclear. +Types are `feat`, `fix`, `chore`, `docs`, `refactor`, or `BREAKING CHANGE`. +Start the description with a lowercase verb, keep it under 50 characters, and include +three to five core change points in the body. For example: `fix(server): handle missing vertices`. + +Push your topic branch and open or update a PR against the intended upstream branch. +Use the [PR template](../.github/PULL_REQUEST_TEMPLATE.md) to explain the problem, +the resulting behavior, relevant validation, and documentation status. Link the related +issue when applicable. A short example or diagram is useful when it clarifies the change; +commit histories and raw test logs are unnecessary. + +Address review findings within the agreed scope and validate the affected paths again. +Resolve a discussion after its issue is fixed or clarified. Inspect failing CI jobs before +retrying; a rerun is appropriate for a temporary infrastructure failure, not a reproducible bug. diff --git a/docs/ci.md b/docs/ci.md new file mode 100644 index 0000000000..6744a1e39f --- /dev/null +++ b/docs/ci.md @@ -0,0 +1,144 @@ +# CI policy + +[HugeGraph-Server CI](../.github/workflows/server-ci.yml) is the single entry point for +PRs, supported branch pushes, weekly coverage and manual validation. Its preflight job +selects affected tests, reads the project Java version, checks CI policy and verifies +license headers and RAT. Selected suites run through reusable workflows in the same run. + +CI results become advisory after [`.asf.yaml`](../.asf.yaml) is applied. It requests no required CI contexts +and retains one approving review. This protection change takes effect after the configuration +is merged and synchronized by Gitbox; verify live protection then. Failed tests keep their +real failed results. Valid selection and Java outputs allow selected tests to run even when +the preflight license checks fail. + +Preflight keeps its real `check-license` name. The temporary `Server memory tests` +protection bridge has been removed after confirming both ASF and org master required +contexts are empty. Actual backend jobs are named `Server memory tests (Java ...)` +and `Server rocksdb tests (Java ...)`; their native checks report real execution. + +## Coverage with selective tests + +Each coverage upload has a stable test-group flag. The [Codecov configuration](../.github/codecov.yml) +enables [carryforward](https://docs.codecov.com/docs/carryforward-flags). When Codecov can +find an earlier flagged report in its recorded parent chain, it preserves that historical +report for unexecuted groups such as Commons, PD/Store and backends. +The PR comment explicitly marks carried-forward data. It is historical coverage, +not evidence that a skipped test ran; native job failures remain failures. + +New flags need a complete initial baseline. The existing weekly full run supplies every +flagged group for that commit. Master pushes remain smoke-only and upload no coverage; +those commits can break Codecov's recorded parent chain. A fresh selective PR based on +such a commit is not guaranteed to inherit the weekly baseline, and its project report +may still be incomplete. Carryforward within an existing reported PR chain does not +prove this works for a fresh PR. Full and selected runs retain explicit report paths and +uploads; no extra collector runner or repeated master coverage tests are added. + +## What runs + +| Validation | Ordinary PR | Push to master | Weekly full run | +| --- | --- | --- | --- | +| Memory and RocksDB | Full unit, core and API tests when affected | No repeated full tests | Full tests | +| macOS ARM | Startup, JNI, graph read/write and restart smoke when affected; full core/API for platform inputs | Affected smoke | Full core/API | +| HBase and RISC-V | Directly related backend/platform or validation inputs only | No repeated tests | Full validation | +| PD, Store, HStore and Cluster | Affected modules and known consumers | Current-commit distributed smoke | Full group | +| CodeQL | Authentication, authorization, security, schema cache transactions, POM or CodeQL configuration inputs | No repeated scan | Full scan | +| Docker, Helm and dependency audit | Corresponding inputs | Corresponding deployment smoke or checks | Full validation | + +macOS uses one `macos-15` ARM job for either smoke or full validation. Intel macOS is removed. +The weekly run uses `cron: '0 6 * * 1'` with `timezone: Asia/Singapore`: Monday at 06:00, +on the latest default-branch commit. GitHub may delay scheduled runs during high load; +see [schedule behavior](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule). + +### Affected inputs + +The rules live in [ci-policy.py](../.github/scripts/ci-policy.py). Ordinary Server changes +select both Memory and RocksDB, plus ARM smoke. They do not select the complete PD/Store group. +PD and Store changes select their own tests and downstream consumers; Commons and Struct +select their known shared consumers. HStore and Cluster additionally follow distributed +backend contracts, protocols, metadata, graph lifecycle and scheduling inputs. Distribution, +startup and RocksDB inputs select ARM full validation and their deployment consumers. + +Unclassified paths or an unavailable diff/API conservatively select mainline coverage. +HBase and RISC-V remain limited to explicit related inputs, weekly coverage or manual selection. +The classifier uses paths, not PR titles or semantic interpretation of changed text. +Contract scripts explicitly select their executing suites without adding runtime consumers. + +### Documentation + +Documentation-only ordinary PRs and pushes allocate just the preflight CI runner. This count +excludes the non-test [Auto PR Commenter](../.github/workflows/auto-pr-review.yml), which runs +once when a PR opens. Scheduled and manual selections still run their requested suites. + +Plain documentation includes regular-file root prose, README/AGENTS files and supported prose +or static images under `docs/`, including nested module documentation. Runtime resources, +configuration, source, fixtures, templates and executable or symbolic-link files do not qualify; +for example, a test fixture ending in `.txt` still selects its module's tests. + +## Manual validation and reruns + +Use **Actions → HugeGraph-Server CI → Run workflow**, select a branch and choose `test-set`: + +| Test set | Validation | +| --- | --- | +| `mainline` (default) | Full mainline modules, deployment/dependency checks and CodeQL; ARM smoke | +| `distributed` | Struct, PD, Store, HStore and Cluster, plus distributed smoke | +| `macos-arm` | ARM smoke and full RocksDB core/API tests | +| `hbase`, `riscv64`, `codeql` | The corresponding complete specialist validation | +| `full` | All retained suites, identical to weekly coverage | + +Fork contributors run additional validation in their fork's Actions. The manual entry point +does not accept a PR number, apply labels or reuse another workflow's results. +Automatic retries are disabled. Use GitHub's native rerun controls for failed jobs or a run; +a rerun retains its original commit and event, subject to the preflight freshness checks. + +## Java runtime compatibility + +A positive suite passes only when its tests succeed and its current-run output has no +Java runtime compatibility errors. Server, PD/Store, Commons, Struct and Cluster test +steps capture both stdout and stderr before running `check-java-runtime-logs.sh`. +Startup and Console smoke tests also inspect their output before cleanup. Cluster nodes +forward their text runtime logs to the captured suite output before deletion. +Missing or unreadable required logs fail the check. + +The gate rejects denied module access and unknown or removed JVM options, including +exceptions caught and printed by dependencies. Ordinary business errors and deliberate +negative fixtures are not compatibility failures. Native smoke additionally uses `--native` +for its existing native linkage checks. + +Launchers and tests share each component's `jvm-module.options`: PD and Store open +`java.lang` for Hessian, while Console and dump-store reuse Server's existing options. +Required module options are independent of custom `JAVA_OPTIONS` or `-j` flags; Surefire +retains the JaCoCo agent. CI continues to use the project Java runtime and its existing +suite-selection and required-check policy. + + +## Freshness, runner use and budgets + +PR selection uses the cumulative diff against the checked-out merge commit's base. A source +PR followed by a documentation-only commit still tests the cumulative source changes. +Preflight verifies that the event merge commit's second parent matches the current PR head. +A stale PR head fails preflight. Target-branch advancement alone does not invalidate a run. +Plans and selection summaries describe this run; previous results are never reused. + +A new PR head cancels the previous PR run, including its called workflows. Manual and weekly +runs remain independent. Each job reports its own result; there are no separate post-test +aggregation runners or end-of-run metadata checks. + +Startup contracts run inside RocksDB validation, and Store stop contracts run inside Struct. +Docker's static checks, deployment contracts and current-image validation share one runner. +The Docker job has a 90-minute total budget, with 25 minutes for deployment contracts and +45 minutes for image build/runtime validation. Distributed smoke builds current PD, Store +and Server images and checks authentication, readiness and graph read/write. It does not +substitute published images for the current commit. + +PD, Store and Commons have total job budgets of 30, 45 and 20 minutes. Cluster has 120 minutes +for its two 45-minute test windows and build/diagnostics. Other budgets are in the workflows. +TinkerPop suites keep their dedicated branch policy: the source, target or push/manual branch +must start with `release-`, `test-` or `tinkerpop-`. Executed suites require non-empty reports +with actual tests. + +Each separate job or matrix item requests a runner; steps within a job share one. +[Standard hosted runners are free for public repositories](https://docs.github.com/en/actions/reference/runners/github-hosted-runners#standard-github-hosted-runners-for-public-repositories), +but [plan-dependent total and macOS concurrency limits](https://docs.github.com/en/actions/reference/limits#job-concurrency-limits-for-github-hosted-runners) +still apply. Reducing job allocations and long executions lowers shared-pool demand; +it does not guarantee a specific queue delay. diff --git a/docs/cypher-api.md b/docs/cypher-api.md new file mode 100644 index 0000000000..12bad68e94 --- /dev/null +++ b/docs/cypher-api.md @@ -0,0 +1,191 @@ +# Cypher API response compatibility + +The Cypher endpoint is +`/graphspaces/{graphspace}/graphs/{graph}/cypher`. Submit one statement per +request. GET accepts the `cypher` query argument and does not accept a parameter +map; use POST for parameterized queries. POST with `Content-Type: application/json` +accepts a JSON object containing a nonblank string `cypher` and an optional +object `parameters`, for example: + +```json +{ + "cypher": "MATCH (n:person) WHERE n.name = $name RETURN n.name", + "parameters": {"name": "marko"} +} +``` + +Legacy raw Cypher bodies sent as `application/json` remain accepted. JSON arrays, +scalars, malformed JSON, missing/non-string/blank `cypher`, and a `parameters` +value of null, an array or a scalar are rejected by HTTP request validation. +Omitting `parameters` supplies an empty map. Additional request fields and extra +parameter names are allowed; every parameter referenced by the parsed query must +be present. Individual parameter values may explicitly be null. +Requests are limited to 16 top-level parameter names. This limit is fixed; see +[Cypher HTTP contracts](server-runtime.md#cypher-http-contracts) for the binding +limits and validation rules. + +The Server checks missing bindings before executing the traversal, so GET, raw +POST and JSON POST with an absent referenced name fail with `Missing Cypher +parameter: name`. This uses the execution-failure envelope below, classified as +`HugeGraph.Cypher.ExecutionError` with an empty hint. The upstream translator's +standalone unbound-to-null behavior is not the Server request contract. Do not +send `?parameters=...` to bind a GET query. Invalid binding values, including the +reserved translator null marker, also fail before traversal execution; explicit +JSON null values are accepted and normalized by the Server. + +## Authentication + +Cypher follows the Server authentication mode. When authentication is disabled, +GET and POST requests do not require an `Authorization` header, and the Server +uses an anonymous Gremlin connection. Both the request's `Authorization` header +and credentials in `conf/remote-objects.yaml` are ignored. Mixed modes with REST +authentication disabled and Gremlin authentication enabled are unsupported. +When authentication is enabled, requests still need +valid Basic or Bearer credentials and retain the caller's graph permissions. +Use `bin/enable-auth.sh` to configure authentication consistently for both REST +and Gremlin; enabling only the REST authenticator does not protect the Gremlin +endpoint used to execute Cypher. + +The Gremlin host and port in `conf/remote-objects.yaml` must point to the same +Server's Gremlin endpoint, including when using nondefault ports. + +The API test runner exercises anonymous GET and POST before enabling +authentication, then runs the authenticated suite, including missing-header +rejection and a read-only user's write and cross-graph rejection cases. + +## Response envelope + +Responses retain the Gremlin-compatible top-level fields `requestId`, `status` +and `result`. There is no top-level `errors` field. Existing Java clients can +continue to deserialize both successful and failed queries without an upgrade. + +A successful query has `status.code = 200`, an empty `status.attributes` map, +and its rows in `result.data`, for example: + +```json +{ + "requestId": "example-request", + "status": {"message": "", "code": 200, "attributes": {}}, + "result": {"data": [{"value": 1}], "meta": {}} +} +``` + +When query submission or execution fails inside the Cypher client, the existing +HTTP 200 / `status.code = 400` convention and `status.message` are preserved. +Authentication, request validation and other HTTP-layer errors remain separate +and need not use this envelope. Consumers must inspect `status.code` instead of +relying only on the HTTP status. + +Structured error metadata lives at `status.attributes.errors`, an array with +one entry for the failed query. Each entry contains a stable string `code`, the +translator/execution `message`, and a `hint` (empty when no specific guidance is +available). Message wording is not a stable contract. Example: + +```json +{ + "requestId": "example-request", + "status": { + "message": "Expected exactly one statement per query but got: 2", + "code": 400, + "attributes": { + "errors": [{ + "code": "HugeGraph.Cypher.SyntaxError", + "message": "Expected exactly one statement per query but got: 2", + "hint": "Submit exactly one Cypher statement per request" + }] + } + }, + "result": {"data": null, "meta": {}} +} +``` + +Clients that only consume `status.code`, `status.message` and `result` can ignore +the attributes. A failure with no mapped metadata may have empty attributes. + +## Error codes and hints + +| Code | Recognized failure | Hint | +| --- | --- | --- | +| `HugeGraph.Cypher.SyntaxError` | Parser invalid input, unknown function or semantic error with a `(line N, column M (offset: K))` suffix | Check the query syntax and function names supported by the built-in Cypher translator | +| `HugeGraph.Cypher.SyntaxError` | More or fewer than one statement | Submit exactly one Cypher statement per request | +| `HugeGraph.Cypher.SyntaxError` | `Variable` followed by a backtick-quoted name and `not defined` | Declare the variable in a MATCH, UNWIND or WITH clause before referencing it | +| `HugeGraph.Cypher.ExecutionError` | Undefined vertex label, edge label, property key or index label | Create the schema element via the schema API before running the query | +| `HugeGraph.Cypher.UnsupportedFeature` | Translator messages beginning `Unsupported clause:`, `Unsupported match pattern:`, `Unsupported create pattern:`, `Unsupported property type:`, `Multiple labels are not supported:` or `Custom functions and predicates are not supported on target implementation:` | This construct is not covered by the built-in translator, see the Cypher compatibility guide for supported syntax | +| `HugeGraph.Cypher.ExecutionError` | Other failures, including unrecognized translation failures | Empty string | + +`ExecutionError` is also the fallback for unrecognized translation failures; it +does not mean traversal execution has started. For example, `reduce(...)` or +semantic rejections without a recognized position suffix may return this code +with an empty hint while preserving the original failure message. Classification +recognizes only the translation-1.0.4 forms above and does not cover every +translator failure; runtime errors such as `Unsupported predicate` and +`CONTAINS query ... is not supported` also fall back. +There is no `HugeGraph.Cypher.MissingParameter` contract. + +This contract repairs the unreleased proposal in +[Server #3241](https://github.com/apache/hugegraph/pull/3241). The paired website +work is [Doc #499](https://github.com/apache/hugegraph-doc/pull/499); its author +must incorporate this response contract before coordinated publication. +Parameterized POST requests are already provided by merged +[Server #3289](https://github.com/apache/hugegraph/pull/3289). +The earlier [bound-query fork #238](https://github.com/hugegraph/hugegraph/pull/238) +is a historical reference; use the current request contract above. + +## Temporal functions + +The built-in translator has no temporal functions of its own. The Server folds +the `date()`, `datetime()` and `localdatetime()` constructors into native date +values before translation, so these calls work wherever a bound value is +accepted, for example `MATCH (n) WHERE n.birthday = date('2000-01-01') RETURN n`. +A constructor folds when it has no argument (the current date or date-time), a +string literal argument, or a parameter bound to a string; `date()` uses the +`yyyy-MM-dd` form and the other two use `yyyy-MM-ddTHH:mm:ss[.SSS]`. Arguments +that are not foldable, other temporal forms such as map or duration arguments, +and the remaining temporal functions (`time()`, `localtime()`, `duration()`) +keep failing translation with `Unknown function` as before. +Constructor names are case-insensitive like the rest of Cypher, and the bare +constructors of one statement read the clock once. EXPLAIN prints a folded +value as an explicit `new Date(...)` constructor instead of a stringified date. + +## Regression checks + +Run the mapper, real-parser and envelope tests from the repository root: + +```sh +mvn test -pl hugegraph-server/hugegraph-test -am -P unit-test \ + -Dtest=CypherErrorTest,CypherClientTest,CypherOpProcessorTest \ + -Dsurefire.failIfNoSpecifiedTests=false +``` + +`CypherApiTest` also exercises malformed syntax, multiple statements, undefined +variables and missing schema through a running test server. Its `testParameters` +and `testMissingParameterHasFallbackError` cover bound JSON requests and missing +bindings. `CypherClientTest` checks outgoing query/binding transport, and +`CypherOpProcessorTest` covers request binding validation. The mapper's +translator-only unbound-parameter test bypasses that validation. Follow the normal +Server API-test setup using disposable test data. + +The standalone `CypherClientCompatibilityTest` uses the actual Java Client +`Response` and `RestResult` classes. It is outside the Server reactor's source +roots to avoid a Server-to-Client dependency cycle. Supply an existing Client +jar (checked with locally available 1.7.0 and development 1.8.0 artifacts) and run from the repository root: + +```sh +mvn -f hugegraph-server/hugegraph-api/pom.xml dependency:build-classpath \ + -Dmdep.outputFile="$PWD/target/cypher-classpath.txt" +CYPHER_CLIENT_JAR="$HOME/.m2/repository/org/apache/hugegraph/hugegraph-client/1.7.0/hugegraph-client-1.7.0.jar" +CYPHER_CP="$CYPHER_CLIENT_JAR:$(cat target/cypher-classpath.txt)" +mkdir -p target/cypher-compatibility +javac -encoding UTF-8 -proc:none -cp "$CYPHER_CP" \ + -d target/cypher-compatibility \ + hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java \ + hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java \ + hugegraph-server/hugegraph-test/src/compatibility/java/CypherClientCompatibilityTest.java +java -cp "target/cypher-compatibility:$CYPHER_CP" \ + org.junit.runner.JUnitCore CypherClientCompatibilityTest +``` + +These checks cover legacy envelopes, current success/failure deserialization, +status/message preservation, and a negative control proving that a top-level +`errors` field is rejected. They do not certify a live Hubble deployment or +matching-release-candidate acceptance. diff --git a/docs/images/switch-rocksdb-to-toplingdb.png b/docs/images/switch-rocksdb-to-toplingdb.png new file mode 100644 index 0000000000..5f6d92c1cf Binary files /dev/null and b/docs/images/switch-rocksdb-to-toplingdb.png differ diff --git a/docs/same-neighbors-batch.md b/docs/same-neighbors-batch.md new file mode 100644 index 0000000000..2d5777c54f --- /dev/null +++ b/docs/same-neighbors-batch.md @@ -0,0 +1,38 @@ +# Pairwise same neighbors + +`POST /graphspaces/{graphspace}/graphs/{graph}/traversers/sameneighborsbatch` +computes the common neighbors of each vertex pair independently. +It uses the graph and graphspace permission checks of other traverser APIs. + +```json +{ + "vertex_list": [["vertex-a", "vertex-b"], ["vertex-c", "vertex-d"]], + "direction": "BOTH", + "label": "knows", + "max_degree": 10000, + "limit": 100 +} +``` + +`vertex_list` must be present, non-null, and non-empty. Each entry must contain exactly two +non-null vertex IDs, using the numeric/string ID representation. Numeric IDs must +use JSON integer syntax, between -9223372036854775808 and 9223372036854775807. +Repeated pairs remain repeated results. +All pairs are validated before traversal. Missing vertices and unsupported ID +values are rejected through the existing argument error handling. + +`direction` defaults to `BOTH`; allowed values are `OUT`, `IN`, and `BOTH`. +`label` is optional and filters edges for each pair. `max_degree` defaults to +10000 and must be positive or `-1` (unlimited). `limit` defaults to 10000000, +applies separately to each pair, and must be positive up to 2147483647 or `-1`. +The API does not introduce a separate batch-size parameter; callers should bound +the number of pairs and use finite degree/limit values to bound traversal work. + +The response contains `same_neighbors`, an array of arrays in input-pair order, +and `measure`, containing traversal iteration counts and elapsed time. +A pair with no common neighbors contributes an empty array. Neighbor order within +a pair is unspecified. Iteration counts cover all pairs once. + +The existing `/traversers/sameneighbors` endpoints remain unchanged: GET computes +the intersection for two vertices, while POST computes one intersection across +all vertices in its flat `vertex_list`. Their response shapes are unchanged. diff --git a/docs/server-runtime.md b/docs/server-runtime.md new file mode 100644 index 0000000000..e8d0283855 --- /dev/null +++ b/docs/server-runtime.md @@ -0,0 +1,891 @@ +# Server runtime and query contracts + +This guide covers Server configuration, wire formats, query semantics and transaction +ownership. Use it when upgrading applications or changing the +Server runtime. Deployment instructions remain in the [build guide](BUILDING.md) +and [Server module guidance](../hugegraph-server/AGENTS.md). + +## Runtime and client configuration + +The runtime uses TinkerPop 3.8.1 and Groovy 4.0.25. Build and run every module with Java +17. Applications migrating from TinkerPop 3.5.1 must review the full version interval, +including the configuration changes below. + +### Required JVM module options + +Server, store initialization, store dumping and Gremlin Console always load the +distribution's `bin/jvm-module.options`. Keep this file with the shipped scripts; +each launcher rejects a missing or unreadable file. Required module permissions +are independent of caller options, so custom `JAVA_OPTIONS` or the Server launcher's +`-j` option do not replace them. Gremlin Console retains its existing +`JAVA_OPTIONS` defaults; store dumping retains its existing command-line interface. +The shared file opens `java.nio` for HBase's reflective `java.nio.Bits.unaligned()` +check during `HBasePlatformDependent` initialization, including on x86 Java 17. +It also opens `java.lang` for SOFA RPC's `ProviderInvoker` initialization, which +reflects on `Throwable.cause`. + +PD and Store use their own `bin/jvm-module.options` for the module permission needed +by Hessian serialization on Java 17. Their tests and cluster launchers use the same +component files. CI checks positive-test and startup logs for Java module-access +errors and unsupported JVM options, including errors printed without failing the +operation. + +### Launcher GC options + +Use `bin/start-hugegraph.sh -g g1` (or `-g G1`) to select G1 explicitly. +`-g zgc` and `-g ZGC` select ZGC; other values are rejected. Without `-g`, the +launcher preserves the JVM's collector choice or the collector set by the caller in +`JAVA_OPTIONS`. When `JAVA_OPTIONS` is empty, `-j "JVM_OPTIONS"` supplies the caller +options instead. + +For the default or G1 path, the launcher supplies `-XX:+ParallelRefProcEnabled`, +`-XX:InitiatingHeapOccupancyPercent=50` and `-XX:G1RSetUpdatingPauseTimePercent=5` +before the caller's options, so caller values override these tuning defaults. For +example, `bin/start-hugegraph.sh -g g1 -j "-XX:InitiatingHeapOccupancyPercent=45"` +uses G1 with an occupancy threshold of 45 when `JAVA_OPTIONS` is empty. + +Do not combine explicit `-g` selection with a different collector in `JAVA_OPTIONS` +or `-j`; the JVM rejects conflicting collector selections. + +### Launcher memory budget + +The default launcher heap uses half the smaller of host available memory and the +remaining budget under visible cgroup v1/v2 hard limits, with a 512 MiB minimum and a +32 GiB maximum. It refuses startup when the effective budget is below 512 MiB. +If a known hard limit has missing, unreadable or invalid usage, the budget is zero; +the launcher refuses startup instead of using host memory. For cgroup v1, numeric +unlimited values are recognized only when `uname -m` reports `x86_64`, `aarch64` or +`riscv64` and `getconf PAGESIZE` returns a positive integer. The threshold is the +signed 64-bit maximum rounded down to that page size. Recognized unlimited levels +are skipped so readable finite ancestors still apply; without a finite limit, +host sizing remains available. Unknown or masked machine names, or an unavailable +or invalid page size, keep numeric limits on the finite-limit path. + +Inactive file cache can contribute to this budget on a warm restart. For a finite +cgroup v2 limit, the launcher checks the immediate cgroup children of that reclaim +target. If any child's `memory.min` is nonzero, missing, invalid or unreadable, it +excludes cache credit at that limit: child cache may be protected from ancestor reclaim. +If the target directory cannot be enumerated, or a child cannot be searched, cache +credit is also excluded because child protection cannot be determined. Ordinary +searchable directories without cgroup controller files are skipped. +Unprotected children retain the existing warm-cache budget, including inherited and +nested hard limits. A cgroup's own `memory.min` does not block cache credit for its +own limit; another ancestor can still constrain it through child protection. +The cgroup v1 cache calculation is unchanged. + +This is a startup estimate, not a guarantee against runtime OOM. Native allocations +and concurrent processes also consume memory. Explicit heap options retain their +existing precedence; see [container heap sizing](../docker/README.md#server-heap-sizing). + +### Arthas diagnostics + +Server and Store bundle Arthas 4.1.5, managed by the root POM. PD does not embed +an Arthas agent. Diagnostic ports bind to `127.0.0.1` by default. Administrators +can explicitly configure another valid address, including wildcard `0.0.0.0` or +`::`; `localhost` is normalized to `127.0.0.1`. Wildcard listeners can accept both +IPv4 and IPv6 depending on the OS/JVM configuration; restrict both in firewall +rules. Server's `PUT /arthas` +requires authentication to be enabled and the caller to be an authenticated administrator. +Without authentication, the Server endpoint returns HTTP 403, including requests +from localhost or through a reverse proxy. Store has no equivalent administrator +authentication, so `GET /v1/arthasstart` always returns HTTP 403. + +For local diagnosis of either running service, use its distribution's bundled +helper as the same OS user that owns the JVM: + +```bash +bin/arthas.sh +``` + +The distribution prepares the agent in `lib/arthas` at build time. The helper +requires JDK 17 and uses `bin/pid`, without downloads, runtime extraction, a user +cache, service restarts or configuration edits. A new local agent listens on +`127.0.0.1:3658`, with HTTP, MCP and reverse tunnels disabled. Use `--pid` or +`--telnet-port` for another running instance, and `--command version` for a one-time +command. Interactive mode requires a terminal; for pipes, scripts or `docker exec` +without `-it`, use `--command` instead. Run `bin/arthas.sh --help` for usage. +When the helper exits, including after `--command`, `quit`, or INT/TERM/HUP, it +stops the agent and all its diagnostic sessions; the application JVM keeps running. +SIGKILL or a host failure cannot run this cleanup: stop any surviving agent manually. +For remote diagnosis, log in to the service host over SSH and run the same command, +or explicitly configure a reachable listener with appropriate authentication and +network access controls as described below. +Official Server (including HStore) and Store images include JDK 17, so the same +helper works inside the container, for example +`docker exec -it /hugegraph-store/bin/arthas.sh`. +A JRE-only installation needs JDK 17 for this helper. +If the Server API has already activated Arthas, connect to its existing telnet +port instead: `bin/arthas.sh --telnet-port 8562` with the default configuration. +Use `--target-ip` to select another bind address, for example +`bin/arthas.sh --target-ip 0.0.0.0` or +`bin/arthas.sh --target-ip ::1 --telnet-port 8562 --command version`. +The default is `127.0.0.1`; `localhost` maps to `127.0.0.1`. Wildcard addresses +`0.0.0.0` and `::` may accept both IPv4 and IPv6; the helper connects and cleans +up through `127.0.0.1` for either wildcard. Remote clients connect +to the service host's actual reachable address, not the wildcard address. +An already running agent retains its listener and authentication settings; +helper options do not reconfigure it. To switch to the helper's +HTTP-disabled defaults, run `bin/arthas.sh --telnet-port 8562 --command stop`, then +`bin/arthas.sh`; this stops only Arthas, and leaves the service JVM running. +Arthas can execute code inside the JVM. OS attach permissions control initial +activation; HugeGraph administrator authentication protects only the activation +API, not subsequent diagnostic connections. We recommend enabling Arthas native +authentication, especially when binding beyond loopback. However, native defaults +allow connections from `127.0.0.1` to skip authentication even with credentials; +other OS users or containers in the same Pod can still connect this way. To disable +this bypass, set `-Darthas.localConnectionNonAuth=false` on the service JVM and +restart it. Restrict host/Pod and port-forward access to trusted operators, and +limit firewall rules, port mappings and SSH forwarding accordingly. Authentication +does not encrypt telnet +or ordinary HTTP traffic; use a protected network or encrypted transport for +remote access. Avoid concurrent helper sessions because exiting one stops the +shared agent. A disabled-command list is optional and is not access control. + +Pass native Arthas credentials when starting or reusing an agent: + +```bash +bin/arthas.sh --target-ip 0.0.0.0 \ + --username diagnostics --password '' +``` + +Use single-line credentials; printable ASCII avoids the bundled Arthas 4.1.5 +telnet client's Unicode limitation. Password-only use defaults to username `arthas`. +Native JVM credentials also apply to helper-started agents: if the target JVM has +`-Darthas.username` or `-Darthas.password`, supply the effective credentials with +`--username` / `--password` for authenticated commands and cleanup, especially +when `-Darthas.localConnectionNonAuth=false` is set. The helper does not clear +native credentials when the corresponding option is omitted. +These credentials are separate from HugeGraph accounts. Batch commands and exit +cleanup use the supplied credentials automatically. For interactive diagnosis, +if prompted, enter `auth --username diagnostics ` in the Arthas +console. Existing agents require their existing credentials; passing new values +does not change them. Password arguments may appear in shell history and process +listings, so handle them on a trusted host. With `--username` alone, native Arthas +generates a password only when none is already configured in the target JVM's +native settings; otherwise it uses that inherited password. Supply `--password` for +authenticated batch commands and automatic cleanup. If cleanup fails, the helper +returns nonzero and the agent may remain running; use the configured or generated credentials with +native `auth`, then `stop`, to close it without stopping the service JVM. + +[Arthas native authentication](https://arthas.aliyun.com/en/doc/auth.html) remains +optional and follows upstream defaults: `arthas.localConnectionNonAuth=true` +allows connections from `127.0.0.1` to skip authentication. The helper uses a private +configuration, so editing `lib/arthas/arthas.properties` does not configure it. +If required, the target JVM property `-Darthas.localConnectionNonAuth=false` is +honored after restarting the service JVM. Arthas 4.1.5 generates a password when +binding `0.0.0.0` or `::` without an effective native password and prints it in the target +JVM output and Arthas log; explicitly supplying credentials is more convenient +for remote diagnosis and authenticated cleanup. + +Authenticated Server administrators can continue using `PUT /arthas` without an +additional enable switch or restart. Its HTTP/telnet interfaces use the configured +bind address; HugeGraph disables the optional MCP endpoint. The activation response +still contains the same four Arthas configuration keys. For API activation with +native credentials, add `-Darthas.username=diagnostics` and +`-Darthas.password=` to the Server JVM startup options and restart +Server before activation. These are native JVM properties, not new REST +configuration keys, and also apply to agents started by the local helper as described +above. They may expose secrets in process listings. Without native +credential settings, a specific non-loopback bind address does not require login; wildcard +binds use the generated password described above. API activation alone +leaves the agent running until an operator sends `stop`; connecting through the +helper applies the helper's exit cleanup to that existing agent too. + +Before reusing an API-started agent, leave `session` and `stop` out of +`arthas.disabled_commands` (or its legacy alias). `session` verifies the target +PID; `stop` closes Arthas without stopping the service JVM. Existing configurations +remain accepted, but failed PID verification or listener cleanup makes the helper +print an error and exit nonzero; the agent may remain running. To recover from an +agent with `stop` disabled, remove that restriction and restart the service before +activating Arthas again. Newly attached local agents keep both commands available. + +No commands are disabled by default. Administrators can optionally set +`arthas.disabled_commands=jad,mc` to restrict selected commands for API activation; +new local agents do not inherit this list. + +The existing `arthas.ip`, `arthas.httpPort`, `arthas.telnetPort` and +`arthas.disabledCommands` options configure the Server API-activated agent. +The distribution uses `arthas.telnet_port`, `arthas.http_port` and +`arthas.disabled_commands`. These snake_case keys take precedence over the legacy +camelCase aliases when both are present, including explicit default values. +Conflicting values produce a warning naming both keys and the effective key when +the agent is activated. Remove the unused alias to avoid ambiguity. Store's legacy +configuration fields are retained; the local helper does not read them. + +For a synchronous one-time command, POST `{"action":"exec","command":"version"}` +to the agent's `/api` endpoint without a `sessionId`. Check `state=SUCCEEDED`, the +`body.results` version and command status, and `body.jobStatus=TERMINATED`; HTTP 200 +alone also occurs for failed commands. Arthas 4.1.5 includes the upstream +[terminal-process interrupt fix](https://github.com/alibaba/arthas/pull/3079) and +[foreground-job cleanup fix](https://github.com/alibaba/arthas/pull/3094), so cleanup +can remove a session whose foreground command has already terminated. +It also includes the [MCP-disabled HTTP handler fix](https://github.com/alibaba/arthas/pull/3127). + +Clients that explicitly call `init_session` must call `close_session` when finished. +After closing, `session_info` for that ID reports `state=FAILED` with `session not found`. +[`ArthasApiTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java) +covers one-time execution, explicit session execution and removal. + +### Serializer configuration + +Use the Gremlin configuration files shipped with the upgraded Server. Serializer classes +move from `org.apache.tinkerpop.gremlin.driver.ser` to +`org.apache.tinkerpop.gremlin.util.ser`. The supplied remote configurations include the +corresponding HugeGraph registries. + +The Java Driver sample `gremlin-driver-settings.yaml` and Gremlin Console's +`remote.yaml` now default to GraphBinary V1. The serializer entry changes as follows: + +```diff +- className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0 ++ className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1 +``` + +Keep the HugeGraph builder and registry in both client configurations: + +```yaml +serializer: + className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1 + config: + serializeResultToString: false + builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder + ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] +``` + +GraphBinary preserves graph elements and UUID IDs for Java Driver accessors such as +`Result.getVertex()` and `Result.get(UUID.class)`. Untyped GraphSON V1 decodes these +results as maps and strings. + +`remote-objects.yaml` serves the Cypher HTTP JSON path and continues to use +`org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1` with +`HugeGraphIoRegistry`. It retains its duplicate registry entry as a TinkerPop driver +compatibility workaround. + +For the server, keep the GraphBinary builder and registry. The serializer order is +GraphBinary, untyped GraphSON V1/V2/V3, then typed GraphSON V2/V3. The GraphSON entries +keep the HugeGraph registry. The first entries look like this: + +```yaml +serializers: + - className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1 + config: + serializeResultToString: false + builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder + ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] + - className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1 + config: + serializeResultToString: false + ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry, + org.apache.hugegraph.io.HugeGraphSONV1MessageIoRegistry] +``` + +The full ordering is in `gremlin-server.yaml`. `application/json` selects untyped V1 in +the supplied server configuration variants. + +Typed GraphSON is available for the supported simple values and IDs, not as a +replacement for untyped serialization of every HugeGraph schema or graph object. Typed +V1 is not registered: its legacy `@class` deserialization can construct Java objects +before request authentication. V2/V3 use `@type`/`@value`. + +GraphBinary uses `HugeGraphTypeSerializerRegistryBuilder` to write HugeGraph IDs as +their underlying values. Schema results use standard maps and Blob values use standard +binary values (`ByteBuffer` in the Java Driver), including when nested in returned +vertices, edges or collections. `Optional` results carry their value or `null` when +empty, including results from `graph.variables().get()`. `File` results retain the map +shape `{"file": "name"}` using the file name, including inside nested results. Untyped +GraphSON V1 keeps the legacy Tree array of `key`/`value` entries and accepts scalar as +well as element keys through the additional `HugeGraphSONV1MessageIoRegistry` in that +serializer's configuration. Keep this registry limited to untyped V1 messages. Standard +graph-file IO and typed GraphSON serializers keep `HugeGraphIoRegistry` alone and use +the TinkerPop Tree format for their negotiated version. + +HugeGraph enums such as `DataType.TEXT` and `Directions.OUT` return their names over +GraphBinary, including in maps and lists. `graph.schema()` returns a map with +`propertykeys`, `vertexlabels`, `edgelabels` and `indexlabels` lists, using the same +schema conversion. TinkerPop enums retain their native wire types. Backend `Shard` +values, including `graph.metadata(HugeType.VERTEX, 'splits', size)`, return standard +maps with `start`, `end` and an exact 64-bit `length`, including inside lists and maps. +The default Driver does not need a private Shard serializer. + +Local edge ID filters accept both serialized strings and native edge IDs in mixed +collections: `hasId(not(without(ids)))` and `hasId(within(ids))` agree on membership and +count. Numeric and string vertex IDs keep their distinct types. + +Groovy interpolated strings (`GString`, including subclasses) return as standard strings +over GraphBinary, including inside lists and maps. For example, `def value = 42; +"value:${value}"` returns `"value:42"`. The untyped GraphSON path retains the +legacy GString bean map with its `values` and `strings` fields. + +Cypher extension predicates use the TinkerPop predicate constructor through the +translator extension point. Computed-expression regex filters retain whole-string +matching (`String.matches`), so `"marko"` matches `"mar.*"` and does not match `"ark"`. +The public `cypherRegex`, `cypherIsString`, `cypherIsNode` and `cypherIsRelationship` +factories imported by the bundled Gremlin configuration use that same adapter, so the +Gremlin translation returned by `EXPLAIN` can be replayed. + +Cypher results normalize HugeGraph IDs, including values nested in collections, maps and +paths. Cyclic results and nesting deeper than 32 levels are rejected. + +The default script engine is Gremlin Groovy. Validate application scripts against the +full TinkerPop 3.5.1 to 3.8.1 upgrade interval before deploying. + +## YAML configuration safety + +Jackson JSON and YAML modules share the root BOM version. Fabric8 kubeconfig extensions +can contain integer and decimal values; the K8s configuration regression verifies auto- +configuration without contacting a cluster. + +The TinkerPop 3.8.1 runtime uses SnakeYAML 2.2, Spring Boot 2.5.15 and Spring Framework +5.3.27. The Boot patch keeps PD and Store's YAML startup loader compatible with +SnakeYAML 2.x. These versions belong to the upgraded runtime; do not apply the SnakeYAML +override to a TinkerPop 3.5.1 deployment. The runtime also manages Jackson YAML 2.15.2 +so Fabric8 can read kubeconfig with SnakeYAML 2.2. Store's Log4j SLF4J binding and CLI +logging use 2.18.0; Store Node retains its Boot-managed Log4j core version. + +Quota templates and PD-delivered Store configuration accept standard YAML maps, lists +and scalars. Custom Java object tags such as `!!com.example.Type` are rejected before +object construction. Keep these files as configuration data; Java class names used as +ordinary string values remain supported. + +## List property compatibility + +The public TinkerPop `Graph.Features` contract changes: +`features().vertex().properties().supportsUniformListValues()` and +`features().edge().properties().supportsUniformListValues()` now return `false` instead +of `true`. Clients and compatibility tests that inspect these flags may stop enabling +uniform-list operations. The corresponding graph-variable flag remains `true`. + +HugeGraph still supports vertex and edge properties whose schema declares `LIST` +cardinality with `valueList()` and a supported element type, for example +`schema.propertyKey("tags").asText().valueList().create()`. Values must match the +schema's element type. This schema capability does not provide arbitrary Java `List` +values for `SINGLE` properties. The feature-reporting change itself does not remove +stored lists or require rewriting existing list data. + +When upgrading, review client feature-detection branches. Applications using HugeGraph +list properties should retain their explicit element type and `LIST` schema, and verify +list writes and reads with that schema. Generic TinkerPop clients should honor the +reported feature flags; do not treat a skipped uniform-list compatibility test as +validation of HugeGraph's schema list path. + +## Query semantics + +### First query after startup + +Server initializes the condition-query serializer on its trusted startup thread before +REST and Gremlin accept requests. A first label-filtered Gremlin query must succeed +without a preceding REST graph query, and subsequent REST queries must remain usable. +Gremlin scripts still cannot read restricted system properties such as `java.version` +or disable the security manager. + +### Property-query index coverage + +For type-compatible values, a global property-index query such as +`g.V().has("name", "alice")` or `g.E().has("weight", 0.5)` requires compatible +indexes on every visible candidate label. A candidate declares all queried non-OLAP +properties; even a label with no records requires coverage. Base edge labels have no +direct records; their indexes can cover sub-labels. An explicit sub-label query +also uses a compatible query-visible parent index when no compatible direct index +exists, while still filtering out sibling sub-labels. Shared OLAP indexes follow the graph read +mode. + +| Candidate state | Query behavior | +| --- | --- | +| Every visible candidate has compatible indexes | The query combines the indexed results. Composite and joint index plans may serve different labels in the same query. | +| A visible candidate lacks a compatible index and the values pass the property-type check | The query fails with `NoIndexException`, including when other candidates are hidden or deleting. | +| Every candidate is excluded by visibility | The query returns an empty result, including after those labels' indexes have been removed. | + +When query-visible candidates remain, index matching precedes the property-type +check. If at least one index matches but a value fails that check, the query returns +an empty result before complete coverage is checked. If no index matches, it still +raises `NoIndexException`, even for a type-mismatched value. + +Normal queries exclude hidden labels and labels in `DELETING` or `UNDELETED` +status. Internal requests that enable `showHidden` or `showDeleting` include the +corresponding labels and still require their indexes. Visibility follows the outer +request when a query is split into subqueries. Each branch checks index state and +complete coverage before opening its backend iterators. If branch construction +fails, the query closes cursors from both the unfinished branch and earlier +branches, preserving the original error. Joint index planning closes unselected +cursors and transfers the selected cursor to the result. This does not provide a +snapshot against concurrent schema changes. + +To fix incomplete coverage, specify an indexed label, for example +`g.V().hasLabel("person").has("name", "alice")`, or add indexes compatible with the +query predicates to every candidate label. Explicit-ID lookups and adjacency +filtering do not introduce a global property-index coverage requirement. + +### Predicate and value compatibility + +Version-specific handling retains negated predicates and filtering barriers where +pushdown would change their meaning. For UUID IDs, local negated filters such as +`hasId(P.not(P.neq(uuid)))` retain the same matches as ID equality. This also applies +when an ID filter stays local beside a property filter that cannot be pushed down. The +complete `HasStep` remains local; string and numeric ID comparisons keep their existing +semantics. `P.typeOf()` runs as a local filter for system and schema properties, +including inside negated or connective predicates. Its type operands are preserved +without schema value conversion. Missing properties do not match, including when the +type predicate is negated. Type filters do not use backend indexes on their own. The new +TinkerPop step APIs are adapted without discarding property metadata. + +Named `GValue` predicates retain their bindings when reused or cloned, and +`updateVariable()` applies when the predicate builds subsequent traversals. Explicit +named `limit()`/`range()` bounds have the same count behavior as literal bounds. Local +String ID comparisons retain their leaf semantics inside `not`, `and` and `or` +predicates. Mixed ID collections retain the typed matches of strings, numbers and UUIDs +when a filter stays local; all-string collections keep standard string-ID comparison +behavior. Fractional numeric IDs are not truncated to integer IDs. + +Count filtering is only truncated when every predicate branch has a supported bound that +preserves the filter result. Unsupported negated collection or custom predicate branches +keep the complete count, including inside `and` or `or` conditions. + +DATE schema properties accept TinkerPop `OffsetDateTime` values for writes and indexed +equality or range queries. Values normalize by instant to the existing `java.util.Date` +millisecond representation; offsets do not change the stored instant, and sub- +millisecond precision is truncated. Existing DATE data needs no migration. + +### Counts and transaction visibility + +Counts made inside a transaction include that transaction's uncommitted vertex and edge +changes. Only `count()` supports this fallback; other aggregates with uncommitted +changes are rejected. Queries that combine uncommitted changes with pagination, a limit, +or an offset remain unsupported. + +Queries and counts select the current transaction version before applying filters. When +an element is removed and re-added with the same ID, the new element shadows the old +updated record, even if only the old record matches the query's label. + +A count query that enters an index query path remains unsupported when that index +transaction has uncommitted changes. It reports `Can't do index query when there are +changes in transaction`. This includes indexed-property conditions and label-only +queries on backends that need a label index. Graph-wide and explicit-ID counts can still +use transaction merging without entering that index path. + +Unsupported text predicates remain traversal filters. For ordinary GraphStep and +VertexStep extraction, if any condition in a `HasStep` cannot be converted, the whole +step remains local, including its sibling label, ID and property conditions. Count +optimization preserves that filter step. Existing special handling around `match()` and +connective label filters remains part of traversal optimization. + +A self-loop contributes two occurrences to a vertex's `bothE()` traversal and one to +each of `outE()`, `inE()` and the graph-wide `E()` traversal. Counts retain these +multiplicities before and after transaction commit, including edge updates. + +Count optimization preserves steps that can filter candidates. Resetting an optimized +count traversal permits it to execute again. Query-step equality does not depend on the +result iterator from a previous execution. + +A scan in the middle of a traversal is counted once per incoming traverser, including +its bulk. It is not replaced by a single backend count. For example, with three +vertices, `g.V().V().count()` returns `9L`, while `g.V().count()` returns `3L` and +remains eligible for count optimization. + +### Read-only application checks + +Run these against an existing graph, replacing the label, keys and values with ones +present in that graph. The queries do not write graph or schema data. Use property keys +with the query indexes required by your backend. The `age` example assumes an integer +property; the path example needs an existing outgoing edge. + +```groovy +g.V().hasLabel('person').has('name', 'marko').count() +g.V().hasLabel('person').has('age', P.gt(0)).order().by('age').range(0, 2).values('age').toList() +g.V().has('name', 'marko').limit(1).out().limit(1).path().toList() +g.V().has('name', 'marko').limit(1).id().toList() +``` + +The first result is a numeric count. The second is a list of at most two numeric values +in ascending order. The third is a list of paths; untyped GraphSON represents each path +with `labels` and `objects`. The fourth is a list of graph IDs encoded as scalar values +by GraphSON or GraphBinary; the scalar type follows the graph's ID type. + +For Cypher, send this read-only query to `POST +/graphspaces/{graphspace}/graphs/{graph}/cypher`, substituting existing graph data: + +```cypher +MATCH (n:person) WHERE n.name = 'marko' RETURN id(n) AS nodeId +``` + +The response has rows under `result.data`; each row is a map and `nodeId` is a JSON +scalar, not a HugeGraph internal ID class. The Cypher API test covers this shape, nested +IDs and paths. + +## HTTP request errors + +The supplied WebSocket/HTTP channelizer releases each HTTP request buffer once, +including when a traversal alias is unknown. Such a request returns one HTTP 400, and a +later valid request on the same keep-alive connection receives its own response. This +avoids TinkerPop 3.8.1's duplicate release and second HTTP 500, which could be mistaken +for the next request's response by a connection pool. The correction applies when the +configured channelizer is TinkerPop's standard `WsAndHttpChannelizer`; HugeGraph selects +its compatibility implementation at startup and retains TinkerPop's WebSocket handling. +Custom channelizer classes keep their configured behavior. Unknown aliases still require +the caller to use a graph or traversal-source name exposed by the server. + +## Cypher HTTP contracts + +### Request forms + +The endpoint is `/graphspaces/{graphspace}/graphs/{graph}/cypher`. + +| Method and content type | Request | Behavior | +| --- | --- | --- | +| GET | `?cypher=` | Query-string form works | +| POST `application/json` | Raw Cypher text | Legacy raw-body form works | +| POST `text/plain` | Any body | Rejected with HTTP 415 | +| POST `application/json` | JSON object below | Separate bindings work | + +```json +{ + "cypher": "MATCH (n:person) WHERE n.name = $name RETURN n.name", + "parameters": {"name": "marko"} +} +``` + +POST bodies containing JSON arrays, strings, numbers, booleans, or null are rejected +with HTTP 400. Malformed JSON objects and trailing tokens are also request errors and do +not fall back to raw Cypher. + +For the JSON-object form, `cypher` must be a nonblank string and `parameters`, when +present, must be an object. Omitting `parameters` means an empty map. Missing bindings +produce an execution error; an explicit null value is accepted. Bindings may be named +`id` and `label`. There is a fixed limit of 16 top-level parameters. This Cypher +processor does not support a `maxParameters` configuration setting. Values containing +quotes and newlines remain bound data rather than changing query text. + +A Map counts as one top-level parameter. Group related values into one Map and reference +its fields explicitly; the vertex label and required properties must match the graph's +existing schema: + +```json +{ + "cypher": "CREATE (n:person {name:$props.name, age:$props.age, city:$props.city}) RETURN n.name", + "parameters": {"props": {"name": "new-person", "age": 20, "city": "Beijing"}} +} +``` + +Do not use the shorthand `CREATE (n:person $props)`: the pinned translator does not +assign the bound Map's properties in that path. The parameter limit counts top-level +bindings, not the entries inside a bound Map or a returned Map. A Map containing 17 +entries can be bound through one parameter with explicit property references; 17 top-level +bindings are rejected. + +The translator's reserved null-marker string `" cypher.null"` (two leading spaces) is +rejected in binding values, including nested maps and lists, to avoid silently +converting a user string to null. Actual JSON null remains accepted. The same marker in +query literals or stored properties is an existing translator limitation and is outside +this compatibility target. Check the HTTP status and body `status.code`: execution +failures retain HTTP 200 with `status.code` 400 and null result data. + +Traversal iteration failures, including Java errors, attempt rollback on the execution +thread and return a terminal failure response. A rollback exception is attached to the +original failure. + +### Compatibility coverage + +The API fixture uses a strong schema with a `SECONDARY` index on `city` and a `RANGE` +index on `age`. The regression suite covers: + +| Area | Covered behavior | +|---|---| +| Reads | Label scans, equality and range predicates, computed regex whole-string matching, boolean combinations, directed one- and two-hop patterns, empty results. | +| Results | Aliases, scalar and node values, nested maps/lists, relationship IDs, path shape, null and missing-property semantics. | +| Aggregation and pagination | `DISTINCT`, `count`/`sum`/`min`/`max`/`avg`, `ORDER BY`, `SKIP`, and `LIMIT`. | +| Parameters | String, number, boolean, null, empty and missing bindings; quote/newline preservation. | +| Writes | Vertex and edge `CREATE`, property `SET`, edge and vertex `DELETE`, with native REST readback. | +| Failures and routing | Invalid syntax, request shape, binding keys, schema values, authentication, and graph routing. | +| Failed writes | A rejected multi-vertex create leaves no residue in later Cypher and native reads. | + +Write regressions check persisted state through native REST reads. Applications are not +required to issue native reads after each successful Cypher write. + +### Scope and regression sources + +Advanced constructs including `MERGE`, `OPTIONAL MATCH`, `WITH`, `UNWIND`, `UNION`, and +variable-length paths are not established by the compatibility coverage described here. +HStore-specific behavior, Bolt, cross-request transactions, external client versions and +performance also require separate validation. This guide does not claim full openCypher +or Neo4j compatibility. + +The pinned translator is `org.opencypher.gremlin:translation:1.0.4`. Its translation +behavior defines the limitations documented above. + +Long-term regression sources: + +- [`CypherApiTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java) + covers request forms, schema-aware reads and writes, result shapes, + authentication and graph routing, and failed-write isolation. +- [`CypherClientTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java) + covers result normalization and HTTP client behavior. +- [`CypherOpProcessorTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessorTest.java) + covers binding validation and execution failure handling. + +## Transaction and graph ownership + +Server releases the current thread's graph transactions when a REST request finishes and +when an authenticated context task exits. Cleanup visits every registered graph even +when one graph fails to close, and reports failures. Cleanup explicitly rolls back +unfinished writes, including when `onClose(COMMIT)` was configured, then releases +backend transactions and resets thread-local transaction behavior and listeners. +Applications must commit successful writes before leaving the request/task boundary. +Closing an OLTP traverser preserves the caller's transaction so that the caller can +still commit or roll it back. + +The cleanup also applies when schema caches have become cold. Deleting an auth project +or user must remove its associated access or belong edges while preserving unrelated +relationships. The special OLAP vertex path remains separate; negative schema IDs alone +do not identify edges that can be skipped. + +Shared schema and element caches retain their invalidation listeners until the graph +closes. Request cleanup releases backend leases while preserving those graph caches and +their schema identity. + +Task database workers release their owned task and graph transactions on the worker +thread. A failure while closing one owner must not skip cleanup of other owners or +server-info resources. Cleanup retains the first exception and attaches later cleanup +failures as suppressed exceptions. +For the local Standard scheduler, a deleted task disappears from public lookup after its persisted record is +removed, while its actual execution still counts toward drain until native cleanup finishes; late completion saves from +that owner do not recreate the removed record. + +Synchronous task cancellation preserves a transaction already open on its caller, +including uncommitted graph writes. Cancellation still runs the normal completion and +status callbacks, but their default cleanup does not close that existing transaction. +The scheduler closes transactions opened by a cancellation callback when it had no +transaction before starting, and a running task retains its worker owner until that +worker's own cleanup finishes. The caller remains responsible for committing or rolling +back its original transaction. + +Graph close attempts auth, scheduler and caller-transaction cleanup separately. If a +scheduler remains registered or the graph reports active transactions, the graph remains +open for a later close attempt. Pending local tasks retain their task database transaction +so they can persist completion before the next close attempt. An incomplete scheduler +drain does not queue transaction cleanup behind that scheduler's blocked worker. Stopping +distributed task dispatch is distinct from completing drain: a timeout does not make +the next close return success. + +During Server shutdown, the task manager waits for task workers to finish, then retries +closing graphs retained in its existing scheduler registry before stopping the task +database worker. This gives pending tasks time to persist completion and releases the +task database owner's transactions on that owner's thread. Each retained graph gets one +close attempt per shutdown call. A close failure remains visible and keeps the database +executor available for a later explicit retry; it does not start background recovery. +A terminated cron executor already satisfies the scheduler's dispatch barrier. Every +executor wait, including an explicit retry after a timeout, checks actual termination; +shutdown requested alone is insufficient. Store-close notifications finish before the +shared event executor is stopped. + +Once these logical checks pass, graph close releases shared cache listeners, invokes +`provider.close()` and destroys graph locks even if an earlier cleanup step reported an error. The error +remains visible to the caller. Logical graph closure does not prove that a failed +backend native-resource release succeeded; callers must inspect the reported failure +rather than infer physical release from graph state. + +Regression sources: + +- [`TransactionLifecycleTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/TransactionLifecycleTest.java) + exercises request and authenticated-task ownership and rollback. +- [`StandardTaskSchedulerTxTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/task/StandardTaskSchedulerTxTest.java) + exercises task worker transaction cleanup. +- [`HugeFactoryTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/HugeFactoryTest.java) + exercises graph close failure continuation, distributed drain retry and Server stop + with a task that outlives the early graph-close attempts. + +## Compatibility evidence and deployment checks + +Checked-in regression tests define a coverage boundary, not a supported matrix for every +external client, authentication method or backend deployment. + +| Path | Regression coverage | Deployment checks still required | +|---|---|---| +| GraphSON | Serializer configuration, MIME selection and wire fixtures. | External client versions, network and authentication combinations. | +| GraphBinary | Shipped Driver configuration, typed results and in-process ID, predicate and element round trips. | External client, network and authentication combinations. | +| Cypher HTTP | Local API harness, result IDs and paths, Basic-auth test client. | Bearer authentication and external REST clients. | + +[`GremlinConfigCompatibilityTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java) +contains serializer and configuration regressions. Use the Cypher sources linked above +for API behavior. Keep runtime dependency versions aligned with the root POM and shipped +configuration rather than copying old configuration fragments into a different runtime. + +Structure and Process suites exercise TinkerPop behavior on Memory, RocksDB and HStore. +HStore has separate standard-process and feature-test selections. +[`HugeGraphProviderLifecycleTest`](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderLifecycleTest.java) +checks backend cleanup and graph reopening. The test selections and setup are +maintained in the [test POM](../hugegraph-server/hugegraph-test/pom.xml), +[Server CI](../.github/workflows/server-ci.yml) and +[PD/Store CI](../.github/workflows/pd-store-ci.yml). + +Inspect executed tests for the backend and protocols used by the deployment. Selected +Structure or Process suites must execute tests; an all-skipped report is not +compatibility evidence. Run the read-only application examples against representative +existing data and validate the serializers, client versions and authentication +combinations actually used by the application. + +## Cache RPC graph lifecycle + +When cache RPC is enabled and a backend declares shared storage, Server registers both +graph and schema cache notifiers for graphs loaded during startup and graphs created or +loaded later. A new graph's RPC registration must succeed before its local publication. +Graph services use the graph space and graph name together as their service identity. +PD deletion and startup coordinate RPC removal for the same graph instance. Startup does not +register an owner whose RPC removal is pending, even after its placement-event token is consumed, +and checks logical closure under the actual owner's RPC lifecycle monitor before registration. +Any retained provider configuration must be unregistered before startup can export services; +a cleanup failure fails startup instead of re-exporting that owner. Graph clearing, task drains +and full graph close remain outside this RPC publication gate. + +Internal graph loading registers RPC services in Server's administrative scope and +restores the caller's identity afterward. Direct registration and cleanup remain +administrative operations. + +Successful graph close releases that graph's notifier listeners, providers and consumer +proxies. A failed task drain during ordinary close or non-clearing local removal retains +the owner and its RPC resources. In PD-enabled deployments, RPC cleanup failures during +graph DELETE retain the local owner and bindings for retry; partial cleanup does not imply +a healthy service. This DELETE path first closes task admission and drains local executions, +then cleans up RPC before metadata deletion and backend clearing. A failed drain or RPC +cleanup can be retried with the same DELETE. Logical graph closure fences access before +all resource cleanup completes. Any close failure retains the local owner and bindings; +retrying the same DELETE retries cleanup without repeating backend clearing. A completed clear +is remembered for that exact graph instance even if an external idle transaction delays close; +retry after that transaction exits. A replacement graph with the same name is cleared independently. +A graph closed by a non-clearing removal has no successful-clear certificate. A later clearing DELETE +refuses that closed owner and keeps its binding, rather than reporting success while old backend data remains. +Raft provider, node and an owned listener are closed independently. The first fatal Java Error takes +priority over ordinary close failures; otherwise the first failure is retained. Other failures are +reported as suppressed, with a repeated reference to the primary failure ignored. +A graph using Server's shared Raft listener closes +only its own node. Server releases that listener and its address registration on shutdown; +deleting one graph keeps the listener available to other local graphs. An interrupted node +join retains the graph owner and interrupt status so a later DELETE can finish cleanup. +The context's backend, snapshot and optional read-index executors stop after node join and +drain queued work before close completes. Their wait uses `task.wait_timeout`; timeout or +interruption retains the graph for retry without cancelling queued operations. +RPC cleanup can start before graph close finishes. Clearing PD DELETE unregisters RPC before +metadata removal and backend clear. Factory, binding and quota removal require a successful graph close; +a failed close retains their owner even if its RPC services have already been unregistered. +One failing graph does not prevent other graphs in a PD removal batch from being processed. +Fatal Java Errors retain their original identity and +propagate out of the removal batch. Failures after backend clearing do not undo deletion; +retries do not restore deleted data or placement metadata. + +For local deletion without PD, backend clearing precedes RPC cleanup during graph close. +A close failure retains the same local graph, factory identity and configuration for DELETE retry, +even when access has already been fenced by logical closure. Only a successful clear by that +instance's local drop permits retry to skip clearing and finish its retained resource cleanup. +An ordinary close does not provide that certificate. A failure before clearing keeps the open +owner's existing RPC services. A later cleanup failure does not preserve or restore deleted data. + +Failed creation or local loading closes the candidate in Server's administrative scope. +If rollback close fails, Server retains the local owner and factory identity for cleanup retry. +Retained failed candidates are excluded from RPC startup even while they are still open. +An old graph's cleanup cannot remove or re-export a replacement graph's services. +A PD graph-add notification blocked by an existing owner is coalesced until that owner is +released, including when independent watches deliver the new ADD before the old REMOVE. +Duplicate ADD notifications alone keep the healthy owner. Server consumes the PD acknowledgement +of its own local graph creation without retaining a replacement ADD unless that exact owner already +has failed peer cleanup; the existing cleanup retry takes priority. A healthy creation acknowledgement +cannot republish the graph during a later reload. After removal, Server rereads the latest configuration; +a missing configuration creates nothing. Reload consumes pending ADDs only after each old owner is +successfully removed, then loads current metadata together instead of replaying ADDs during removal. +After a peer removal fails, a later same-name PD add retries non-clearing cleanup of that exact +owner before loading its replacement. Another cleanup failure keeps the owner for the next add +or normal reload; no background retry is scheduled. A replacement load failure is logged and +remains pending for a later add or normal reload. The completed old-owner DELETE still succeeds. +Events queued for a closed notifier are ignored; an RPC request already sent cannot be recalled. + +Cache RPC uses the configured peer addresses; local provider export does not establish +that every remote consumer connection is ready. Notifications are asynchronous, and +connection or delivery failures are logged. This lifecycle handling does not provide +acknowledged delivery or a distributed cache consistency protocol. Validate actual DML +and cache behavior against the deployment's shared backend and topology. + +## Deleting a graph with local tasks + +Stop application writes before deletion and keep them stopped during retries. The local +scheduler closes task admission and waits for actual executions, cancellation callbacks +and their transaction cleanup before data or metadata is removed. If draining times out, +the graph stays registered with its data and configuration, but task admission remains +closed: new asynchronous tasks are rejected even after the existing owners exit. Keep +application writes stopped and retry deletion after those owners exit. If no destructive +cleanup has begun, restart the Server to resume task admission instead of completing +the deletion. Closing the scheduler alone leaves unstarted persistent work in storage. + +This is a local shutdown guarantee. For a graph shared by several Servers, stop writes +and tasks on every Server before deletion; notification delivery is not a cluster-wide +barrier. Do not recreate the same graph name until deletion finishes. Use a restart for +configuration changes rather than hot reload, whose recovery is handled separately. +Deletion is not transactional: a failure after destructive cleanup needs operator +inspection, and retry does not restore removed data. An owner-cleanup failure must not +be treated as a successful drain; inspect it instead of force-removing the scheduler. + +## Local loading ownership + +Server keeps ordinary import ownership in memory on each graph instance. The first +owner changes NONE to LOADING; releasing one owner leaves LOADING while others are +active, and the last release restores NONE. Manual LOADING, RESTORING and MERGING +cannot be adopted. Original enum setters reject every request while owners are active, +including bare LOADING; their original behavior remains available without active owners. + +`GET /graphspaces/{space}/graphs/{graph}/mode/owners` returns +`{"protocol":1,"instance":"","epoch":0,"revision":0}`. This response is illustrative; +recovery must use the actual values returned by the latest GET. `PUT` on that path accepts +`{"action":"acquire","instance":"","epoch":0,"token":""}`; +`release` uses the same tuple. Receipts echo protocol/instance/epoch/token and return +`state` (`active` or `released`) and the effective `mode`. Both operations use existing +graph-owner/STATUS WRITE permissions; the authenticated principal owns the token. +Each live graph admits at most 1024 concurrent owners. At that limit, a new owner +receives HTTP 409 without changing state; existing-owner retries, release and +administrator recovery remain available. Releasing an owner frees a slot. +The request principal is also bound for graphs opened without the authentication proxy. +Each new acquire, actual release and successful recovery writes an INFO audit entry +with the graph name, graph instance, previous/resulting mode, epoch and owner counts. Idempotent +retries do not add entries. These audit entries omit tokens and authenticated principal values. + +Keep one tuple and identity for the whole import attempt, including uncertain acquire +outcomes. Release after that import's workers stop. Acquire/release retries are +idempotent. An unknown release cancels a late acquire; a released handle cannot be +reused. Terminal records are bounded to 1024 per epoch. Epoch advancement retains +active old handles and permanently retires all other old handles. An initial +acquire prepared before epoch advancement can therefore be rejected even if it +never became active. The client must not automatically refresh that retired handle: +a new token could undo a prior cancellation or administrator recovery. After a +terminal acquire rejection, stop and drain that import attempt before creating a +fresh Loader context to retry. + +This protection coordinates Loader processes connected directly to the same stable +Server and graph instance. It does not fence ordinary data requests across load +balancers, node failover, graph replacement or Server restart. There is no automatic +expiry: after kill -9 or permanent disconnection, stop the import and its in-flight +writes before retrying release with the same handle and identity. If that handle is +lost, an administrator can recover without that token: + +1. Stop every related import and drain its in-flight writes. Keep ordinary writers + stopped too; the owner protocol does not fence graph data requests. +2. Read the current instance, epoch and revision from the owners endpoint on the same Server. +3. Send `PUT` to that endpoint with + `{"action":"recover","instance":"","epoch":0,"revision":0,"confirm":"I'm sure all imports have stopped"}`. + +Recovery requires the authenticated global administrator role even when graph-space +permission checks are disabled; a proxied graph also enforces administrator permission. +With Server authentication disabled, the existing trusted anonymous administrator semantics apply. +Recovery also requires the exact current instance/epoch/revision and explicit confirmation. It clears owners, advances the epoch and returns NONE; +late acquire/release requests from the retired epoch cannot register an owner again. +The receipt contains the new epoch and `state: recovered`, without tokens. A stale +recovery request returns a conflict and cannot clear an import started in a newer +epoch. Owner registration/release and every successful enum mode write advance the +revision, including a write of the current mode. Either operation after inspection +therefore rejects the stale recovery request even within the same epoch. After an uncertain result, inspect current state rather than blindly +retrying with a newly fetched epoch/revision. This operation does not stop or drain imports itself. + +The same recovery procedure can reset manual RESTORING or MERGING after that work +has stopped and drained; restore completion keeps its historical RESTORING mode. +Before ordinary import on an owner-capable Server, explicitly recover or set NONE +only after inspecting the completed work and confirming there are no active imports. +Expected loading-owner and manual-mode conflicts return HTTP 409; invalid requests +return 400 and unauthorized recovery is rejected by the authentication layer. + +Loaders retain historical ordinary-import and restore behavior with Servers that do +not provide this API. Concurrent owner protection applies only when this API is +supported; RESTORING keeps its existing enum mode path. diff --git a/docs/shared-foundation-migration.md b/docs/shared-foundation-migration.md new file mode 100644 index 0000000000..b7221f5b9b --- /dev/null +++ b/docs/shared-foundation-migration.md @@ -0,0 +1,267 @@ +# Shared foundations: preparing for the 1.8.0 migration + +The planned 1.8.0 consolidation changes Java imports and extension contracts, and requires a coordinated Server, PD and Store upgrade. Use this guide to identify affected code, check historical-data compatibility and prepare your deployment. + +Shared implementations move out of Server core so Server and Store can use the same types and codecs. This guide covers the planned 1.8.0 migration. + +## What do I need to change? + +| If you maintain… | Your next step | +|------------------|----------------| +| A REST client, Loader integration or client DTO | Check actual affected Java calls; REST use alone does not require replacing client DTOs with struct types. | +| Java code importing Server IDs, schema, queries or indexes | Update the imports and affected signatures below, then recompile against matching 1.8.0 artifacts. | +| A `HugeGraph` implementation, serializer or element subclass | Adapt the API/SPI contracts below; old compiled implementations are not binary-compatible. | +| Custom Gremlin scripts or `classImports` | Change the `IdGenerator` class name and verify it in the packaged Gremlin service. | +| Hubble, Toolchain or Computer | Apply the downstream exception/classpath changes and validate the built jars. Use matching downstream artifacts and check their packaged classpaths. | +| An HStore installation | Check OLAP writes, Store-rebuilt indexes and metadata namespaces before a coordinated upgrade. | +| PD or Store code | Use the shared owners without importing Server core; keep service-specific execution and resource lifecycle local. | + +## Why share these implementations? + +Before this change, Server core and struct carry separate implementations of several IDs, schema types, queries, elements and codecs. A fix to one copy can leave the other with different behavior. Consolidating them gives those fixes one owner and gives Server and Store a common implementation to test. + +```text +Before: Core and Struct each maintain shared types and codecs. + +After: +Server core --> Struct --> Common +Store core --> Struct +PD service -------------> Common +``` + +*Shared-foundation ownership, rather than the complete Maven dependency tree.* Server still needs PD/Store clients for HStore; Store still needs its network, PD client and storage libraries. Struct retains its shared index/analyzer and codec dependencies, including shaded Kryo where required. + +| Capability | Owner after migration | What remains with the caller | +|------------|-----------------------|------------------------------| +| IDs, schema metadata and type codes | struct | Import and use the shared types | +| Queries, base elements, byte/property codecs | struct | Adapt graph-engine and backend behavior | +| Index construction, analyzers and OLAP selection | struct | Supply schema candidates and result sinks; preserve configured analysis and index results | +| Transactions, traversal, tasks and schema mutation | Server core | Operate on shared metadata and elements | +| PD-backed schema access | Store | Manage `SchemaGraph`/`SchemaDriver`, listeners and cache lifecycle | +| JWT signing/verification and auth constants | common | Supply configuration and translate failures into service responses | +| RPC client/server configuration interfaces | common | Keep existing implementations and configuration values | + +`HugeGraphSupplier` lets shared code access schema, configuration and the clock without depending on the graph engine. Core and Store implement it. + +Core elements wrap shared base elements: property mutation, cloning, removal, expiration and loading state use that same state. `BaseVertex` owns adjacency; the engine caches edge wrappers by identity over the same base edges. Core serializers retain their engine/backend adapters and delegate shared encoding. Core also retains transaction writes, backend capability checks and index-update orchestration around shared index construction and OLAP selection. + +## Migrate Java code and extensions + +### Start with the imports + +For example, code that creates an ID changes its imports while keeping the factory call: + +**Before** + +```java +import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.backend.id.IdGenerator; + +Id id = IdGenerator.of("alice"); +``` + +**After** + +```java +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; + +Id id = IdGenerator.of("alice"); +``` + +| Previous entry | Shared entry or migration | +|----------------|---------------------------| +| Shared types in `org.apache.hugegraph.backend.id` | `org.apache.hugegraph.id.*` | +| `org.apache.hugegraph.schema.*` metadata | `org.apache.hugegraph.struct.schema.*` | +| Shared types in `org.apache.hugegraph.backend.query` | `org.apache.hugegraph.query.*` | +| `org.apache.hugegraph.backend.query.serializer.*` | `org.apache.hugegraph.query.serializer.*` | +| `org.apache.hugegraph.backend.store.Shard` | `org.apache.hugegraph.backend.Shard` | +| `org.apache.hugegraph.backend.store.BackendEntry.BackendColumn` | `org.apache.hugegraph.backend.BackendColumn` | +| `org.apache.hugegraph.structure.HugeIndex` | `org.apache.hugegraph.structure.Index` | +| Shared bytes/encoding in backend serializers | `org.apache.hugegraph.serializer.*` | +| Core `HugeException` | `org.apache.hugegraph.exception.HugeException` | +| `org.apache.hugegraph.SchemaGraph` / `SchemaDriver` | `org.apache.hugegraph.store.schema.*` | + +Keep the core imports for `SnowflakeIdGenerator` and the query execution helpers +`QueryResults`, `ConditionQueryFlatten`, `EdgesQueryIterator`, `QueryBatch` and `QueryResultContext`. +The table applies to shared types, not entire packages. `SchemaManager`, schema mutation builders and backend-specific serializers remain in core. There is no general compatibility package for removed core classes. Relocated types also change method descriptors that expose IDs, schema, queries and indexes, so update implementations and call sites and recompile every affected integration against matching artifacts. Changing source imports does not make old binaries compatible. + +### Resolve conditions through the shared query type + +Server and Store now use `org.apache.hugegraph.query.ConditionQuery` from struct. +The explicit resolution methods operate on top-level `EQ` and `IN` relations +for a key and intersect their candidate values. Nested conditions and other +operators remain outside this resolution. + +| Method | Result | +|--------|--------| +| `containsCondition(HugeKeys)` | Reports whether any top-level relation exists for the system key. | +| `containsConditionValues(key)` | Reports whether a top-level `EQ` or `IN` relation exists, including an empty `IN`. | +| `conditionValues(key)` | Returns the candidate intersection. Pair it with `containsConditionValues` to distinguish absence from an empty intersection. | +| `conditionValue(key)` | Returns `null` for an empty intersection, the value for a singleton, and throws for multiple candidates. | +| `singleConditionValueOrNull(key)` | Returns the value for a singleton and `null` for every other candidate count. | + +The existing `condition(key)` method keeps its legacy behavior, including +returning the original list for a sole `IN` relation. Use strict +`conditionValue` when a serializer requires one value; use +`singleConditionValueOrNull` when an optimization applies only to one candidate. +For example, `IN [1, 2]` combined with `EQ 2` resolves to `2`, while an +additional `EQ 3` produces an empty intersection. + +SEARCH index construction and local SEARCH matching share the analyzer and +term handling in struct's `IndexBuilder`. Engine callers use +`HugeGraph.searchPredicate(text)` so the graph selects its configured analyzer +and the authorization proxy checks access before delegation. + +### Check API and SPI implementations + +| Integration point | Required change or preserved contract | +|-------------------|---------------------------------------| +| `HugeGraph.sameAs` | Change `sameAs(HugeGraph)` to `sameAs(HugeGraphSupplier)`. External overrides must accept the supplier, as `StandardHugeGraph` and `HugeGraphAuthProxy` now do. There is no old-signature overload. | +| Custom `HugeElement` subclasses | Provide shared `BaseElement` state through `element()` and adapt `BaseProperty` through `wrapProperty`. Removed protected engine state fields and a second adjacency store must not be restored. | +| `GraphSerializer.writeIndex` / `readIndex` | Accept/return `org.apache.hugegraph.structure.Index`; update SPI implementations and callers. No long-lived `HugeIndex` wrapper is retained. Hash-ID generation belongs to shared `Index`. | +| `RpcServiceConfig4Client` / `RpcServiceConfig4Server` | Both now belong to common, with the same `org.apache.hugegraph.rpc` names and signatures. Existing RPC implementations and values need no source migration. Duplicate core/commons-rpc definitions are removed without adding a dependency edge. | +| `TokenGenerator` | Use common's `org.apache.hugegraph.auth.TokenGenerator`; supply the signing secret and translate JWT failures locally. Common's JWT dependencies are optional, so direct JWT consumers must declare their required JWT libraries. | +| Engine vertices and edges | `HugeVertex` and `HugeEdge` remain engine adapters; Store uses base elements. | + +### Update downstream exceptions without changing their meaning + +Distinct downstream exception contracts need distinct class names on the shared classpath. Use these replacements with the matching Toolchain artifacts: + +| Previous downstream entry | Replacement and contract | +|---------------------------|--------------------------| +| Hubble `org.apache.hugegraph.exception.HugeException` | `org.apache.hugegraph.exception.HubbleException`: still a `RuntimeException` with its string constructor | +| Client `org.apache.hugegraph.exception.NotSupportException` | `org.apache.hugegraph.exception.ClientNotSupportException`: still a `ClientException`, with existing message/cause and message/arguments constructors | +| Hubble's copy of `org.apache.hugegraph.license.MachineInfo` | Keep the same import; use the implementation supplied by `hugegraph-common` | + +Update imports, constructor calls and handlers for these exceptions. Struct's `HugeException` and `NotSupportException` have different contracts and are not substitutes for those catches. Computer's `HgkvDirImpl` must also use `ClientNotSupportException`. + +Rebuild the affected Toolchain and Computer integrations and check their packaged classpaths before deployment. A source scan for duplicate names cannot rule out stale or transitive classes in built jars. Loader's REST boundary and independent client DTOs do not need wholesale conversion to struct. + +### Check configuration-only class names + +Replace `org.apache.hugegraph.backend.id.IdGenerator` with `org.apache.hugegraph.id.IdGenerator` in Gremlin `classImports` and scripts. The bundled default and Raft examples use the shared class. These strings are not checked by Java compilation, so test them in the packaged Gremlin service. + +## Understand data and runtime compatibility + +The migration preserves existing type codes, ID/property bytes, ordinary Server index keys, TTL formats, query semantics and configuration defaults. It introduces no general data-rewrite operation. The following cases need separate attention: + +| Area | Behavior after migration | Action or limit | +|------|--------------------------|-----------------| +| String ID comparison | Uses Java UTF-16 string order even before an ID loaded from UTF-8 bytes is decoded | Persisted bytes stay the same; supplementary Unicode characters no longer compare differently after lazy decoding. | +| Property values | Preserves schema-driven persisted rows and tagged Store shuffle/aggregation values | Readers choose the mode from the known producer/request plan, not by guessing from bytes. | +| SYSTEM-label index expiry | Preserves Store's stable key and 13-byte value envelope for positive expiry, and core's existing SYSTEM-label bytes | The bounded legacy envelope is read at the serializer boundary; no key TTL suffix or prefix-deletion change. | +| Store-rebuilt long-text indexes | Uses the complete value and hash expected by Server queries | Assess old Store rebuilds; shortened historical rows may require rebuilding from graph data. | +| HStore OLAP physical keys | New writes use `[property ID][vertex ID]` and readers retain a matching legacy-row fallback | Upgrade writers together. Previously overwritten properties cannot be recovered. | +| Schema map decoding | Retains ordered primary/sort keys and resolves endpoint metadata independent of map field order | Matching or legacy endpoint forms work; conflicting or malformed metadata is rejected. | +| Metadata namespace | Store uses `pd.cluster` (default `hg`) across schema, configuration, watches and TTL-cleaner metadata | Match the Server/graph namespace and preserve backend graph names. | +| Historical serialized Java names | Query/Kryo readers retain required legacy names and aliases | Type relocation does not authorize a new wire format. | + +### Store-rebuilt indexes + +The old Store `IndexBuilder` shortened long text to 20 characters, while Server queries used the complete value and its hash. The shared builder adopts the Server contract. Frozen producer/query fixtures describe that existing mismatch in the [compatibility README](../hugegraph-struct/src/test/resources/compatibility/2f827d6e8/README.md#existing-indexbuilderquery-mismatch). + +Old shortened rows remain decodable, but do not automatically match a query using the full value. Check whether your installation used Store-side index rebuilding and rebuild affected indexes from graph data where necessary. The migration neither rebuilds them automatically nor recovers missing historical entries. Ordinary Server-created index keys keep their existing contract. + +### HStore OLAP writes + +A vertex-only physical key allowed one OLAP property to overwrite another on the same vertex. The new `[property ID][vertex ID]` key lets those properties coexist. This is a behavior repair beyond package relocation; existing row values remain readable without rewriting them. + +Readers try the requested property's compound key first, then the legacy vertex-only key if its value contains the matching property ID. Deleting one property removes its compound row and a matching legacy row while preserving other properties. Values already overwritten by the old writer are lost. + +Clearing an OLAP property deletes its matching compound and legacy rows while +retaining its schema. Removing the property deletes those rows before the existing +schema/index removal job completes. Both operations retain the shared OLAP table, +other properties and other graphs. Cleanup scans the shared table and commits +bounded batches; if a batch fails, the task fails and the pending session is rolled +back. Deletions already committed to Store nodes can remain, so retrying cleanup +is idempotent; the operation does not promise graph-wide atomicity. + +Quiesce writes to the affected OLAP property before clearing or removing it, and +keep those writes stopped until cleanup completes. The scan and bounded delete +batches do not coordinate concurrent writers: a new row can be missed, and a +rewrite of a scanned row can be deleted. An inconsistent compound key/value +blocks cleanup until the affected row is repaired; the error includes its key +in hexadecimal, limited to the first 64 bytes. + +Mixed-version writes can produce stale reads: an old writer may update a legacy row while a new reader prefers an earlier compound row. Upgrade all Server and Store writers before resuming writes. + +### Match metadata namespaces + +In Store `application.yml`, the existing namespace setting is: + +```yaml +pd: + cluster: hg +``` + +The environment equivalent is `PD_CLUSTER`. Match Server's `cluster` when `usePD=true`. +With `usePD=false`, the first HStore graph opened binds the process-wide `MetaManager` to its `pd.cluster`; +a later graph's explicit conflicting value is logged and ignored. Configure all HStore graphs in the +process with the same namespace. Store rejects reuse of its schema driver under a conflicting namespace. + +Keep backend graph-name components such as `DEFAULT/hugegraph/g` (`graphspace/store/table`). The REST identity `DEFAULT-hugegraph` is not a metadata key. + +## Plan a coordinated upgrade + +```text +Prepare --> Adapt Java code --> Upgrade matching services --> Verify + +HStore OLAP keys: +Before: vertex ID +After: [property ID][vertex ID] +``` + +*Use matching Server, PD and Store artifacts; mixed-version rolling upgrades are outside this migration's supported upgrade contract.* + +1. **Prepare integrations.** Update Java APIs/SPIs and Gremlin class names, recompile affected callers and check downstream delivery. Assess Store-rebuilt long-text indexes and OLAP usage. +2. **Back up data and configuration.** Use the existing deployment procedure. Record metadata namespaces and backend graph names so they remain consistent. +3. **Stop writes and upgrade together.** Install the matching Server, PD and Store release artifacts. Upgrade all OLAP writers before allowing writes again. +4. **Verify the built deployment.** Start the packaged services, check Gremlin imports, authentication, schema access and relevant RocksDB/HStore paths. Validate historical data and the built classpath; compilation alone is not sufficient. +5. **Rebuild affected indexes and resume writes.** Where old Store rebuilds produced shortened text keys, rebuild from graph data as needed and verify full-value queries. Check OLAP properties and metadata routing before resuming. + +Validate the packaged deployment and affected integrations before resuming writes, using the compatibility checks below. + +## Compatibility reference for implementers + +### Codecs and schema + +Persisted property rows and transient tagged results share the scalar codec. Both must retain their historical samples. Ordinary schema index writers keep the Server key/name-TTL layout. `Index.hasTtl()` follows the engine's existing system-index contract; Store's expiry envelope stays at the serializer boundary. Unsupported index-only element reconstruction remains unsupported. + +Shared `Shard` preserves its GraphSON tag and fields, and query decoding retains its legacy fully qualified name. Toolchain's `org.apache.hugegraph.structure.graph.Shard` stays a separate REST DTO. `BackendColumn` retains mutable `name`/`value` byte arrays. Its hash now uses array contents consistently with equality instead of core's array-identity hash. Historical Kryo names require decoder aliases and fixture checks; the fields retain their bytes. Store's `RocksDBSession.BackendColumn` stays a backend adapter. + +Schema maps preserve wire field names and ID identity. Two decoder fixes accompany relocation: primary-key and edge sort-key lists retain order and duplicates rather than passing through a set, and redundant edge endpoints are resolved consistently regardless of map field order. Matching `links` and source/target fields are accepted; legacy source/target-only maps remain readable; conflicting or malformed endpoints are rejected. Ordered-schema and endpoint-map tests cover generated vertex/edge IDs and query values, but do not establish all distributed or packaged release gates. + +Query JSON still uses Java names for some relation values. Decoder mappings must read historical names, and historical values still require Kryo decoding where applicable. Replacing these Java names with value tags would require a separate wire-format compatibility policy. + +### System-vertex classification + +`BaseVertex.TypeContext` makes the existing engine/storage distinction explicit: + +| Context | `~variables` | `~server` / `~role_data` | `~task` / `~taskresult` | +|---------|--------------|------------------------|------------------------| +| `STORAGE` (standalone storage elements) | Task data | Ordinary vertex data | Task data | +| `ENGINE` (engine wrappers) | Ordinary vertex data | Server data | Task data | + +Engine wrappers must select `ENGINE`; standalone storage elements retain `STORAGE`. Selecting the correct context preserves existing table routing and readers for system vertices. + +### Review and release checks + +Review behavior alongside the package moves. The key entrypoints and invariants are: + +| Area | Main entrypoints | What to verify | +|------|------------------|----------------| +| Java API/SPI | `HugeGraphSupplier`, `HugeGraph.sameAs`, `GraphSerializer`, custom `HugeElement` | Caller recompilation and the documented migration | +| Element state | `BaseElement/BaseVertex/BaseEdge`, `HugeElement/HugeVertex/HugeEdge`, offheap properties | One state owner, callback order, clone/adjacency identity and virtual value reads | +| Historical codecs | `BytesBuffer`, `BinaryElementSerializer`, `BinarySerializer`, `LegacyClassNames`, `KryoUtil` | Producer-specific bytes and old enum ordinals, with canonical writers preserved | +| Queries, schema and indexes | `ConditionQuery`, `EdgeLabel.fromMap`, `IndexBuilder`, `Index` | Query behavior, ordered keys, endpoints, full-value hashes and TTL contracts | +| HStore and metadata | `HstoreTables`, `OlapStage`, `SchemaDriver`, `FilterIterator` | OLAP repair, exact owner reads, batch bounds, namespaces and watch closure | +| Delivery | Shared-foundation/inventory guards, packaged jars, Gremlin imports, downstream callers | Resolved/shipped dependencies, startup and paired documentation | + +Release evidence must include shared tests and frozen pre-migration byte/JSON fixtures for IDs, schema, properties, indexes, pagination, TTL/OLAP and supported Kryo data. It must also cover RocksDB/HStore execution, Store filtering and schema access, authentication, element state propagation and affected downstream tests. + +Check resolved dependencies and built distributions: PD/Store must not depend on Server core, struct must not depend on PD client/core, and packaged jars must not contain duplicate HugeGraph classes. Start the packaged services without a classpath-ordering workaround. Independent review and CI guards must prevent new duplicate owners or forbidden dependencies. + +Build/test commands and service prerequisites are in [BUILDING](BUILDING.md), module guidance and CI. Ensure tests actually run; `-DskipTests` is not compatibility evidence. + +Deploy matching Server, PD, Store and downstream artifacts. Validate both the Java integrations and the packaged services before adopting the migration. diff --git a/docs/storage-lifecycle.md b/docs/storage-lifecycle.md new file mode 100644 index 0000000000..4f4eed5b0d --- /dev/null +++ b/docs/storage-lifecycle.md @@ -0,0 +1,625 @@ +# Storage lifecycle, recovery and runtime compatibility + +This guide covers Store RPC ownership and shutdown coordination, standalone RocksDB +snapshot recovery, and storage runtime upgrades. Use it when changing request cleanup, +deploying storage components, recovering an interrupted restore, or upgrading JNI. + +Store RPC lifecycle and standalone RocksDB recovery are separate contracts. The +standalone recovery protocol does not provide HStore multi-partition recovery or an +atomic restore across all databases in a graph. + +## Guide map + +- [RPC and Scan ownership](#rpc-and-scan-ownership): final batches, half-close, + cancellation, iterator cleanup and callback dispatch. +- [Shutdown coordination](#shutdown-coordination): admission, worker drain, TTL cleanup + and [Store node shutdown](#store-node-shutdown). +- [Standalone snapshot recovery](#standalone-snapshot-recovery): pending records, + checkpoint identity, WAL paths and safe retry. +- [Storage provider selection and ToplingDB](#storage-provider-selection-and-toplingdb): + JNI preparation, provider configuration and persistent paths. +- [Runtime compatibility and upgrades](#runtime-compatibility-and-upgrades): backup, + JNI/JRaft compatibility, the compatibility runner and monitoring changes. + +## RPC and Scan ownership + +### Half-close, feedback and cancellation + +Request half-close is separate from transport cancellation. +An aggregate query finishes work already supported by feedback credit after half-close; +a later transport cancellation or deadline still cancels its workers. Graph partition +scans deliver their permitted pages and final batch, or return an explicit error when +half-close leaves insufficient feedback credit. Batch-scan RPCs accept one initial +query. + +### Client response and iterator ownership + +Response parsing runs outside the client state lock. Accepted final batches remain +visible across response completion; errors and iterator close discard unpublished data. +Early query close cancels the transport without waiting for a blocked request send. +Normal half-close stays serialized with sends. Closing a composite iterator closes all +started children once. A closed multi-partition iterator is terminal: later probes do +not reopen another partition, including after a child cleanup failure. + +Accepting a response refreshes the watchdog before parsing starts. Completion preserves +an accepted batch while it is being parsed; cancellation and errors can discard it. +The state lock protects these transitions rather than response extraction. + +### Server worker and cleanup ownership + +Each scan/query owns its workers and iterators until cleanup finishes. Iterator cleanup +failures remain visible, including automatic native iterator close and empty-partition +selection. Final aggregate success is sent only after plan and iterator cleanup +succeeds. + +Cancellation signals the currently owned reader before waiting for its iterator lock. +Publishing, interrupting and clearing that thread use the same ownership lock so a +reused executor thread is not mistaken for the cancelled reader. Half-close does not +interrupt the reader. Queued cancelled tasks still execute their cleanup/accounting +paths, and iterator close waits for an active read to leave the iterator. + +Batch-scan request state is published under its manager lock, while reader notification, +worker scheduling and iterator cleanup run outside that lock. A half-close or receipt +waiting for a slow read therefore does not block cancellation from interrupting the reader. + +Unary query and count requests share the query admission, worker and cleanup owner +registry. Count keeps partition work parallel on the service executor. Iterator cleanup +failure prevents successful completion and remains visible to request drain. + +One-shot scans run on the gRPC caller thread. Transport cancellation and Store shutdown +signal that caller directly, so interruptible waits can exit without waiting for a +queued call-executor callback. Interruption is cooperative: a RocksDB native operation +does not guarantee that it will return on Java thread interruption. The call remains +registered until its read and iterator cleanup finish; cancellation does not permit +another thread to close a native iterator that is still in use. + +### Server OLTP traversal batches + +Server OLTP traversal workers close each active batch iterator on completion, +interruption or failure, then release their thread's graph transactions. A capacity +stop also releases producers waiting on a full queue. Iterator cleanup errors remain +visible when the capacity stop ends traversal normally; a business failure retains +its cleanup errors as suppressed exceptions. Caller transactions remain with the +request thread. + +### Callback executor configuration + +The gRPC application callback queue is always unbounded (`Integer.MAX_VALUE`), matching +its existing default. `thread.pool.grpc.queue` remains accepted for configuration +compatibility but no longer limits this callback queue. Bounded callback dispatch can +reject cancellation or completion callbacks in gRPC 1.55.3, leaving request cleanup +registered after the transport has closed. `thread.pool.grpc.core` and `.max` still +configure the same executor; an unbounded queue normally keeps dispatch at the core +thread count. Bound expensive scan/query work and admission at their worker queues; +sustained overload can otherwise increase callback backlog and memory use. + +## Shutdown coordination + +RPC and Scan drain establishes completion of their request owners. It does not by itself +establish completion of unrelated background maintenance jobs. + +The RPC boundary stops admission to every Store RPC before cancelling active scans and +queries. It runs queued request cleanup and waits for terminal application callbacks +before Spring destroys their databases. This reuses the same per-call owners; it does +not create a second shutdown registry. Interrupted cleanup waits and native teardown +preserve interruption without abandoning these owners. + +The Store context-close listener stops RPC, scan and query admission, requests transport +shutdown, and drains scan/query workers before waiting for terminal gRPC callbacks and +scan cleanup. Queued cancelled scans still run their cleanup paths. + +Shutdown terminates unfinished aggregate responses with `UNAVAILABLE`, rather than +reporting normal completion for a truncated result. Terminal delivery is serialized +with batch delivery and does not release the request's cleanup ownership: shutdown +still waits for the workers and iterators even if the response callback fails. + +Transport termination alone is insufficient evidence that an application callback or +native iterator owner has finished. Preserve the application cleanup barrier when +changing the server framework, executor configuration or shutdown ordering. + +Drain waits retain ownership until cleanup finishes and restore the shutdown thread's +interrupted status. They must not close databases under a worker solely because a wait +was interrupted. A blocked owner can keep shutdown waiting; investigate that owner +rather than bypassing the database lifetime guard. + +The engine closes partition databases after their partition engines shut down, then +releases graph database and schema resources. Keep this ordering when modifying Raft or +database teardown. Request drain and database release have different responsibilities; +reuse their existing owners instead of adding a parallel registry. + +### Store node shutdown + +Spring coordinates Store shutdown: new RPCs are refused, active requests are cancelled, +and their application callbacks and scan workers are drained before database teardown. +The shutdown coordinator continues cancellation and independent cleanup even if one +callback fails. It reuses the existing per-call owners. + +TTL cleanup closes the scan before returning its borrowed RocksDB session lease. Both +releases are attempted when either fails. The first cleanup failure is retained, later +failures are attached, and completion is signalled after these release attempts. +Executor termination alone does not establish that native cleanup succeeded. + +Shutdown waits for TTL cleanup as well as ordinary request callbacks. Failed native +releases remain diagnostic drain blockers. The engine stops heartbeat producers and +joins partition Raft services before releasing native databases. Do not add a concurrent +JVM shutdown hook that closes those databases independently of this drain. + +Cancellation can fail in-flight writes. Before planned maintenance, stop new writes and +check outstanding write outcomes. + +For an unpacked distribution, invoke its normal stop script: + +```bash +bash bin/stop-hugegraph-store.sh +``` + +The script waits up to 30 seconds for process exit. A timeout returns a nonzero status +and retains `bin/pid` for diagnosis; it does not force-kill the process or remove data. +A stuck callback or failed iterator close can keep shutdown pending. Inspect Store logs +and a JVM thread dump to identify the remaining owner before retrying. Preserve the data, +WAL and Raft directories, and do not bypass the resource drain to make the process exit. + +## Standalone snapshot recovery + +### Installation and pending metadata + +Standalone RocksDB snapshot restore retains its source checkpoint until the replacement +is installed and reopened. Before changing data, it records the source checkpoint and +WAL location in a sibling `.resume-pending` file, together with the restore +operation and checkpoint generation. After an I/O failure or process interruption, a +subsequent HugeGraph open retries the interrupted installation before native recovery. +Missing sources, incomplete metadata or a changed WAL configuration stop opening instead +of replaying uncertain data. + +### Filesystem durability boundaries + +On Linux, recovery synchronizes the marker parent directory before replacing live data +and after removing the marker, before consuming the checkpoint. A directory-sync failure +stops that step and preserves the checkpoint; after installation has succeeded, an +unlink-sync failure leaves the restored live data intact. Other platforms retain their +existing filesystem durability behavior. This marker ordering does not guarantee that +every copied data or WAL file survives a power loss. + +### Publishing the pending record + +Recovery metadata publication requires same-directory hard-link support in the data +directory's parent filesystem. The complete temporary record is written and forced +before its final name is published without overwriting an existing marker. A +pre-publication I/O or unsupported-link failure leaves live data and the checkpoint +untouched; resolve the filesystem or permission fault, reopen normally and reissue +restore. Existing damaged or old-format markers are still preserved and rejected. + +### Preparing application work + +Quiesce graph requests, background tasks and open query iterators before starting a +restore. The recovery lease coordinates database opens and file installation; it does +not drain active application work. + +### Retrying an interrupted restore + +Preserve the checkpoint, pending marker and configured paths after a failure. Restore +access or free space, then retry normal startup with the same runtime and configuration. +Do not delete the pending marker to bypass the guard. Successful native reopening clears +it and then attempts checkpoint cleanup, preserving the existing consume-on-success +behavior. + +The normal retry sequence is: + +1. Preserve the original checkpoint, pending record, WAL location and mount layout. +2. Correct the access, space or filesystem fault without changing their identities. +3. Release the remaining Store sessions with normal close calls when the failed restore + has already closed the native handle. +4. Reopen with the same runtime and configuration so startup retries the recorded operation. +5. Allow successful native reopening to clear the record and perform checkpoint cleanup. + +If native close itself failed, use the failure handling below instead of attempting a +competing open. + +### Recovery ownership and native close failures + +A sibling `.resume-lock` serializes cooperating opens and restores. The OS +lock remains held through native close and reopen, and is released when the database +finally closes. The lock file remains on disk; its presence alone does not prove an +active owner. Do not delete it to force another opener through. Within one JVM, +physical-file ownership also covers path aliases. An unrelated database can still open +while another database is closing. A failed native or lock-descriptor close retains +recovery ownership and reports the failure; retrying close cannot bypass it. Preserve +the recovery files and stop the process before retrying startup after such a failure. +Older binaries and unrelated writers do not honor this protocol and must not access +these directories concurrently. + +### Mount layout and physical path identity + +Mount the parent data root, such as `rocksdb-data`, rather than an individual store +directory such as `data/g`. Java opening rejects a store that is itself a volume mount, +including same-filesystem bind mounts on Linux. Such aliases can hide the sibling +guards, and directory replacement cannot replace a mount point. This Java check is per +database: it does not promise a pre-JVM scan of every graph or that no sibling database +has initialized before another open fails. Keep mount layout unchanged throughout +startup and recovery. Checkpoint/live-data overlap and a checkpoint containing the WAL +directory are rejected by physical directory identity, including parent bind aliases; +separate path strings do not make overlapping trees safe. Recovery refuses the operation +if a required physical identity cannot be determined. + +### WAL replacement and recovery scope + +Independent WAL, WAL inside data, and data inside a WAL root use in-place log +replacement. Preserve WAL symlink configuration across retries. Local fault tests cover +interrupted operations and reopening; they do not establish power-cut durability. This +mechanism does not provide an atomic whole-graph restore or an HStore multi-partition +snapshot protocol. + +### Checkpoint generation and identity + +Each non-consuming restore creates a unique checkpoint directory. Pending recovery binds +the original directory and immutable SST filesystem identities, plus checkpoint metadata +and WAL contents, so replacing a checkpoint at the same path is rejected before data +replacement. Keep the original checkpoint in place: copying it elsewhere and back may +change its identity. Filesystems without file identities use content hashes instead. +Unrelated writers must not modify checkpoint files; this protocol does not protect +against malicious in-place changes to immutable SST files. + +### WAL staging and retained material + +WAL staging uses a sibling `.resume-staging-` directory recorded +by the pending operation. Retired WAL remains there until successful native reopening; +repeated attempts reuse that same directory. Cleanup removes only the current +operation's directory, never all matching prefixes. Unknown staging directories and +markers from older formats remain preserved for diagnosis rather than being guessed or +silently upgraded. + +### Store-level retry + +After a Store-level restore failure closes its native owner, reopen the database to +retry the recorded pending operation. Calling restore again on the already closed Store +does not reopen it. Recovery retries the recorded unique checkpoint; it does not create +a new non-consuming copy. + +### Non-consuming restore and temporary-copy cleanup + +Non-consuming Store restore first creates all temporary checkpoints, then restores each +database. If either phase fails, it attempts to remove only the exact temporary copies +created by that invocation, including an unfinished checkpoint created before its path +could be returned. Cleanup requires the same database recovery ownership and a confirmed +absent pending marker. Any existing or unreadable marker preserves the copy and emits a +diagnostic; lock contention also preserves it. Original user checkpoints and unrelated +temporary directories are never part of this cleanup. This does not collect historical +orphan directories or copies left by a process killed before writing its pending marker. +Restore remains non-atomic across databases. + +### Metadata integrity and runtime changes + +Pending records include a SHA-256 checksum over all metadata property names and values, +excluding the checksum itself. Recovery verifies it before reconstructing any WAL alias, +then uses the filesystem's actual canonical WAL path to validate the destination. This +detects incomplete or changed metadata; it is not authentication against someone who can +edit the record and recompute its checksum, and it does not freeze the surrounding +filesystem. Records from older versions without this checksum are rejected and +preserved, not silently upgraded. Finish an outstanding restore with its producing +version before upgrading; otherwise preserve the checkpoint, marker and paths for +diagnosis. Do not add a checksum manually to bypass this check. + +### Pending recovery blocks new checkpoint copies + +Before creating a non-consuming copy, restore holds the database recovery ownership and +requires a confirmed absent pending marker. An existing or unreadable marker rejects the +new copy before opening its checkpoint source; reopen the database to retry the recorded +recovery instead. Repeated requests therefore do not allocate more UUID copies for an +already pending database. If a later database is pending, copies already created for +earlier databases are still cleaned by the same invocation's failure handling. + +### Database directory aliases + +The database directory itself must not be a symbolic link, including a dangling link; +put a stable alias on its parent directory instead. This keeps the pending marker and +recovery lock at the same physical database identity while its contents are replaced. +Parent-directory symbolic links and bind mounts remain supported. Data and WAL aliases +of the same physical directory are treated as one directory, so checkpoint WAL records +are not deleted as a separate source. + +### WAL symbolic links + +Simple WAL symbolic links remain supported, including an external stable link followed +by `..`. Recovery rejects a WAL path combining `..` with a link inside the data +directory, or a symbolic-link target chain containing another link inside data: data +replacement would delete an unrecorded part of that path. Reconfigure such layouts to +use a stable external WAL directory before restoring. Rejection happens before writing +new pending metadata or deleting data. If marker existence cannot be determined, opening +fails with the recovery material preserved rather than bypassing a possible pending +restore. + +## Runtime compatibility and upgrades + +The standard runtime uses RocksDB JNI 8.10.2. Validate existing data before upgrading +the storage runtime. Keep a restorable backup made by the old runtime; do not +assume that data opened or modified by 8.10.2 can be reopened by an older runtime. +Rollback requires restoring the backup with the matching old application and +dependencies, rather than replacing the JAR in an upgraded data directory. + +All components use JRaft 1.3.14. Older JRaft versions call compressed-cache methods removed from RocksDB 8.x, preventing +Raft storage initialization, including Server when `raft.mode=true`. Treat the JNI and +JRaft changes as one service upgrade and validate existing Raft logs with the complete +new package. + +Use the complete matching application package for an upgrade or backup-based rollback. +Validate service-created data and Raft logs separately from the synthetic JNI check. A +successful JNI compatibility run does not establish that a cluster rolling upgrade or +downgrade is safe. + +### RocksDB compatibility runner + +The permanent [compatibility +test](../hugegraph-server/hugegraph-test/src/test/rocksdb-compatibility/run.sh) uses +Maven effective POMs to compare the actual Server, PD and Store RocksDB versions. CI +compares PR base/head or push before/after, fails if a version cannot be resolved, and +runs only changed version pairs, deduplicated across components. POM or JRaft changes +alone do not run the JNI test. No historical version matrix is maintained. + +Run from two complete source checkouts with Java 11+, Maven and Python 3: + +```bash +bash hugegraph-server/hugegraph-test/src/test/rocksdb-compatibility/run.sh \ + /path/to/before /path/to/after /tmp/new-rocksdb-compatibility-run +``` + +Each pair creates synthetic SST and synchronous WAL data with the old JNI, +reads/modifies/deletes/adds data with the new JNI, then verifies it in a separate JVM. +Original data, commands, JAR hashes, native versions and failure evidence are retained +in the new output directory. This test never opens business data. It does not run at application startup or enter a +production binary distribution. When changing this test or its CI, +verify both changed/unchanged version selection and run the changed version pairs. +Service-created data, Raft logs, cluster upgrades and Topling acceptance remain separate +checks. + +The runner requires a new output directory and refuses environment overrides that inject +Java options or native libraries. Before running, unset `JAVA_TOOL_OPTIONS`, +`JDK_JAVA_OPTIONS`, `_JAVA_OPTIONS`, `LD_PRELOAD` and `DYLD_INSERT_LIBRARIES`. Keep each +run's synthetic data and evidence separate; the script does not reuse a previous output +directory. + +### Monitoring compatibility + +The JNI upgrade removes registration of these unavailable ticker constants: + +- `BLOCK_CACHE_INDEX_BYTES_EVICT`, `BLOCK_CACHE_FILTER_BYTES_EVICT` +- `NO_FILE_CLOSES`, `RATE_LIMIT_DELAY_MILLIS`, `NO_ITERATORS` +- `NUMBER_FILTERED_DELETES` +- `BLOCK_CACHE_COMPRESSED_MISS`, `BLOCK_CACHE_COMPRESSED_HIT`, + `BLOCK_CACHE_COMPRESSED_ADD`, `BLOCK_CACHE_COMPRESSED_ADD_FAILURES` +- `WRITE_TIMEDOUT` + +It also removes these histogram constants: + +- `STALL_L0_SLOWDOWN_COUNT`, `STALL_MEMTABLE_COMPACTION_COUNT`, + `STALL_L0_NUM_FILES_COUNT` +- `HARD_RATE_LIMIT_DELAY_COUNT`, `SOFT_RATE_LIMIT_DELAY_COUNT` + +Store meter names use `rocks.stats.` plus the lowercase constant name. For histograms, +the removed meters include `.max`, `.mean`, `.min`, `.summary` (with its quantile tags), +`.summary.sum` and `.summary.count`. Exporters may normalize names further. Update +dashboards and alerts that reference these series; their disappearance is not a zero +reading. No replacement series or semantic equivalence is asserted here. See +[RocksDBMetricsConst](../hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/metrics/RocksDBMetricsConst.java) +and [meter +registration](../hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/metrics/RocksDBMetrics.java). + +## Storage provider selection and ToplingDB + +HugeGraph defaults to standard RocksDB. Server, PD and Store can explicitly select a +prepared ToplingDB runtime using their normal distributions. Prepare and configure each +component separately; this guide covers the shipped launch scripts, not custom IDE or +embedded launchers. + +| Component | Business provider setting | Start command | +| --- | --- | --- | +| Server with the RocksDB backend | `rocksdb.provider=topling` in each graph properties file | `bin/start-hugegraph.sh` | +| PD | `rocksdb.provider: topling` in `conf/application.yml` | `bin/start-hugegraph-pd.sh` | +| Store | `rocksdb.provider: topling` in `conf/application-pd.yml` | `bin/start-hugegraph-store.sh` | + +For a distributed graph, Server uses `backend=hstore`; PD and Store own its persistent +storage. A standalone Server keeps `backend=rocksdb`. The launcher selection and the +business provider must agree. A mismatch fails before opening that component's database. + +### Standalone Server setup + +![Switch RocksDB to ToplingDB](images/switch-rocksdb-to-toplingdb.png) + +Use a stopped Server distribution on Linux x86_64 with a Java 17 JDK. If your +distribution lacks these scripts, [build it from this source](#build-and-prepare) first. +Prepare the runtime once, select it, then initialize and start the Server. The graph +APIs and normal start/stop commands stay the same; Topling's native options are +configured in `conf/toplingdb.yaml`. Use new, empty, service-writable data and WAL +directories outside the distribution for this example. + +**1. Prepare the runtime.** Supply your ToplingDB JNI JAR and its trusted SHA-256: + +```bash +export TOPLING_JNI_JAR=/absolute/path/to/rocksdbjni-topling.jar +export TOPLING_JNI_SHA256='' +bash bin/prepare-topling.sh +``` + +**2. Select Topling.** Edit `conf/graphs/hugegraph.properties` and every other graph loaded from the configured graphs directory: + +```properties +backend=rocksdb +rocksdb.provider=topling +rocksdb.data_path=/srv/hugegraph/topling/server/data +rocksdb.wal_path=/srv/hugegraph/topling/server/wal +``` + +Create those external directories with permissions for the service account. The paths +above are examples; choose persistent locations for your deployment. + +**3. Initialize and start.** From the same shell, select the matching JNI runtime and initialize the new store once: + +```bash +export TOPLINGDB_ROCKSDB_PROVIDER=topling +bash bin/init-store.sh +bash bin/start-hugegraph.sh +``` + +Keep `TOPLINGDB_ROCKSDB_PROVIDER=topling` in the service environment for subsequent +starts; initialization is only needed for a new store. For PD and Store, prepare each +component separately, then use its YAML provider setting and start command from the +table above. A Server connected to distributed storage keeps `backend=hstore`; PD and +Store select their own storage runtime. + +### Build and prepare + +Build the normal distributions from the repository root using Java 17 and Maven 3.6.3 or +later: + +```bash +mvn clean package -Dmaven.test.skip=true -Dmaven.javadoc.skip=true +``` + +Obtain a trusted ToplingDB JNI JAR and its SHA-256 through your approved artifact +channel. The runtime is not downloaded by the launcher. It must implement the +`TOPLINGDB_EASY_MIGRATE_CONF` configuration entry point. Preparation requires a Java 17 +JDK, Linux x86_64, `sha256sum`, `unzip`, `od`, `ldd` and GNU `mv`. Native dependencies depend on the selected JNI JAR; builds requiring glibc 2.38 or +newer and libaio need those libraries on the deployment host. Preparation rejects +unresolved dependencies. + +In each stopped, unpacked component directory: + +```bash +export TOPLING_JNI_JAR=/absolute/path/to/rocksdbjni-topling.jar +export TOPLING_JNI_SHA256='' +bash bin/prepare-topling.sh +``` + +The script verifies the copied JAR's hash, the Topling Java marker, one Linux x86_64 JNI +entry, the ELF architecture and native dependencies. Before installation, it opens and +closes a disposable database with the selected JNI and checks that a probe configuration +changes the persisted write-buffer size. A marker or matching hash alone is +insufficient: builds without the EasyMigrate configuration entry point are rejected. The +probe validates the configuration entry point, not existing-data compatibility or every +setting in your production profile. Its checksum receipt binds the probe to the +installed JAR and native library. Launchers reject missing or stale receipts; prepare a +fresh distribution when upgrading from an older preparation script. It installs +`topling/rocksdbjni.jar`, its matching native library and optional web resources outside +the standard `lib` directory. On Ubuntu with libaio's t64 name, the compatibility +symlink stays inside that component's runtime directory. The published runtime +directories use 0755 and regular files use 0644, so a different service account can read +the prepared JNI. That account also needs traversal permission on the component's parent +directories; preparation does not change those parents. + +Preparation refuses an existing `topling` directory. To replace a runtime, stop the +service and prepare a fresh distribution; it never changes database files. + +### Select and start + +Set the provider in the component's business configuration. For PD and Store, edit the +existing `rocksdb` mapping, retaining its other options: + +```yaml +rocksdb: + provider: topling +``` + +For a standalone Server, edit every selected graph configuration: + +```properties +backend=rocksdb +rocksdb.provider=topling +``` + +Use separate, initially empty data directories for validation. This integration does not +convert or certify an existing database for a different provider. + +For persistent use, create service-writable directories outside source checkouts, build +outputs and unpacked distributions, and configure all applicable paths: + +| Component | Configuration file | Setting | Example external path | +| --- | --- | --- | --- | +| Server data | Graph properties | `rocksdb.data_path` | `/srv/hugegraph/topling/server/data` | +| Server WAL | Graph properties | `rocksdb.wal_path` | `/srv/hugegraph/topling/server/wal` | +| PD metadata | `conf/application.yml` | `pd.data-path` | `/srv/hugegraph/topling/pd` | +| Store data | `conf/application.yml` | `app.data-path` | `/srv/hugegraph/topling/store/data` | +| Store Raft | `conf/application.yml` | `app.raft-path` | `/srv/hugegraph/topling/store/raft` | + +Set both Server data and WAL paths. Retaining the default distribution-relative WAL path +can lose that directory when rebuilding or replacing the distribution. Preserve the +external data, WAL and Raft roots across runtime updates. Start each component with +explicit runtime selection: + +```bash +export TOPLINGDB_ROCKSDB_PROVIDER=topling +# Run the start command for this component from the table above. +``` + +The component uses its own `conf/toplingdb.yaml`. An explicit +`TOPLINGDB_EASY_MIGRATE_CONF=/absolute/path/config.yaml` overrides that native +configuration. The shipped profile disables the native HTTP server and keeps +`memtable_as_log_index=false`, which is required by the Java write path. Tune the native +profile for your machine before production use; it does not replace the component's +business configuration. + +Server's `init-store.sh` and `dump-store.sh` use the same explicit runtime selection. PD +and Store put the prepared JNI JAR before their Spring Boot dependencies. Standard +launch remains the default and does not require Topling files or preparation tools. +Launch rejects inherited recognizable RocksDB JNI preloads in default/standard mode and +competing JNI in Topling mode. Unrelated allocator or tracing preloads remain intact; +switching from a previously selected Topling runtime removes only that runtime. Store +adds verified jemalloc without replacing or duplicating caller preloads; the selected +JNI stays first. Do not preload a second RocksDB JNI alongside the selected runtime. + +### Validate and stop + +Before adopting a JAR, validate each component's real writes, reads, normal stop and +restart using a dedicated database directory. Confirm both the Java classes and the +loaded JNI library come from the prepared runtime; a standard-JNI unit test or a +successful startup alone is insufficient. For distributed storage, validate PD and Store +together, then perform a Server `hstore` graph operation. + +At request/task completion, explicitly committed writes are preserved; unfinished writes +are rolled back and cached backend leases are released. Shared schema and element caches +retain their invalidation listeners until the graph closes. + +Stop incoming work and use the component's normal stop script. PD drains its scheduled +metadata work, joins Raft and closes the metadata database and native options. Do not +use a forced kill as evidence of normal resource cleanup. + +To return to standard RocksDB, stop the component, unset `TOPLINGDB_ROCKSDB_PROVIDER` +and set its business provider back to `rocksdb`. Use the corresponding standard-provider +data directory; do not test a provider switch by reusing the other provider's database. + +## Implementation navigation + +Use the current implementation and its tests when reviewing a lifetime change. These +entry points separate client response ownership, server worker ownership, callback +admission and standalone recovery installation. + +### RPC and shutdown entry points + +- [CommonKvStreamObserver](../hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/CommonKvStreamObserver.java) + owns client stream state and response delivery. +- [KvBatchScanner](../hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/KvBatchScanner.java) + sends batch scan feedback and distinguishes early cancellation from half-close. +- [ScanResponseObserver](../hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/scan/ScanResponseObserver.java) + coordinates scan readers, cancellation and iterator cleanup. +- [GrpcShutdownBarrier](../hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/GrpcShutdownBarrier.java) + guards admission and waits for terminal application callbacks. +- [ContextClosedListener](../hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/ContextClosedListener.java) + orders RPC shutdown, worker drain and callback completion. +- [GRpcServerConfig](../hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/GRpcServerConfig.java) + configures application callback dispatch. +- [HgStoreEngine](../hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/HgStoreEngine.java) + orders partition shutdown and database release. + +### Recovery and compatibility entry points + +- [RocksDBSnapshotRestore](../hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSnapshotRestore.java) + implements pending recovery and filesystem installation. +- [RocksDBSessions](../hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSessions.java) + manages database sessions and recovery ownership. +- [Snapshot restore tests](../hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSnapshotRestoreTest.java) + exercise restore failures, path identities and retry behavior. +- [Compatibility runner](../hugegraph-server/hugegraph-test/src/test/rocksdb-compatibility/run.sh) + selects changed JNI version pairs and runs isolated synthetic data checks. +- [CI guidance](ci.md) describes how to select relevant checks for a change. + +Keep operational contracts here and deployment topology in the [Store operations +guide](../hugegraph-store/docs/operations-guide.md) and [deployment +guide](../hugegraph-store/docs/deployment-guide.md). diff --git a/helm/hugegraph/.helmignore b/helm/hugegraph/.helmignore new file mode 100644 index 0000000000..e37a0cdfb0 --- /dev/null +++ b/helm/hugegraph/.helmignore @@ -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. + +# Patterns to ignore when building packages. +.DS_Store +.git/ +.gitignore +*.swp +*.bak +*.tmp +*.orig +*~ +.idea/ +.vscode/ + +# Contributor tooling, if present in a working tree. Never ship it. +scripts/ +testdata/ +/tests/ + +# Source documentation diagrams are not runtime chart files. Helm stores these +# files in the release Secret; binary assets can exceed Kubernetes' 1 MiB limit. +/images/ + +# Never package a chart archive inside a chart. +*.tgz diff --git a/helm/hugegraph/Chart.yaml b/helm/hugegraph/Chart.yaml new file mode 100644 index 0000000000..16397760b1 --- /dev/null +++ b/helm/hugegraph/Chart.yaml @@ -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. +# + +apiVersion: v2 +name: hugegraph-helm +description: Helm chart for Apache HugeGraph HStore cluster (PD + Store + Server) +type: application +version: 0.2.0 +# TODO: set appVersion to "1.8.0" when the release images are published. +appVersion: "latest" +kubeVersion: ">=1.23.0-0" +keywords: + - hugegraph + - graph + - hstore + - raft +home: https://hugegraph.apache.org/ +icon: https://hugegraph.apache.org/img/logo.svg +sources: + - https://github.com/apache/hugegraph +maintainers: + - name: HugeGraph Community + url: https://hugegraph.apache.org/ + email: dev@hugegraph.apache.org diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md new file mode 100644 index 0000000000..5f12ba6435 --- /dev/null +++ b/helm/hugegraph/README.md @@ -0,0 +1,269 @@ +# HugeGraph HStore Helm Chart + +Deploys **3 PD + 3 Store + 3 Server and Hubble** by default, with authentication +and persistent storage enabled. Requires **Helm 3.10+ and Kubernetes 1.23+**. +The OCI chart is named `hugegraph-helm`; the application and default Kubernetes resource names remain +`hugegraph`. This chart targets HugeGraph 1.8.0 and later; earlier service releases +are not supported. + +Provide a default StorageClass or prepare matching PVs. The default installation +needs **7 volumes**: 3 × 10 GiB for PD, 3 × 50 GiB for Store, and 1 × 1 GiB +for Hubble. The chart creates PVCs; it does not create a storage provisioner. + +## Install a published Chart + +For a quick trial, install the latest available Chart from GHCR: + +```bash +kubectl config current-context +kubectl get storageclass +helm install hugegraph oci://ghcr.io/hugegraph/hugegraph-helm --devel \ + -n hugegraph --create-namespace --wait --timeout 15m +``` + +> [!TIP] +> `--devel` selects the highest Chart version, including development and stable +> versions, without limiting the version series. For production or reproducible +> installs, replace it with `--version ` and review the images. + +See [distribution and offline installation](docs/distribution.md) for Docker Hub, +the single-node preset and internal OCI/HTTP repositories. Development Charts may +use `latest` images; pin component `image.digest` values when needed. +The source installation and configuration examples below assume a repository +checkout and commands run from its root. + +## Install from source and open Hubble + +```bash +helm install hugegraph ./helm/hugegraph -n hugegraph \ + --create-namespace --wait --timeout 15m + +# Find assigned ports and a reachable node IP +kubectl -n hugegraph get svc hugegraph-server hugegraph-hubble +kubectl get nodes -o wide + +# Read the initial admin password +kubectl -n hugegraph get secret hugegraph-admin \ + -o jsonpath='{.data.password}' | base64 --decode +``` + +Open **`http://:`** and log in as `admin` with that +password. Server API uses `http://:`. +HTTP works directly; a hostname, certificate or extra acknowledgement is not +required. PD remains internal. + +If node access is unavailable, use port-forward: + +```bash +kubectl -n hugegraph port-forward svc/hugegraph-hubble 8088:8088 +# Open http://127.0.0.1:8088 +``` + +Commands assume release/namespace `hugegraph`. For another release, use the names +shown by `kubectl get svc,secret -n `. Default workloads use +`-`; generated Secrets use `-admin`, +`-auth-token`, and `-pd-auth`. + +## Common configurations + +### API only + +Hubble is optional. Current Hubble login requires Server authentication. + +```bash +# Authenticated API, without the UI +helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \ + --set hubble.enabled=false + +# Also disable API authentication, if wanted +# Add: --set server.auth.enabled=false +``` + +Inactive credential values may remain in the values file when auth is disabled. + +### Internal image registry + +Mirror the four component images (`hugegraph/pd`, `hugegraph/store`, +`hugegraph/server`, `hugegraph/hubble`) at the configured tags or digests: + +```bash +helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \ + --set global.imageRegistry=registry.internal.example.com +``` + +A component repository with an explicit registry takes priority over the global +prefix. Set `imagePullSecrets` if the registry needs credentials. +Store's wait init container and Helm test reuse the Store and Server images, +which already contain `curl`; no separate curl image is needed by default. +See the [complete registry example](examples/values-internal-registry.yaml). + +### Storage + +Use a named dynamic StorageClass when the cluster has no suitable default: + +```bash +helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \ + --set pd.storage.storageClassName=my-csi \ + --set store.storage.storageClassName=my-csi \ + --set hubble.persistence.storageClassName=my-csi +``` + +Empty `storageClassName` inherits the cluster default. `"-"` explicitly selects +no StorageClass. Neither creates a provisioner. For local disks, prepare host +directories and PVs first; the [seven-volume local example](examples/README.md#storage) +includes node affinity, capacities and scheduling requirements. + +For a **new temporary installation**, add `--set persistence.enabled=false` +to use `emptyDir`. Component storage flags override that global setting. +PVC failures never cause an automatic fallback. Do not switch initialized +PD/Store data between persistent and temporary storage. + +### Server service identity + +`server.serviceId` and `server.graphSpace` select the Server's PD service metadata. +Both default to `DEFAULT`, which preserves the image configuration. Non-default +values inject `HG_SERVER_SERVICE_ID` and `HG_SERVER_GRAPHSPACE` for the image +entrypoint. Service IDs accept printable ASCII, including spaces and backslashes; +the full name, including `/` and `-`, survives metadata reload and graph-space +clearing. Dollar signs remain literal through Kubernetes environment expansion. +Graph spaces must be `DEFAULT` or up to 48 lowercase letters, digits and +underscores, starting with a letter. Create the target space before starting its +assigned Server; these settings do not create it. + +```bash +helm upgrade --install hugegraph ./helm/hugegraph -n hugegraph \ + --set-string server.serviceId=analytics \ + --set-string server.graphSpace=team_a \ + --set-string server.testGraphAlias=team_a-demo +``` + +Use a chart and Server image build from the same source version containing these +mappings. Older release images without them ignore these environment settings; +select a matching image build explicitly. The chart does not detect old images. +The Server's existing `SERVICE_ID` normalization (`GraphManager.serviceId()`) is +unchanged: select `GRAPHSPACE` and the complete raw `SERVICE_NAME` in discovery +queries. Identifier-only lookup has no uniqueness or isolation guarantee. + +`server.testGraphAlias` selects an existing Gremlin binding for every Server Pod, +defaulting to `DEFAULT-hugegraph`. It does not create a graph or infer its binding +from `server.graphSpace`. Match the binding's case: `Demo.properties` can register +`DEFAULT-Demo`, while REST-created graph names are lowercase. A successful query +of the default local binding does not verify a graph in `team_a`. Before replacing +a Pod after `Could not rebind`, verify that the graph exists and the selected alias +matches its binding; restarting cannot create a missing graph or correct an alias. + +These settings do not change the REST default path graph space or Hubble's +selected service. Use explicit `/graphspaces/team_a/...` paths and the matching +Hubble selection. Non-default discovery, authentication and graph-space isolation +still need verification with a running Linux cluster. See the +[container identity contract](../../docker/README.md#server-service-identity). + +## Modes and entry points + +`mode: easy` is the default. `mode: strict` changes Server/Hubble to ClusterIP +and enables NetworkPolicy; authentication, Hubble and persistence stay enabled. +Use `--set mode=strict` or `-f helm/hugegraph/values-strict.yaml`. + +**Explicit component value > global value > mode default.** An explicit `false` +is preserved; null component values inherit. The global settings are +`persistence.enabled` and `global.imageRegistry`. No mode requires HTTPS. + +| Entry | Access | When to use it | +|---|---|---| +| NodePort | Reachable node IP + assigned port | Direct access without another controller; easy default | +| ClusterIP | Internal Service IP/DNS | In-cluster clients, an existing gateway or port-forward | +| LoadBalancer | Infrastructure-assigned address | A load balancer controller is available | +| Ingress | HTTP routes through a controller | Shared HTTP routing; hostname and TLS are optional | + +ClusterIP is an internal virtual service address, not a node IP. An ordinary +laptop usually cannot reach it directly. With default `externalTrafficPolicy: +Cluster`, NodePort can forward through a node that does not host the target Pod. + +To fix a NodePort, set `server.service.nodePort` or `hubble.service.nodePort`. +Omission or `0` allocates automatically. Kubernetes validates the actual cluster +range; the chart does not force `30000–32767`. + +A [hostless HTTP Ingress example](examples/values-http-ingress.yaml) is available. +With NetworkPolicy enabled, add native rules for actual clients and controllers; +empty external rule lists are valid but can block access. Explicit Service and +policy values override either mode. Legacy exposure acknowledgement fields are +accepted for compatibility and no longer gate rendering. + +## Common parameters + +Defaults below describe a fresh easy-mode install. Supply `--set` or a YAML file +with `-f`; see [values.yaml](values.yaml) for all fields and their comments. + +| Parameter | Meaning | Default | +|---|---|---| +| `mode` | Entry and policy defaults | `easy` | +| `pd.replicas` / `store.replicas` / `server.replicas` | Component replicas | `3` / `3` / `3` | +| `hubble.enabled` | Install the UI | `true` | +| `server.auth.enabled` | Server authentication | `true` | +| `server.service.type` / `hubble.service.type` | Explicit entry type; null inherits mode | resolved `NodePort` | +| `pd.service.type` | PD client entry type | `ClusterIP` | +| `*.service.nodePort` | Fixed Server/Hubble port; omitted/0 allocates automatically | unset | +| `global.imageRegistry` | Prefix image repositories without an explicit registry | `""` | +| `*.image.repository` / `tag` / `digest` | Component image; digest takes priority | see values | +| `persistence.enabled` | Global persistent storage | `true` | +| `pd.storage.enabled` / `store.storage.enabled` / `hubble.persistence.enabled` | Explicit component storage override | `null` (inherits) | +| `*.storageClassName` | Cluster default when empty; `"-"` means no class | `""` | +| `pd.storage.size` / `store.storage.size` / `hubble.persistence.size` | Volume capacities | `10Gi` / `50Gi` / `1Gi` | +| `server.serviceId` / `server.graphSpace` | PD service name and existing graph space | `DEFAULT` / `DEFAULT` | +| `server.testGraphAlias` | Existing Gremlin binding checked by `helm test` | `DEFAULT-hugegraph` | +| `server.storageWaitTimeoutSeconds` | Storage-ready deadline | `300` | +| `networkPolicy.enabled` | Explicit policy override; null inherits mode | resolved `false` | + +
+Advanced settings and presets + +- [Operations](docs/operations.md): credentials, probes, upgrades, membership, + scheduling, policies and troubleshooting. +- [Deployment examples](examples/README.md): private registry, local volumes, + fixed GitOps credentials, external Secrets, HTTP Ingress and API-only usage. +- `values-single.yaml`: 1 PD + 1 Store + 1 Server, with Hubble and authentication. +- `values-cluster.yaml`: required PD/Store anti-affinity, resources and controlled + Store updates. NetworkPolicy inherits mode; the preset is a capacity starting point. +- All client Services support native `externalTrafficPolicy`, `loadBalancerClass`, + `loadBalancerIP`, `loadBalancerSourceRanges`, `externalIPs` and + `allocateLoadBalancerNodePorts`, subject to cluster/controller support. +- Resources, JVM options, scheduling, probes, HPA/PDB, metadata and external + Hubble discovery are configured under their components in `values.yaml`. + Default images preserve automatic JVM sizing; set resources for your capacity. + +
+ +## Upgrade and verify + +```bash +helm get values hugegraph -n hugegraph --all +helm upgrade hugegraph ./helm/hugegraph -n hugegraph --reuse-values \ + --wait --timeout 15m +helm test hugegraph -n hugegraph --logs --timeout 5m +``` + +`--reuse-values` preserves existing UI, entry, storage and credential settings. +An easy-mode upgrade that inherits Server/Hubble entry types prints a short +NodePort-default notice in NOTES; explicitly configured entry types are preserved. +To adopt new defaults, use `--reset-values` with a reviewed values file retaining +all settings your release needs. Older explicit helper images also remain under +value reuse; clear `store.waitImage` and `server.waitImage` to reuse component images. + +The admin Secret is an **initial credential**. Changing it alone does not rotate +an existing account; follow [credential rotation](docs/operations.md#credentials). +The chart checks persisted Raft identities and storage/membership changes during +upgrades; those changes require a coordinated procedure. + +`helm --wait` checks readiness. `helm test` checks graph binding and visible +associated indexes for `CREATED` status on each Server Pod. Run it after index +creation/rebuild jobs finish, with no schema changes during the test. On HStore, +reading an unfinished index can cache its temporary status on other Pods; +retries do not refresh that state, and indexed queries may keep failing until +those Pods restart after the jobs finish. It does not prove index query results; add +[business assertions](docs/operations.md#business-query-assertions) +with `--set-file server.testGremlin=assertions.groovy`. Also verify a fresh Hubble +login and graph query. Templates and linting alone do not prove live volume, +network or application behavior. + +Website guides: [Helm deployment](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm/) +and [Kubernetes operations](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/). diff --git a/helm/hugegraph/docs/distribution.md b/helm/hugegraph/docs/distribution.md new file mode 100644 index 0000000000..8728df0b28 --- /dev/null +++ b/helm/hugegraph/docs/distribution.md @@ -0,0 +1,140 @@ +# Chart distribution and offline installation + +The OCI chart is named `hugegraph-helm`, independently of the HugeGraph service +version. Its source remains in `helm/hugegraph`. The application name, default +resource names and `nameOverride` / `fullnameOverride` behavior remain unchanged. +Use Helm 3.10+ to render and install this Chart, including OCI support. + +This distribution targets HugeGraph **1.8.0 and later**. Development Charts may +use `latest` component images; production deployments need a stable Chart and +published fixed component images. + +## Choose a version and registry + +The same packaged chart is published to both registries: + +| Registry | Chart reference | Typical use | +| --- | --- | --- | +| GHCR | `oci://ghcr.io/hugegraph/hugegraph-helm` | Public CI | +| Docker Hub | `oci://registry-1.docker.io/hugegraph/hugegraph-helm` | Alternative public entry point | + +GHCR is recommended for public CI. To use Docker Hub, replace `CHART` with its +reference from the table: + +```bash +CHART=oci://ghcr.io/hugegraph/hugegraph-helm +helm install hugegraph "$CHART" --devel \ + -n hugegraph --create-namespace --wait --timeout 15m +``` + +> [!TIP] +> `--devel` selects the highest Chart version, including development and stable +> versions, without limiting the version series. For production or reproducible +> installs, replace it with `--version ` to pin your choice. + +[Artifact Hub](https://artifacthub.io/packages/helm/hugegraph-helm/hugegraph-helm) +indexes versions and installation instructions. The Chart archive itself comes +from GHCR or Docker Hub. + +For a single-node trial, use this instead of the default installation. Install +the downloaded Chart so its bundled preset and templates use the same version: + +```bash +CHART=oci://ghcr.io/hugegraph/hugegraph-helm +helm pull "$CHART" --devel --untar +helm install hugegraph ./hugegraph-helm -n hugegraph \ + -f ./hugegraph-helm/values-single.yaml \ + --create-namespace --wait --timeout 10m +``` + +Development packages append a workflow run number to the source chart version, +for example `0.2.0-dev.123`. They may use `latest` service images. Helm OCI tags +come from the chart's SemVer version; `helm-dev` and `latest` are not Chart versions. + +Formal packages use the exact `Chart.version` maintained in source, for example +`0.2.0`. They are published only after the corresponding HugeGraph service release +exists and the chart selects published fixed component images. A source tag +such as `helm-1.8.0` therefore denotes an already released service, not an +advance preview. Chart fixes may be released independently from a reviewed master +commit after the service release; chart and service version numbers need not match. + +The manual workflow in [hugegraph/actions](https://github.com/hugegraph/actions) +resolves its selected source ref to a fixed commit, validates and packages the +chart, and installs that exact `.tgz` for its Kubernetes checks before publishing. +Turning off `publish` runs validation without uploading to a registry. + +## Install and mirror a chart offline + +On an internet-connected machine, select the latest available version and +record it before downloading. Transfer the `.tgz` and container images into +your network. The archive contains no container images. + +```bash +CHART=oci://ghcr.io/hugegraph/hugegraph-helm +CHART_VERSION=$(helm show chart "$CHART" --devel | sed -n 's/^version: //p') +helm pull "$CHART" --version "$CHART_VERSION" +``` + +Set your internal container registry in `private-images.yaml`. Preserve the +component paths, such as `hugegraph/pd`, when mirroring the images: + +```yaml +# private-images.yaml +global: + imageRegistry: registry.internal.example.com +``` + +Choose one installation method below and replace the example addresses with +your own. Each method installs the same Chart version and uses the private images. + +
+Install the archive directly + +```bash +helm install hugegraph "./hugegraph-helm-${CHART_VERSION}.tgz" \ + -n hugegraph --create-namespace -f private-images.yaml --wait --timeout 15m +``` + +
+ +
+Internal OCI registry + +```bash +helm registry login registry.internal.example.com +helm push "./hugegraph-helm-${CHART_VERSION}.tgz" \ + oci://registry.internal.example.com/charts +helm install hugegraph oci://registry.internal.example.com/charts/hugegraph-helm \ + --version "$CHART_VERSION" -n hugegraph --create-namespace \ + -f private-images.yaml --wait --timeout 15m +``` + +`helm push` takes the parent registry path; Helm adds the Chart name and version. + +
+ +
+Internal HTTP repository + +Upload the same `.tgz` using the repository's upload mechanism and update +`index.yaml`. For a static HTTP repository, `helm repo index` creates the index. +Native `helm push` only supports OCI. + +```bash +helm repo add hugegraph-internal https://charts.internal.example.com +helm repo update hugegraph-internal +helm install hugegraph hugegraph-internal/hugegraph-helm \ + --version "$CHART_VERSION" -n hugegraph --create-namespace \ + -f private-images.yaml --wait --timeout 15m +``` + +
+ +Mirror the configured `hugegraph/pd`, `hugegraph/store`, `hugegraph/server` and, +when enabled, `hugegraph/hubble` images at their selected tags or digests. The +default Store wait container and Helm test hook reuse component images. Explicit +`store.waitImage` / `server.waitImage` overrides must also be mirrored. Use +`global.imageRegistry`, per-component repositories and `imagePullSecrets` as +shown in the [private registry example](../examples/values-internal-registry.yaml). +The mirror must preserve the selected image references and support the cluster's +CPU architecture. diff --git a/helm/hugegraph/docs/operations.md b/helm/hugegraph/docs/operations.md new file mode 100644 index 0000000000..4c90e1e6ba --- /dev/null +++ b/helm/hugegraph/docs/operations.md @@ -0,0 +1,359 @@ +# Helm operations + +Start with [installation and common settings](../README.md). +[values.yaml](../values.yaml) documents every available parameter; this page +covers settings that need coordination or diagnosis. + +Commands below use the local chart from the repository root. For a published +Chart, replace `./helm/hugegraph` with +`oci://ghcr.io/hugegraph/hugegraph-helm --version `. +Keep the installed version for configuration-only changes; choose and review a +new version for upgrades. See [version selection](distribution.md#choose-a-version-and-registry). + +## Credentials + +Normal Helm installations generate/reuse three kept Secrets: + +| Default Secret | Data key | Readers | +|---|---|---| +| `-admin` | `password` | Server, Helm test | +| `-auth-token` | `token_secret` | All Server replicas | +| `-pd-auth` | `secret-key` | PD, Server storage wait, Hubble | + +`existingSecret` takes priority over an inline value, which takes priority over +auto-generation. Existing Secrets are not managed by the chart. Override their +key names through the corresponding `key` fields. + +The admin password must be printable ASCII without spaces, colons or +backslashes. The PD REST secret must be printable ASCII without surrounding +whitespace or backslashes. JWT signing keys must be at least 32 bytes. +These constraints match the current image's authentication/property encoding. + +The admin Secret supplies the **initial** password when the account is created. +To rotate an existing account, first call the Server API with the old credential: + +```bash +curl --user 'admin:' \ + -X PUT -H 'Content-Type: application/json' \ + -d '{"user_password":""}' \ + "http:///graphspaces/DEFAULT/auth/users/admin" +``` + +Do not replace password placeholders with real credentials in recorded shell +commands: they can enter history or briefly appear in process arguments. +For interactive calls, `curl --user admin` (or `--user hg` for PD) prompts for +the password. Variable-based examples avoid literal passwords in shell history. + +
+Optional credential files for shared terminals or repeated requests + +Create a curl config and password-update JSON with an editor or secret manager, +using owner-only permissions (`600`). The config contains +`user = "admin:"`; the JSON contains the `user_password` body above. +Then use `--config /path/to/server.curl` and +`--data-binary @/path/to/password-update.json` instead of inline credentials. +For PD commands, use a separate config containing `user = "hg:"` +and replace `--user "hg:${PD_SECRET}"` with `--config /path/to/pd.curl`. +Do not log or commit these files; remove them when no longer needed. + +
+ +Update the Secret and its source values to the same new password, then restart +all Server replicas to clear cached credentials: + +```bash +kubectl -n hugegraph rollout restart deployment/hugegraph-server +``` + +Use your secret manager rather than storing real passwords in public files. +GitOps needs fixed encrypted values or external Secrets, because offline +renderers cannot reuse a live generated Secret through `lookup`. +[Both complete examples](../examples/README.md#gitops-credentials) include all +three credential sources. Changing PD credentials rolls PD, Server and Hubble; +verify Gremlin and UI login after that coordinated rollout. + +### PD REST authentication + +PD REST uses HTTP Basic auth with a service username: `hg`, `store`, `hubble` +or `vermeer`. Server's `admin` account is not a PD REST account and receives 401. +All four service names use the shared PD REST password, not the admin password. +For the default release/Secret, query a reachable PD REST address (port 8620): + +```bash +PD_SECRET=$(kubectl -n hugegraph get secret hugegraph-pd-auth \ + -o jsonpath='{.data.secret-key}' | base64 --decode) +curl --user "hg:${PD_SECRET}" "http://:8620/v1/stores" +``` + +With `pd.auth.existingSecret` or a custom `pd.auth.key`, read the actual Secret +and key instead. See the [PD REST API reference](../../../hugegraph-pd/docs/api-reference.md#rest-api) +for probe exceptions and API details. Management tasks below must target the PD leader. + +## Storage and upgrades + +Inspect saved values before upgrading. `--reuse-values` preserves old effective +settings, including disabled Hubble, ClusterIP and explicit helper images. +Adopt new defaults only with reviewed values and `--reset-values`. + +PD/Store PVC size and StorageClass live in StatefulSet claim templates and are +not routine mutable settings. For capacity growth, the StorageClass must support +expansion: resize each PVC, wait for completion, then coordinate an orphaned +StatefulSet recreation with matching claim-template values. Preserve Pod/claim +names and persisted data; do not recreate claims as a resize workaround. + +PD/Store storage-mode changes are rejected after initialization. Hubble uses +an embedded H2 database with connection metadata and saved graph credentials; +a PVC and `Recreate` deployment strategy preserve it across Pod replacement. +Disabling Hubble or uninstalling does not delete kept claims/Secrets automatically. +Changing Hubble storage requires a separate reviewed metadata migration. + +An older Hubble Deployment may have no `data` volume: its H2 records live in +its container filesystem. The new persistent default is a storage change too, +so a live Helm upgrade rejects it rather than silently creating an empty database. +Keep `hubble.persistence.enabled=false` with `--reuse-values` or an explicit +GitOps value to preserve the previous storage mode. Move any records you need +to retain through a reviewed metadata migration before enabling persistence; +retaining the temporary mode does not make those records survive Pod replacement. +Export or back up required metadata before any rollout of that old Pod, including +an upgrade that retains `hubble.persistence.enabled=false`. +Offline `helm template` and GitOps rendering cannot inspect the existing Deployment +through `lookup`, so this live guard cannot enforce the migration requirement there. +Export and explicitly pin the existing storage flags before a GitOps sync; a +successful offline render does not establish a safe migration. + +Local PVs bind to nodes and cannot move their files automatically on node loss. +See [all seven local PVs](../examples/README.md#storage) for directory, capacity, +claim binding and scheduling requirements. `Retain` preserves files on claim +deletion; reclamation/recovery needs explicit inspection. + +## Startup and probes + +Store's init container waits for a majority of PD peers on `store.waitPath` +(default `/v1/ready`) for up to `store.waitTimeoutSeconds` (default 900). +PD readiness requires a Raft leader. Images with the fatal-health fix return +`503` from `/v1/health` for terminal Raft errors or shutdown, while elections +and temporary quorum loss remain healthy. Older images only check the listener. +Store's own readiness does not prove every shard has recovered. + +Server waits for storage for `server.storageWaitTimeoutSeconds` (default 300), +passed as `WAIT_STORAGE_TIMEOUT_S`. An unspecified startup threshold is derived +from that wait plus a 150-second startup allowance. Explicit thresholds are +preserved and must leave at least 120 seconds beyond the storage wait. +The probe budget is `initialDelaySeconds + (failureThreshold - 1) * periodSeconds`; +without an initial delay, the first probe can run immediately. Contradictory budgets fail rendering with +the relevant parameter names; `HG_SERVER_STARTUP_TIMEOUT_S` is chart-managed. +Legacy reused values retain their earlier probe-budget compatibility behavior. + +`store.waitImage` and `server.waitImage` default empty and reuse the Store/Server +images, respectively. Those images already include `/bin/sh` and `curl`. +Explicit helper overrides remain supported for custom images. To clear an older +override without resetting other release settings: + +```bash +helm upgrade hugegraph ./helm/hugegraph -n hugegraph --reuse-values \ + --set-string store.waitImage= --set-string server.waitImage= +``` + +## Topology and scheduling + +The startup wrapper sets `usePD=true` and the chart PD peers so all Servers +share graph metadata. Distributed Servers keep `initStoreEnabled=false` and do +not concurrently run `init-store`. Each Server registers its Pod address with +PD unless `server.advertiseUrl` specifies one shared discovery address. + +At first bootstrap, the chart seeds three shard replicas with at least three +Stores, or one otherwise. PD persists those settings. Chart changes after +initialization do not rewrite the partition metadata; use the PD configuration +API and coordinate partition patrol for existing groups. + +PD Raft addresses, Store Raft addresses and StatefulSet names are persisted +identities. Do not change those hostnames/ports as an ordinary upgrade. +PD membership changes and Store shrink require explicit member/shard operations; +the chart rejects unsupported changes against initialized live StatefulSets. +Fresh installs do not perform the upgrade-only StatefulSet query. + +For Store shrink, first confirm the remaining Stores can hold the configured +shard replica count and data. Retire the leaving Store IDs through PD and run +partition patrol on the PD leader. Wait until no shard group references a retired ID and +all remaining groups have their full membership and one leader. Only then +scale the live StatefulSet and upgrade with the matching `store.replicas`; +PVC removal is a separate step after retirement. Keep the installed PD count +unless its persisted voting configuration is reconfigured outside Helm; a +fresh release with data migration is the alternative. The +[PD management API](../../../hugegraph-pd/docs/api-reference.md) documents the +Store state and task endpoints. + +Default preferred anti-affinity permits fewer nodes than replicas. The cluster +preset requires separate nodes for PD/Store; explicit affinity overrides presets. +Local PV node affinity must agree with scheduling rules. PDBs protect voluntary +eviction, not controller-driven updates. PD needs a majority available; Store +needs enough shard copies to remain available. + +Server replicas can be changed routinely; CPU-utilization HPA requires positive +CPU requests. Store growth still needs volume and shard planning. Use chart +updates for Store scale changes so Hubble's endpoint list is updated too. + +## Rolling Store updates and recovery + +Use `store.updateStrategy.type=OnDelete` for a controlled rollout. Replace one +Store Pod at a time. Before replacing another, confirm the previous Pod is +Ready, its Store heartbeat is fresh, and every shard group reports full +membership and exactly one leader from the PD leader's view. +PD `Up` status alone does not prove shard recovery; stop on missing members or +leaders. The [website operations guide](https://hugegraph.apache.org/docs/quickstart/hugegraph/hugegraph-helm-operations/) +explains the rollout and recovery constraints. +A single Store replacement can still cause request errors or timeouts while Raft +leadership and client routes converge. Observe application errors as well as +shard recovery before continuing; do not blindly retry writes. + +### PD patrol and leader balance + +Partition patrol repairs shard membership; it does not balance shard leaders. +After a Store replacement, leader counts can remain uneven because leader +balancing is not scheduled automatically. Use the existing manual task when +all Stores have fresh heartbeats and every group has full, healthy membership. + +Find the PD leader through authenticated `/v1/members` (`data.pdLeader.restUrl`). +Forward that Pod in a separate terminal, not a randomly selected Service peer: + +```bash +kubectl -n hugegraph port-forward pod/ 8620:8620 +``` + +In the terminal holding `PD_SECRET` from the credentials section, check that +`/v1/ready` reports `isLeader: true`, then inspect the current group and Store state: + +```bash +PD_REST=http://127.0.0.1:8620 +curl "${PD_REST}/v1/ready" +curl --user "hg:${PD_SECRET}" "${PD_REST}/v1/partitions" +curl --user "hg:${PD_SECRET}" "${PD_REST}/v1/stores" +``` + +After an intentional member retirement or shard-count change, patrol on that +leader if needed; it is not a leader-balancing command: + +```bash +curl --user "hg:${PD_SECRET}" "${PD_REST}/v1/task/patrolPartitions" +``` + +Wait for membership recovery and split/move tasks to finish before requesting +leader balance separately: + +```bash +curl --user "hg:${PD_SECRET}" "${PD_REST}/v1/task/balanceLeaders" +``` + +A busy task returns body `status: 1001`; inspect the result and wait for current +maintenance to finish rather than looping patrol/balance calls. A follower can +return an empty success, and leader transfers are asynchronous: HTTP 200 or a +returned transfer plan does not prove completion. Recheck `/v1/partitions` and +`/v1/stores` for actual leaders, full membership and fresh heartbeats before +continuing maintenance. + +Preserve volumes and recorded member identities during recovery. Deleting a +RocksDB lock file or assigning an empty volume does not recover shard data. +The chart does not provide backup or automatic disk-loss recovery; keep backups +outside the data PVs and validate recovery separately. + +## Networking and Hubble + +Easy mode leaves NetworkPolicy off; strict enables it. Explicit values win. +Policies require a network plugin that enforces them. Generated rules allow +component communication and DNS; admit external clients/controllers with native +`extraIngress`, and external addresses with appropriate `extraEgress`. +Selecting strict mode and then overriding a Service to NodePort/LoadBalancer, +setting `externalIPs`, or enabling Ingress does not automatically admit outside traffic. NOTES names +exposed components with no external ingress rule and points to the matching +`networkPolicy..extraIngress` setting; template rendering remains valid. +For Hubble PD discovery via an external `server.advertiseUrl`, also configure +Hubble's `extraEgress` for that destination. +Empty external rule lists are valid and can block clients; `{}` is a valid +allow-all rule. Consider NodePort source NAT when choosing source ranges. + +Client Services accept native Kubernetes traffic and load-balancer fields under +`pd.service`, `server.service` and `hubble.service`. LoadBalancer-specific fields +need that Service type and a compatible controller. Hostless HTTP Ingress is +supported; install an Ingress controller separately. TLS is optional. + +In-chart Hubble is wired to internal PD/Server endpoints. `hubble.mode=direct` +uses only the Server Service and has no PD operations view. External Hubble +needs reachable Server addresses; PD discovery additionally needs reachable PD +gRPC and a suitable `server.advertiseUrl`. That override affects every discovery +client, including in-cluster Hubble. Current Hubble login requires Server auth; +use `hubble.enabled=false` for an unauthenticated API-only deployment. + +API response compression follows `Accept-Encoding`. Clients that support gzip +can request it explicitly or use `curl --compressed`; requests with `identity` +or without a compression header receive plain responses. Quality factors `q=0.` +and `q=1.` are equivalent to `q=0` and `q=1`. Explicit gzip exclusion overrides +`*`; a higher `identity` quality selects a plain response. +Older images can still force gzip; use a Server image built from current source. +Updating chart values alone does not change an existing image's behavior. + +## Business query assertions + +The default `helm test` checks the `server.testGraphAlias` binding (`DEFAULT-hugegraph` by default) +and requires `CREATED` indexes +associated with the vertex/edge labels visible on each Pod. It reads those +indexes by ID, using the same local cache path as queries; it does not refresh +the full index cache. This does not detect missing/stale label associations, +orphaned indexes or incorrect index data, and does not prove range-query results. +Run it after index creation/rebuild jobs finish, with no schema changes during +the test. Like indexed queries, the check rejects every other status, including +`CREATING`, `REBUILDING` and `INVALID`. On HStore, reading an unfinished index can +cache its temporary status on Pods other than the job runner. Later attempts +reuse that cache; the retry window does not repair it. Indexed queries on those +Pods may keep failing after the job succeeds; replace affected Server Pods only +after the jobs finish. This check does not fix cross-Server schema cache consistency. + +For a graph with existing test data, provide a read-only Gremlin script that +asserts the expected result. For example, with 40 existing `person` vertices +(ages 0–39), save this as `assertions.groovy`, adapting names/results to your data: + +```groovy +def n = graph.traversal().V().hasLabel('person') + .has('age', org.apache.tinkerpop.gremlin.process.traversal.P.gte(20)) + .count().next() +assert n == 20 : "Expected 20 adults, got ${n}" +``` + +```bash +helm upgrade hugegraph ./helm/hugegraph -n hugegraph --reuse-values \ + --set-file server.testGremlin=assertions.groovy +helm test hugegraph -n hugegraph --logs --timeout 5m +``` + +In each attempt, the hook checks binding/index state on every Ready Server Pod, +then runs optional business assertions on Pods where that check succeeds. A failing +request or assertion reports its Pod IP, check stage and response, and fails +the hook if no complete attempt succeeds before its deadline. Use +`kubectl -n hugegraph get pods -o wide` to map IPs to Pod names. Returning a value +or `false` alone is not a failure: use `assert` or throw an exception. Without a +script, the hook explicitly reports business assertions as skipped. The hook +creates no schema/data by default. Custom scripts use the hook's Server credential +(`admin` when authentication is enabled); read-only execution is not enforced, +so supply trusted, read-only assertions. The hook does not guarantee the first request +after startup succeeds; its retries allow transient elections to settle. + +## Troubleshooting + +| Symptom | First checks | +|---|---| +| PVC Pending | StorageClass/provisioner, matching PVs, PVC and Pod events; initial `WaitForFirstConsumer` Pending can be normal | +| Mount failure | Host directory, node affinity and image UID/GID permissions | +| ImagePullBackOff | Pod events, registry reachability, image tags/digests and `imagePullSecrets` | +| `HG_PD_AUTH_SECRET_KEY` missing | PD Pod Secret reference and key; chart injects the shared PD REST password automatically | +| Server storage wait timeout | Ready PD majority, Store registration, network policies and `server.storageWaitTimeoutSeconds` | +| NodePort/Ingress unreachable | Assigned port, reachable node/controller address, firewall and NetworkPolicy rules | +| REST works but Gremlin reports `Could not rebind` | Run `helm test`. Server images built from source that includes apache/hugegraph#3351 (the fix for #3228) rebind Gremlin when REST loads the graph; if it persists, check the Server log for `could not be bound in Gremlin Server` or `can't be rebound in Gremlin Server`, then replace the failing Server Pod after PD stabilizes and retry | + +```bash +kubectl -n hugegraph get pods,pvc,svc +kubectl -n hugegraph describe pod +kubectl -n hugegraph logs +helm test hugegraph -n hugegraph --logs --timeout 5m +``` + +Never print credential values in diagnostic logs. Ready and `/versions` do not +prove graph binding or fresh UI login; verify graph queries and a new login too. diff --git a/helm/hugegraph/examples/README.md b/helm/hugegraph/examples/README.md new file mode 100644 index 0000000000..1f748d70d7 --- /dev/null +++ b/helm/hugegraph/examples/README.md @@ -0,0 +1,140 @@ +# HugeGraph deployment examples + +These files are examples, not automatically installed resources. Replace registry, +node names, paths, release/namespace and credentials for your cluster before use. +Commands below are deployment instructions; authoring them does not deploy anything. + +## Storage + +The default topology needs **seven volumes**: 3 × 10 GiB for PD, 3 × 50 GiB for +Store, and 1 × 1 GiB for Hubble. With a default dynamic StorageClass, ordinary +`helm install` requests them automatically. For a named dynamic StorageClass: + +```bash +helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \ + --set pd.storage.storageClassName=my-csi \ + --set store.storage.storageClassName=my-csi \ + --set hubble.persistence.storageClassName=my-csi +``` + +An empty chart `storageClassName` inherits the cluster default; `"-"` renders +an explicit empty Kubernetes StorageClass and disables default-class assignment. +Neither creates a provisioner. PVCs remain Pending without matching PVs. + +### Static local volumes + +[local-pvs.yaml](local-pvs.yaml) contains a `kubernetes.io/no-provisioner` +StorageClass with `WaitForFirstConsumer` and all seven prebound PVs. It assumes +release/namespace `hugegraph` and hostname labels `worker-1`, `worker-2`, `worker-3`. +Change both `claimRef` and nodes if those names differ. StatefulSet claim names +are `---`; Hubble uses +`-hubble-data`. + +On each indicated host, create these directories on disks with sufficient free +space and make them writable by the UID/GID of the chosen images (published +images currently run as root). Existing unrelated data must not be reused. + +| Node | Directories | Required capacities | +|---|---|---| +| worker-1 | `/var/lib/hugegraph/pd-0`, `/var/lib/hugegraph/store-0`, `/var/lib/hugegraph/hubble` | 10 GiB + 50 GiB + 1 GiB | +| worker-2 | `/var/lib/hugegraph/pd-1`, `/var/lib/hugegraph/store-1` | 10 GiB + 50 GiB | +| worker-3 | `/var/lib/hugegraph/pd-2`, `/var/lib/hugegraph/store-2` | 10 GiB + 50 GiB | + +PV capacity is a binding declaration, not a directory quota; arrange filesystem +capacity separately. Check labels with `kubectl get nodes -L kubernetes.io/hostname`, +then apply the reviewed resources: + +```bash +kubectl create namespace hugegraph +kubectl apply -f helm/hugegraph/examples/local-pvs.yaml +helm install hugegraph ./helm/hugegraph -n hugegraph \ + -f helm/hugegraph/examples/values-local-storage.yaml --wait --timeout 15m +``` + +`WaitForFirstConsumer` may leave a PVC Pending until a Pod can be scheduled; +local PV node affinity and workload affinity/nodeSelector must allow the same +node. Local volumes survive Pod replacement on that node, but cannot move their +files to another node on failure. `Retain` keeps data on claim deletion; +recovery/reuse requires operator inspection. + +For a fresh temporary installation, `--set persistence.enabled=false` uses +`emptyDir`. Component `pd.storage.enabled`, `store.storage.enabled` and +`hubble.persistence.enabled` override the global choice. A failed PVC never +falls back to temporary storage. Changing an initialized cluster's storage mode +requires a separate data migration/recovery procedure. + +## Internal images + +[values-internal-registry.yaml](values-internal-registry.yaml) mirrors the default +four images: `hugegraph/pd:latest`, `hugegraph/store:latest`, +`hugegraph/server:latest`, and `hugegraph/hubble:latest`. Tags follow the chart's +current version policy; pin your own published tags/digests. + +`global.imageRegistry` prefixes repositories without an explicit registry. +A repository such as `other-registry.example.com/team/pd` overrides that prefix. +Registry credentials remain `imagePullSecrets`. + +The PD wait init container reuses the Store image and Helm test reuses the +Server image. Both already include `curl`; the chart selects a shell command +rather than the component entrypoint. No extra `curlimages/curl` image or runtime +package installation is needed by default. `store.waitImage` and `server.waitImage` +remain explicit overrides for custom images. Store wait overrides need +`/bin/sh`, `curl`, `tr`, `date` and `sleep`; Server test overrides additionally +need `base64`, `getent`, `awk`, `sort`, `grep` and `tail`. Reused component images +also inherit the component pull policy. Older values may retain a curl image +under `--reuse-values`; clear those two fields to restore inheritance. + +```bash +helm install hugegraph ./helm/hugegraph -n hugegraph --create-namespace \ + -f helm/hugegraph/examples/values-internal-registry.yaml --wait --timeout 15m +``` + +## GitOps credentials + +Use [values-gitops-fixed.yaml](values-gitops-fixed.yaml) with encrypted values, +or [values-gitops-existing-secrets.yaml](values-gitops-existing-secrets.yaml) with +a secret controller. Replace sample credentials before use. For the latter, +provision these three Secret data keys before the workloads start: + +| Secret | Key | Purpose | +|---|---|---| +| `hugegraph-pd-credentials` | `secret-key` | Shared PD REST Basic-auth password | +| `hugegraph-admin-credentials` | `password` | Initial Server admin password | +| `hugegraph-token-credentials` | `secret` | Shared JWT signing key, at least 32 bytes | + +The admin password must be printable ASCII without spaces, colons or backslashes. +PD credentials must be printable ASCII without surrounding whitespace or +backslashes. `existingSecret` takes priority over fixed values and is not managed +by the chart. Ordinary Helm installations generate/reuse credentials; repeated +offline renders cannot look up live generated Secrets and otherwise generate new +random values. Changing the admin Secret alone does not change an already-created +account; follow the [account password and Secret synchronization steps](../docs/operations.md#credentials). + +Before moving an existing release to a declarative GitOps renderer, export and +pin its effective values; offline rendering cannot apply Helm's `--reuse-values`. +Keep the existing `server.service.type`, `hubble.service.type`, `hubble.enabled` +and storage flags explicitly, as well as fixed/external credentials. Selecting +`mode: strict` keeps inherited Services internal, but also enables NetworkPolicy; +set `networkPolicy.enabled` explicitly to preserve the installed behavior. +A legacy Hubble with no mounted data volume still has metadata in its container +filesystem: keep `hubble.persistence.enabled: false` until a reviewed migration +moves that metadata to a PVC. Export or back up any records you need before +upgrading: keeping the temporary mode does not retain the old container's H2 +files when its Pod is replaced. Do not use a default reset as a migration. + +## Entry points and UI + +[values-http-ingress.yaml](values-http-ingress.yaml) uses an already installed +nginx controller for hostless HTTP Server Ingress. Both modes allow HTTP; +certificates, hostnames and acknowledgement fields are optional. +NodePort needs reachable node IPs and firewall permissions; query the actual port +with `kubectl -n hugegraph get svc`, then use `http://:`. +ClusterIP stays inside the cluster; use a gateway or port-forward for external +access. LoadBalancer needs your infrastructure's controller. + +[values-api-only.yaml](values-api-only.yaml) disables Hubble. Keep auth for an +authenticated API; disable `server.auth.enabled` as well for an unauthenticated +API. Current Hubble login still needs Server auth. Strict mode defaults to internal +Services and NetworkPolicy, but explicit parameters override defaults. With +NetworkPolicy enabled, add native `extraIngress` rules for actual clients; `{}` +is a valid allow-all rule. diff --git a/helm/hugegraph/examples/local-pvs.yaml b/helm/hugegraph/examples/local-pvs.yaml new file mode 100644 index 0000000000..72721ea9f0 --- /dev/null +++ b/helm/hugegraph/examples/local-pvs.yaml @@ -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. +# + + +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: hugegraph-local +provisioner: kubernetes.io/no-provisioner +volumeBindingMode: WaitForFirstConsumer +reclaimPolicy: Retain +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-pd-0 +spec: + capacity: + storage: 10Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: pd-data-hugegraph-pd-0 + local: + path: /var/lib/hugegraph/pd-0 + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-1] +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-pd-1 +spec: + capacity: + storage: 10Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: pd-data-hugegraph-pd-1 + local: + path: /var/lib/hugegraph/pd-1 + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-2] +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-pd-2 +spec: + capacity: + storage: 10Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: pd-data-hugegraph-pd-2 + local: + path: /var/lib/hugegraph/pd-2 + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-3] +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-store-0 +spec: + capacity: + storage: 50Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: store-data-hugegraph-store-0 + local: + path: /var/lib/hugegraph/store-0 + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-1] +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-store-1 +spec: + capacity: + storage: 50Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: store-data-hugegraph-store-1 + local: + path: /var/lib/hugegraph/store-1 + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-2] +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-store-2 +spec: + capacity: + storage: 50Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: store-data-hugegraph-store-2 + local: + path: /var/lib/hugegraph/store-2 + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-3] +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: hugegraph-hubble +spec: + capacity: + storage: 1Gi + volumeMode: Filesystem + accessModes: [ReadWriteOnce] + persistentVolumeReclaimPolicy: Retain + storageClassName: hugegraph-local + claimRef: + namespace: hugegraph + name: hugegraph-hubble-data + local: + path: /var/lib/hugegraph/hubble + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: [worker-1] diff --git a/helm/hugegraph/examples/values-api-only.yaml b/helm/hugegraph/examples/values-api-only.yaml new file mode 100644 index 0000000000..9e6c4c5216 --- /dev/null +++ b/helm/hugegraph/examples/values-api-only.yaml @@ -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. +# + + +# Current Hubble login needs Server auth; disable UI for API-only. +hubble: + enabled: false +# Optional unauthenticated API: +# server: +# auth: +# enabled: false diff --git a/helm/hugegraph/examples/values-gitops-existing-secrets.yaml b/helm/hugegraph/examples/values-gitops-existing-secrets.yaml new file mode 100644 index 0000000000..8b5d65d544 --- /dev/null +++ b/helm/hugegraph/examples/values-gitops-existing-secrets.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. +# + + +# Provision these Secrets before workloads through your secret controller. +pd: + auth: + existingSecret: hugegraph-pd-credentials + key: secret-key + autoGenerate: false +server: + auth: + admin: + existingSecret: hugegraph-admin-credentials + key: password + autoGenerate: false + token: + existingSecret: hugegraph-token-credentials + key: secret + autoGenerate: false diff --git a/helm/hugegraph/examples/values-gitops-fixed.yaml b/helm/hugegraph/examples/values-gitops-fixed.yaml new file mode 100644 index 0000000000..b4c5294054 --- /dev/null +++ b/helm/hugegraph/examples/values-gitops-fixed.yaml @@ -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. +# + + +# Example only: replace with unique credentials in encrypted values. +pd: + auth: + value: REPLACE_WITH_PD_REST_SECRET + autoGenerate: false +server: + auth: + admin: + password: REPLACE_WITH_INITIAL_ADMIN_PASSWORD + autoGenerate: false + token: + value: REPLACE_WITH_UNIQUE_JWT_SIGNING_KEY_AT_LEAST_32_BYTES + autoGenerate: false diff --git a/helm/hugegraph/examples/values-http-ingress.yaml b/helm/hugegraph/examples/values-http-ingress.yaml new file mode 100644 index 0000000000..f459bb2866 --- /dev/null +++ b/helm/hugegraph/examples/values-http-ingress.yaml @@ -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. +# + + +# Requires an installed Ingress controller; host/TLS are optional. +server: + service: + type: ClusterIP + ingress: + enabled: true + className: nginx + hosts: + - paths: + - path: / + pathType: Prefix + tls: [] +hubble: + service: + type: ClusterIP + ingress: + enabled: false +# Use separate hosts/paths to expose Hubble through the same controller. diff --git a/helm/hugegraph/examples/values-internal-registry.yaml b/helm/hugegraph/examples/values-internal-registry.yaml new file mode 100644 index 0000000000..65c6301f34 --- /dev/null +++ b/helm/hugegraph/examples/values-internal-registry.yaml @@ -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. +# + + +# Mirror hugegraph/pd:latest, hugegraph/store:latest, +# hugegraph/server:latest, hugegraph/hubble:latest at your private registry. +global: + imageRegistry: registry.internal.example.com +store: + waitImage: "" +server: + waitImage: "" +# Qualified component repository overrides the global prefix: +# pd: +# image: +# repository: other-registry.example.com/team/pd +# imagePullSecrets: +# - name: registry-credentials diff --git a/helm/hugegraph/examples/values-local-storage.yaml b/helm/hugegraph/examples/values-local-storage.yaml new file mode 100644 index 0000000000..a6a31a9f7e --- /dev/null +++ b/helm/hugegraph/examples/values-local-storage.yaml @@ -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. +# + + +# Apply local-pvs.yaml after preparing the host directories. +# Release/namespace must match the example PV claimRef: hugegraph. +pd: + storage: + storageClassName: hugegraph-local +store: + storage: + storageClassName: hugegraph-local +hubble: + persistence: + storageClassName: hugegraph-local diff --git a/helm/hugegraph/templates/NOTES.txt b/helm/hugegraph/templates/NOTES.txt new file mode 100644 index 0000000000..10892ba8ef --- /dev/null +++ b/helm/hugegraph/templates/NOTES.txt @@ -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. +*/}} +{{ include "hugegraph.resolveValues" . }} +{{- $hubbleEnabled := .Values.hubble.enabled }} +{{ .Release.Name }} installed in namespace {{ .Release.Namespace }} ({{ .Values.mode | default "legacy" }} mode). + + PD {{ .Values.pd.replicas }} replica(s) + Store {{ .Values.store.replicas }} replica(s) + Server {{ if .Values.server.hpa.enabled }}HPA {{ .Values.server.hpa.minReplicas }}-{{ .Values.server.hpa.maxReplicas }}{{ else }}{{ .Values.server.replicas }} replica(s){{ end }} +{{- if $hubbleEnabled }} + Hubble 1 replica (UI) +{{- end }} +{{- $inheritedEntries := list }} +{{- range $component := list "server" "hubble" }} +{{- $cfg := get $.Values $component }} +{{- if and (or (ne $component "hubble") $hubbleEnabled) (get $cfg "__serviceTypeInherited") (eq $cfg.service.type "NodePort") }} +{{- $inheritedEntries = append $inheritedEntries ($component | title) }} +{{- end }} +{{- end }} +{{- if and .Release.IsUpgrade (eq (.Values.mode | default "legacy") "easy") $inheritedEntries }} + +Upgrade notice: chart 0.2.0 changed entry defaults. This easy-mode upgrade inherits NodePort for {{ join ", " $inheritedEntries }}. +For releases installed before 0.2.0, review helm/hugegraph/docs/operations.md#storage-and-upgrades. +Use --reuse-values to retain previous settings, or explicitly set the desired Service types and Hubble setting. +{{- end }} + +Watch startup (run each command in a separate terminal): + + kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }} -w + kubectl get pvc -n {{ .Release.Namespace }} -w +{{- $claims := 0 }} +{{- if .Values.pd.storage.enabled }}{{ $claims = add $claims (int .Values.pd.replicas) }}{{ end }} +{{- if .Values.store.storage.enabled }}{{ $claims = add $claims (int .Values.store.replicas) }}{{ end }} +{{- if and $hubbleEnabled .Values.hubble.persistence.enabled }}{{ $claims = add $claims 1 }}{{ end }} +{{- if gt (int $claims) 0 }} + +This configuration needs {{ $claims }} persistent volume claim(s). A dynamic +StorageClass or matching pre-created PVs must be available; see helm/hugegraph/examples/README.md#storage. +{{- end }} + +Store waits for a PD quorum before startup. If a Pod remains in Init, inspect +its init-container logs and PD readiness; see helm/hugegraph/docs/operations.md#troubleshooting. + +Access: +{{- range $component := list "server" "hubble" "pd" }} +{{- if or (ne $component "hubble") $hubbleEnabled }} +{{- $cfg := get $.Values $component }} +{{- $name := include (printf "hugegraph.%s.name" $component) $ }} +{{- if eq $component "pd" }}{{ $name = include "hugegraph.pd.clientName" $ }}{{ end }} +{{- $portName := "http" }} +{{- $port := get $cfg "port" }} +{{- if eq $component "pd" }}{{ $portName = "rest" }}{{ $port = $cfg.ports.rest }}{{ end }} +{{- $svc := $cfg.service }} + + {{ $component | title }}: +{{- if eq $svc.type "NodePort" }} + kubectl get svc -n {{ $.Release.Namespace }} {{ $name }} -o jsonpath='{.spec.ports[?(@.name=="{{ $portName }}")].nodePort}{"\n"}' + kubectl get nodes -o wide + Open http://:{{ if eq $component "server" }}/versions{{ end }} +{{- else if eq $svc.type "LoadBalancer" }} + kubectl get svc -n {{ $.Release.Namespace }} {{ $name }} -o wide + kubectl get svc -n {{ $.Release.Namespace }} {{ $name }} -o jsonpath='{.status.loadBalancer.ingress}{"\n"}' + Once an address is assigned, use http://:{{ $port }}{{ if eq $component "server" }}/versions{{ end }} +{{- else }} + In-cluster address: http://{{ $name }}.{{ $.Release.Namespace }}.svc:{{ $port }} + ClusterIP is reachable inside the cluster; use port-forward below from your workstation. +{{- end }} +{{- $ingress := get $cfg "ingress" | default dict }} +{{- if get $ingress "enabled" }} + Ingress address/status: kubectl get ingress -n {{ $.Release.Namespace }} {{ $name }} -o wide +{{- $tlsHosts := list }} +{{- range (get $ingress "tls" | default list) }} +{{- range (.hosts | default list) }}{{ $tlsHosts = append $tlsHosts . }}{{ end }} +{{- end }} +{{- range (get $ingress "hosts" | default (list (dict "host" ""))) }} +{{- $host := .host | default "" }} +{{- $scheme := "http" }} +{{- if or (has $host $tlsHosts) (and (empty $host) (not (empty (get $ingress "tls")))) }}{{ $scheme = "https" }}{{ end }} + {{ $scheme }}://{{ $host | default "" }}{{ (first (.paths | default (list (dict "path" "/")))).path | default "/" }} +{{- end }} +{{- end }} + Port-forward: kubectl port-forward -n {{ $.Release.Namespace }} svc/{{ $name }} {{ $port }}:{{ $port }} + Local address: http://127.0.0.1:{{ $port }} +{{- end }} +{{- end }} + +Credentials: + + PD_SECRET="$(kubectl get secret -n {{ .Release.Namespace }} {{ include "hugegraph.pd.authSecretName" . }} -o jsonpath='{.data.{{ include "hugegraph.pd.authSecretKey" . | replace "." "\\." }}}' | base64 --decode)" +{{- if .Values.server.auth.enabled }} + PASSWORD="$(kubectl get secret -n {{ .Release.Namespace }} {{ include "hugegraph.server.authSecretName" . }} -o jsonpath='{.data.{{ include "hugegraph.server.authSecretKey" . | replace "." "\\." }}}' | base64 --decode)" + curl --user "admin:${PASSWORD}" http:///versions + +The admin Secret contains the initial password. Changing it does not reset an +existing account; see helm/hugegraph/docs/operations.md#credentials for account password changes and Secret synchronization. +{{- else }} + Server authentication is disabled. +{{- end }} +{{- if and $hubbleEnabled (not .Values.server.auth.enabled) }} + Current Hubble images require Server authentication to complete login. + Enable server.auth.enabled or set hubble.enabled=false for API-only deployment. +{{- end }} +{{- if and $hubbleEnabled (not .Values.hubble.persistence.enabled) }} + Hubble connection metadata uses temporary storage and is lost on Pod replacement. +{{- end }} +{{- $pdAuth := .Values.pd.auth }} +{{- $srvAuth := .Values.server.auth }} +{{- $generated := list }} +{{- if and (not $pdAuth.existingSecret) (not $pdAuth.value) }}{{ $generated = append $generated "pd.auth" }}{{ end }} +{{- if and $srvAuth.enabled (not $srvAuth.admin.existingSecret) (not $srvAuth.admin.password) }}{{ $generated = append $generated "server.auth.admin" }}{{ end }} +{{- if and $srvAuth.enabled (not $srvAuth.token.existingSecret) (not $srvAuth.token.value) }}{{ $generated = append $generated "server.auth.token" }}{{ end }} +{{- if $generated }} + +Chart-generated credentials are in use: {{ join ", " $generated }}. +For GitOps/template-only pipelines use fixed credentials or existingSecret; +see helm/hugegraph/examples/README.md#gitops-credentials. +{{- end }} +{{- if .Values.networkPolicy.enabled }} + +NetworkPolicy is enabled. Add application or monitoring clients through +networkPolicy..extraIngress; enforcement depends on the cluster network plugin. +{{- range $component := list "server" "hubble" "pd" }} +{{- if or (ne $component "hubble") $hubbleEnabled }} +{{- $cfg := get $.Values $component }} +{{- $ingress := get $cfg "ingress" | default dict }} +{{- $exposed := or (has $cfg.service.type (list "NodePort" "LoadBalancer")) (get $ingress "enabled" | default false) (not (empty (get $cfg.service "externalIPs"))) }} +{{- $policy := get $.Values.networkPolicy $component | default dict }} +{{- if and $exposed (empty (get $policy "extraIngress")) }} + {{ $component | title }} has an external entry, but NetworkPolicy has no external ingress rule. + Add networkPolicy.{{ $component }}.extraIngress for the client/controller, or disable networkPolicy.enabled. + An explicit [{}] ingress rule allows all sources and ports; choose the rule for your network. +{{- end }} +{{- end }} +{{- end }} +{{- $hubblePolicy := get $.Values.networkPolicy "hubble" | default dict }} +{{- if and $.Values.server.advertiseUrl (empty (get $hubblePolicy "extraEgress")) $hubbleEnabled (eq $.Values.hubble.mode "pd") }} + For an external server.advertiseUrl, also add networkPolicy.hubble.extraEgress for that address. +{{- end }} +{{- end }} +{{- if .Values.server.advertiseUrl }} + +Server registers {{ .Values.server.advertiseUrl }} with PD for external discovery. +A standalone Hubble also needs a reachable PD endpoint; see helm/hugegraph/docs/operations.md#networking-and-hubble. +{{- end }} + +Verify all Server Gremlin endpoints: + + helm test {{ .Release.Name }} --namespace {{ .Release.Namespace }} --logs --timeout 5m + +Run this after upgrades too. Ready probes alone do not verify Gremlin binding. +{{- if $hubbleEnabled }} +Also verify a fresh Hubble login and graph query; the hook checks Server, not UI login. +{{- end }} diff --git a/helm/hugegraph/templates/_helpers.tpl b/helm/hugegraph/templates/_helpers.tpl new file mode 100644 index 0000000000..e31813e60e --- /dev/null +++ b/helm/hugegraph/templates/_helpers.tpl @@ -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. +# + +{{/* Resolve inherited defaults once. Never use default/or for explicit booleans. */}} +{{- define "hugegraph.resolveValues" -}} +{{- if not (get .Values "__hugegraphResolved") -}} +{{- $_ := set .Values "__hugegraphResolved" true -}} +{{- $legacy := not (hasKey .Values "mode") -}} +{{- if not (hasKey .Values "hubble") -}} +{{- $_ := set .Values "hubble" (dict "enabled" false "service" dict "persistence" dict) -}} +{{- end -}} +{{- $strict := eq (.Values.mode | default "easy") "strict" -}} +{{- $serviceType := ternary "ClusterIP" "NodePort" (or $strict $legacy) -}} +{{- range $comp := list "server" "hubble" -}} +{{- $component := get $.Values $comp -}} +{{- $svc := get $component "service" | default dict -}} +{{- $_ := set $component "service" $svc -}} +{{- $typeInherited := or (not (hasKey $svc "type")) (eq (get $svc "type" | toString) "") -}} +{{- $_ := set $component "__serviceTypeInherited" $typeInherited -}} +{{- if $typeInherited -}}{{- $_ := set $svc "type" $serviceType -}}{{- end -}} +{{- end -}} +{{- $np := .Values.networkPolicy | default dict -}} +{{- $_ := set .Values "networkPolicy" $np -}} +{{- if or (not (hasKey $np "enabled")) (eq (get $np "enabled" | toString) "") -}}{{- $_ := set $np "enabled" $strict -}}{{- end -}} +{{- $persistent := true -}} +{{- if hasKey (.Values.persistence | default dict) "enabled" -}}{{- $persistent = .Values.persistence.enabled -}}{{- end -}} +{{- range $comp := list "pd" "store" "hubble" -}} +{{- $component := get $.Values $comp -}} +{{- $key := ternary "persistence" "storage" (eq $comp "hubble") -}} +{{- $storage := get $component $key | default dict -}} +{{- $_ := set $component $key $storage -}} +{{- if or (not (hasKey $storage "enabled")) (eq (get $storage "enabled" | toString) "") -}}{{- $_ := set $storage "enabled" $persistent -}}{{- end -}} +{{- end -}} +{{- $registry := trimSuffix "/" (get (.Values.global | default dict) "imageRegistry" | default "") -}} +{{- if contains "://" $registry -}}{{- fail "global.imageRegistry must be a registry host/path without http:// or https://" -}}{{- end -}} +{{- if $registry -}} +{{- range $comp := list "pd" "store" "server" "hubble" -}} +{{- $image := get (get $.Values $comp) "image" | default dict -}} +{{- if $image -}} +{{- $_ := set $image "repository" (include "hugegraph.registryImage" (dict "reference" $image.repository "registry" $registry)) -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- range $comp := list "store" "server" -}} +{{- $values := get $.Values $comp -}} +{{- $override := get $values "waitImage" | default "" -}} +{{- $componentImage := include "hugegraph.image" (dict "image" $values.image "appVersion" $.Chart.AppVersion) -}} +{{- if $override -}} +{{- $_ := set $values "waitImage" (include "hugegraph.registryImage" (dict "reference" $override "registry" $registry)) -}} +{{- else -}} +{{- $_ := set $values "waitImage" $componentImage -}} +{{- end -}} +{{- $_ := set $values "__waitImageInherited" (eq $values.waitImage $componentImage) -}} +{{- end -}} +{{- $server := .Values.server -}} +{{- if not (hasKey $server "storageWaitTimeoutSeconds") -}}{{- $_ := set $server "storageWaitTimeoutSeconds" 300 -}}{{- end -}} +{{- $startup := $server.probes.startup -}} +{{- if $legacy -}} +{{- $period := int $startup.periodSeconds -}} +{{- $budget := add (int $server.storageWaitTimeoutSeconds) 150 -}} +{{- $_ := set $startup "failureThreshold" (max (int $startup.failureThreshold) (add (div (add (sub $budget 1) $period) $period) 1)) -}} +{{- end -}} +{{- if or (not (hasKey $startup "failureThreshold")) (eq (get $startup "failureThreshold" | toString) "") -}} +{{- $period := int $startup.periodSeconds -}} +{{- $budget := add (int $server.storageWaitTimeoutSeconds) 150 -}} +{{- $_ := set $startup "failureThreshold" (add (div (add (sub $budget 1) $period) $period) 1) -}} +{{- end -}} +{{- end -}} +{{- end }} + +{{/* A qualified component reference overrides the global registry. */}} +{{- define "hugegraph.registryImage" -}} +{{- $reference := .reference -}} +{{- $first := first (splitList "/" $reference) -}} +{{- $qualified := and (contains "/" $reference) (or (contains "." $first) (contains ":" $first) (eq $first "localhost")) -}} +{{- if and .registry (not $qualified) -}}{{- printf "%s/%s" .registry $reference -}}{{- else -}}{{- $reference -}}{{- end -}} +{{- end }} + +{{/* +Keep the application identity stable when the distribution chart is renamed. +The OCI artifact is hugegraph-helm; Kubernetes resources still use hugegraph. +*/}} +{{- define "hugegraph.name" -}} +{{- default "hugegraph" .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "hugegraph.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default "hugegraph" .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{- define "hugegraph.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "hugegraph.labels" -}} +helm.sh/chart: {{ include "hugegraph.chart" . }} +{{ include "hugegraph.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "hugegraph.selectorLabels" -}} +app.kubernetes.io/name: {{ include "hugegraph.name" . | quote }} +app.kubernetes.io/instance: {{ .Release.Name | quote }} +{{- end }} + +{{- define "hugegraph.pd.name" -}} +{{- printf "%s-pd" (include "hugegraph.fullname" . | trunc 57 | trimSuffix "-") }} +{{- end }} + +{{- define "hugegraph.pd.clientName" -}} +{{- printf "%s-pd-client" (include "hugegraph.fullname" . | trunc 53 | trimSuffix "-") }} +{{- end }} + +{{- define "hugegraph.store.name" -}} +{{- printf "%s-store" (include "hugegraph.fullname" . | trunc 54 | trimSuffix "-") }} +{{- end }} + +{{- define "hugegraph.server.name" -}} +{{- printf "%s-server" (include "hugegraph.fullname" . | trunc 56 | trimSuffix "-") }} +{{- end }} + +{{- define "hugegraph.server.headlessName" -}} +{{- printf "%s-server-headless" (include "hugegraph.fullname" . | trunc 47 | trimSuffix "-") }} +{{- end }} + +{{- define "hugegraph.hubble.name" -}} +{{- printf "%s-hubble" (include "hugegraph.fullname" . | trunc 56 | trimSuffix "-") }} +{{- end }} + +{{- define "hugegraph.hubble.dataName" -}} +{{- printf "%s-hubble-data" (include "hugegraph.fullname" . | trunc 51 | trimSuffix "-") }} +{{- end }} + +{{- define "hugegraph.test.name" -}} +{{- printf "%s-test-connection" (include "hugegraph.fullname" . | trunc 47 | trimSuffix "-") }} +{{- end }} + +{{/* +Resolve the Server authentication Secret. A user-provided Secret always wins; +otherwise use a stable chart-managed name so the generated Secret can survive +uninstall and be reused by a later install of the same release. +*/}} +{{- define "hugegraph.server.authSecretName" -}} +{{- $auth := get .Values.server "auth" | default dict -}} +{{- $admin := get $auth "admin" | default dict -}} +{{- $existingSecret := get $admin "existingSecret" | default "" -}} +{{- if $existingSecret -}} +{{- $existingSecret -}} +{{- else -}} +{{- printf "%s-admin" (.Release.Name | trunc 55 | trimSuffix "-") -}} +{{- end -}} +{{- end }} + +{{- define "hugegraph.server.authSecretKey" -}} +{{- $auth := get .Values.server "auth" | default dict -}} +{{- $admin := get $auth "admin" | default dict -}} +{{- get $admin "key" | default "password" -}} +{{- end }} + +{{/* +Return the chart-managed admin password (base64). Inline admin.password wins +on first write; otherwise lookup keeps upgrades from rotating a generated +credential. Never used for an external existingSecret. +*/}} +{{- define "hugegraph.server.authSecretPassword" -}} +{{- $auth := get .Values.server "auth" | default dict -}} +{{- $admin := get $auth "admin" | default dict -}} +{{- $password := get $admin "password" | default "" -}} +{{- if $password -}} +{{- $password | b64enc -}} +{{- else -}} +{{- $key := include "hugegraph.server.authSecretKey" . -}} +{{- $secret := lookup "v1" "Secret" .Release.Namespace (include "hugegraph.server.authSecretName" .) -}} +{{- if and $secret (hasKey $secret "data") (hasKey (get $secret "data") $key) -}} +{{- get (get $secret "data") $key -}} +{{- else -}} +{{- include "hugegraph.generatedCredential" (dict "root" . "name" "server-admin") | b64enc -}} +{{- end -}} +{{- end -}} +{{- end }} + +{{/* +One random credential per render, by name. randAlphaNum returns a different +value on every call, and the Secret template and the rollout checksum both +need the value the install is about to write, so the first call generates it +and later calls read it back from a scratch map kept on .Values for the +duration of this render (it is not part of the stored release values; the +next render finds the Secret through lookup instead). +*/}} +{{- define "hugegraph.generatedCredential" -}} +{{- if not (hasKey .root.Values "__generatedCredentials") -}}{{- $_ := set .root.Values "__generatedCredentials" dict -}}{{- end -}} +{{- $memo := get .root.Values "__generatedCredentials" -}} +{{- if not (hasKey $memo .name) -}}{{- $_ := set $memo .name (randAlphaNum 32) -}}{{- end -}} +{{- get $memo .name -}} +{{- end }} + +{{/* +Resolve the JWT signing Secret. User-provided token.existingSecret wins; +otherwise use a stable chart-managed name so every Server replica shares one key. +*/}} +{{- define "hugegraph.server.authTokenSecretName" -}} +{{- $auth := get .Values.server "auth" | default dict -}} +{{- $token := get $auth "token" | default dict -}} +{{- $existing := get $token "existingSecret" | default "" -}} +{{- if $existing -}} +{{- $existing -}} +{{- else -}} +{{- printf "%s-auth-token" (.Release.Name | trunc 51 | trimSuffix "-") -}} +{{- end -}} +{{- end }} + +{{- define "hugegraph.server.authTokenSecretKey" -}} +{{- $auth := get .Values.server "auth" | default dict -}} +{{- $token := get $auth "token" | default dict -}} +{{- get $token "key" | default "token_secret" -}} +{{- end }} + +{{/* +Return the chart-managed JWT signing secret (base64). Inline token.value wins +on first write; otherwise lookup keeps multi-replica pods and upgrades on the +same signing key. +*/}} +{{- define "hugegraph.server.authTokenSecretValue" -}} +{{- $auth := get .Values.server "auth" | default dict -}} +{{- $token := get $auth "token" | default dict -}} +{{- $value := get $token "value" | default "" -}} +{{- if $value -}} +{{- $value | b64enc -}} +{{- else -}} +{{- $name := include "hugegraph.server.authTokenSecretName" . -}} +{{- $key := include "hugegraph.server.authTokenSecretKey" . -}} +{{- $secret := lookup "v1" "Secret" .Release.Namespace $name -}} +{{- if and $secret (hasKey $secret "data") (hasKey (get $secret "data") $key) -}} +{{- get (get $secret "data") $key -}} +{{- else -}} +{{- include "hugegraph.generatedCredential" (dict "root" . "name" "server-token") | b64enc -}} +{{- end -}} +{{- end -}} +{{- end }} + +{{/* +Resolve the PD REST auth Secret. User-provided pd.auth.existingSecret wins; +otherwise a stable chart-managed name shared by PD, the Server storage wait +and Hubble. +*/}} +{{- define "hugegraph.pd.authSecretName" -}} +{{- $auth := get .Values.pd "auth" | default dict -}} +{{- $existing := get $auth "existingSecret" | default "" -}} +{{- if $existing -}} +{{- $existing -}} +{{- else -}} +{{- printf "%s-pd-auth" (.Release.Name | trunc 55 | trimSuffix "-") -}} +{{- end -}} +{{- end }} + +{{- define "hugegraph.pd.authSecretKey" -}} +{{- $auth := get .Values.pd "auth" | default dict -}} +{{- get $auth "key" | default "secret-key" -}} +{{- end }} + +{{/* +Return the chart-managed PD REST secret (base64). Inline pd.auth.value wins +on first write; otherwise lookup keeps every PD, Server and Hubble Pod, and +every upgrade, on the same secret. +*/}} +{{- define "hugegraph.pd.authSecretValue" -}} +{{- $auth := get .Values.pd "auth" | default dict -}} +{{- $value := get $auth "value" | default "" -}} +{{- if $value -}} +{{- $value | b64enc -}} +{{- else -}} +{{- $name := include "hugegraph.pd.authSecretName" . -}} +{{- $key := include "hugegraph.pd.authSecretKey" . -}} +{{- $secret := lookup "v1" "Secret" .Release.Namespace $name -}} +{{- if and $secret (hasKey $secret "data") (hasKey (get $secret "data") $key) -}} +{{- get (get $secret "data") $key -}} +{{- else -}} +{{- include "hugegraph.generatedCredential" (dict "root" . "name" "pd-auth") | b64enc -}} +{{- end -}} +{{- end -}} +{{- end }} + +{{/* +Checksum for the PD, Server and Hubble pod templates so rotating the PD REST +Secret rolls the Pods that read it. It hashes the Secret name, its key, and +one revision input chosen by where the credential comes from. + +An active inline value is known at render time, so its digest is the +revision: it changes exactly once, on the upgrade that rotates it. Mixing the +live resourceVersion into that case would roll the Pods a second time on the +next no-change upgrade, once the rotated Secret had been applied and its +resourceVersion moved. + +A chart-generated Secret contributes the digest of its value. At install +that is the value the Secret template is about to write (generated once per +render, see hugegraph.generatedCredential); on every later render it is the +live Secret's data, so the first no-change upgrade after an install renders +the same checksum and rolls nothing. The value is a 32-character random, so +its digest in a Pod annotation discloses nothing usable. The alternative, the +live resourceVersion, differs between install (no Secret yet) and the first +upgrade, which rolled PD and Server together on a no-change upgrade and let +a Server start against a PD member mid-restart (apache/hugegraph#3228). + +An operator-supplied existingSecret keeps the live resourceVersion as its +revision: its value may be weak enough for a digest to be worth attacking, +and that Secret exists before the install, so install and upgrade already +agree. Template-only renders (helm template, GitOps renderers) see no live +Secret and generate a fresh value on every render; they are unsupported +with chart-generated credentials, see README, Template-only pipelines. +*/}} +{{- define "hugegraph.pd.authChecksum" -}} +{{- $parts := list (include "hugegraph.pd.authSecretName" .) (include "hugegraph.pd.authSecretKey" .) -}} +{{- $pdAuthCfg := get .Values.pd "auth" | default dict -}} +{{- $inline := get $pdAuthCfg "value" | default "" -}} +{{- $existing := get $pdAuthCfg "existingSecret" | default "" -}} +{{- if $existing -}} +{{- $secret := lookup "v1" "Secret" .Release.Namespace $existing -}} +{{- if $secret -}}{{- $parts = append $parts (dig "metadata" "resourceVersion" "" $secret) -}}{{- end -}} +{{- else if $inline -}} +{{- $parts = append $parts (sha256sum $inline) -}} +{{- else -}} +{{- $parts = append $parts (include "hugegraph.pd.authSecretValue" . | b64dec | sha256sum) -}} +{{- end -}} +{{- join "|" $parts | sha256sum -}} +{{- end }} + +{{/* +Path the PD startup and liveness probes hit. + +/v1/health stays 200 during elections and quorum loss, and images with the +apache/hugegraph#3222 fix answer 503 on fatal raft errors or shutdown. Losing +leadership alone must not restart followers and turn an election into a +rolling outage. Readiness carries the leader-availability signal instead. + +A single PD has no election to lose. There, a PD that steps down and cannot +recover, as after a failed snapshot on a full disk +(apache/hugegraph#3222), keeps answering /v1/health forever on older images. +For compatibility with those images, the default moves to /v1/ready when +pd.replicas is 1. + +The startup probe follows this path as well. Kubernetes suppresses liveness +until the startup probe succeeds, so leaving startup on /v1/health would give +a single PD only the liveness budget (60 s by default) to reach raft +readiness after a restart, and a slow log replay would crash-loop instead of +booting. Following the same path puts that window inside the startup budget +(300 s by default) instead. + +Setting pd.livenessPath overrides the choice in both places. Drop the +single-PD readiness derivation once the minimum supported image includes +the /v1/health fatal raft error fix. +*/}} +{{- define "hugegraph.pd.livenessPath" -}} +{{- $explicit := get .Values.pd "livenessPath" | default "" -}} +{{- if $explicit -}} +{{- $explicit -}} +{{- else if eq (int .Values.pd.replicas) 1 -}} +/v1/ready +{{- else -}} +/v1/health +{{- end -}} +{{- end }} + +{{/* +PD Raft peers list: pod-0.svc.ns.svc:8610,... +Uses short headless DNS (cluster.local optional) resolvable inside the namespace. +*/}} +{{- define "hugegraph.pd.raftPeersList" -}} +{{- $peers := list -}} +{{- $replicas := int .Values.pd.replicas -}} +{{- $name := include "hugegraph.pd.name" . -}} +{{- $ns := .Release.Namespace -}} +{{- $port := int .Values.pd.ports.raft -}} +{{- range $i := until $replicas -}} + {{- $peers = append $peers (printf "%s-%d.%s.%s.svc:%d" $name $i $name $ns $port) -}} +{{- end -}} +{{- join "," $peers -}} +{{- end }} + +{{/* +PD gRPC peers for Store/Server. +*/}} +{{- define "hugegraph.pd.grpcPeersList" -}} +{{- $peers := list -}} +{{- $replicas := int .Values.pd.replicas -}} +{{- $name := include "hugegraph.pd.name" . -}} +{{- $ns := .Release.Namespace -}} +{{- $port := int .Values.pd.ports.grpc -}} +{{- range $i := until $replicas -}} + {{- $peers = append $peers (printf "%s-%d.%s.%s.svc:%d" $name $i $name $ns $port) -}} +{{- end -}} +{{- join "," $peers -}} +{{- end }} + +{{/* +PD REST endpoints for Server storage-readiness checks. +*/}} +{{- define "hugegraph.pd.restPeersList" -}} +{{- $peers := list -}} +{{- $replicas := int .Values.pd.replicas -}} +{{- $name := include "hugegraph.pd.name" . -}} +{{- $ns := .Release.Namespace -}} +{{- $port := int .Values.pd.ports.rest -}} +{{- range $i := until $replicas -}} + {{- $peers = append $peers (printf "%s-%d.%s.%s.svc:%d" $name $i $name $ns $port) -}} +{{- end -}} +{{- join "," $peers -}} +{{- end }} + +{{/* +Checksum for the Server pod template so rotating the referenced auth Secrets +rolls Server pods. The admin and token credentials pick their revision input +independently, by source, for the reasons given on hugegraph.pd.authChecksum: +an active inline value and a chart-generated Secret contribute the digest of +their value (so a rotation rolls Server exactly once, and the first no-change +upgrade after an install rolls nothing); an operator-supplied existingSecret +contributes its live metadata.resourceVersion, and its out-of-band rotation +applies on the next `helm upgrade`. Template-only renders are unsupported +with chart-generated credentials, see README, Template-only pipelines. +*/}} +{{- define "hugegraph.server.authChecksum" -}} +{{- $parts := list (include "hugegraph.server.authSecretName" .) (include "hugegraph.server.authSecretKey" .) (include "hugegraph.server.authTokenSecretName" .) (include "hugegraph.server.authTokenSecretKey" .) -}} +{{- $srvAuth := get .Values.server "auth" | default dict -}} +{{- $adminCfg := get $srvAuth "admin" | default dict -}} +{{- $inlineAdmin := get $adminCfg "password" | default "" -}} +{{- $existingAdmin := get $adminCfg "existingSecret" | default "" -}} +{{- $tokenCfg := get $srvAuth "token" | default dict -}} +{{- $inlineToken := get $tokenCfg "value" | default "" -}} +{{- $existingToken := get $tokenCfg "existingSecret" | default "" -}} +{{- if $existingAdmin -}} +{{- $admin := lookup "v1" "Secret" .Release.Namespace $existingAdmin -}} +{{- if $admin -}}{{- $parts = append $parts (dig "metadata" "resourceVersion" "" $admin) -}}{{- end -}} +{{- else if $inlineAdmin -}} +{{- $parts = append $parts (sha256sum $inlineAdmin) -}} +{{- else -}} +{{- $parts = append $parts (include "hugegraph.server.authSecretPassword" . | b64dec | sha256sum) -}} +{{- end -}} +{{- if $existingToken -}} +{{- $token := lookup "v1" "Secret" .Release.Namespace $existingToken -}} +{{- if $token -}}{{- $parts = append $parts (dig "metadata" "resourceVersion" "" $token) -}}{{- end -}} +{{- else if $inlineToken -}} +{{- $parts = append $parts (sha256sum $inlineToken) -}} +{{- else -}} +{{- $parts = append $parts (include "hugegraph.server.authTokenSecretValue" . | b64dec | sha256sum) -}} +{{- end -}} +{{- join "|" $parts | sha256sum -}} +{{- end }} + +{{/* +Initial store list for PD bootstrap: store-0.svc.ns.svc:8500,... +*/}} +{{- define "hugegraph.store.initialStoreList" -}} +{{- $peers := list -}} +{{- $replicas := int .Values.store.replicas -}} +{{- $name := include "hugegraph.store.name" . -}} +{{- $ns := .Release.Namespace -}} +{{- $port := int .Values.store.ports.grpc -}} +{{- range $i := until $replicas -}} + {{- $peers = append $peers (printf "%s-%d.%s.%s.svc:%d" $name $i $name $ns $port) -}} +{{- end -}} +{{- join "," $peers -}} +{{- end }} + +{{/* +First store REST endpoint for STORE_REST / wait-partition. +*/}} +{{- define "hugegraph.store.restPrimary" -}} +{{- $name := include "hugegraph.store.name" . -}} +{{- $ns := .Release.Namespace -}} +{{- printf "%s-0.%s.%s.svc:%d" $name $name $ns (int .Values.store.ports.rest) -}} +{{- end }} + +{{/* +Server REST URL reached through the client Service. +*/}} +{{- define "hugegraph.server.clientUrl" -}} +{{- printf "http://%s.%s.svc:%d" (include "hugegraph.server.name" .) .Release.Namespace (int .Values.server.port) -}} +{{- end }} + +{{/* +URL registered with PD (server.urls_to_pd / HG_SERVER_URLS_TO_PD). +server.advertiseUrl wins when set so outside PD-mode Hubble receives a reachable address; otherwise each Server Pod announces its own Pod IP so PD discovery preserves the replica list for in-cluster clients. +*/}} +{{- define "hugegraph.server.urlsToPd" -}} +{{- $advertise := trim (default "" .Values.server.advertiseUrl) -}} +{{- if $advertise -}} +{{- $advertise -}} +{{- else -}} +{{- printf "http://$(POD_IP):%d" (int .Values.server.port) -}} +{{- end -}} +{{- end }} + +{{/* +PD REST endpoint reached through the client Service, for Hubble's pd.server. +*/}} +{{- define "hugegraph.pd.restClientEndpoint" -}} +{{- printf "%s.%s.svc:%d" (include "hugegraph.pd.clientName" .) .Release.Namespace (int .Values.pd.ports.rest) -}} +{{- end }} + +{{/* +Store REST origins in Hubble's bracketed allow-list form: +[http://store-0.svc.ns.svc:8520,...] +*/}} +{{- define "hugegraph.store.restOriginsList" -}} +{{- $origins := list -}} +{{- $replicas := int .Values.store.replicas -}} +{{- $name := include "hugegraph.store.name" . -}} +{{- $ns := .Release.Namespace -}} +{{- $port := int .Values.store.ports.rest -}} +{{- range $i := until $replicas -}} + {{- $origins = append $origins (printf "http://%s-%d.%s.%s.svc:%d" $name $i $name $ns $port) -}} +{{- end -}} +{{- printf "[%s]" (join "," $origins) -}} +{{- end }} + +{{/* +Quorum size: floor(replicas/2)+1 +*/}} +{{- define "hugegraph.pd.quorum" -}} +{{- add (div (int .Values.pd.replicas) 2) 1 -}} +{{- end }} + +{{/* +Render JAVA_OPTS only when explicitly configured. An empty value preserves the +image entrypoint's existing automatic JVM sizing behavior. +*/}} +{{- define "hugegraph.javaOptsEnv" -}} +{{- $javaOpts := default "" . -}} +{{- if ne (trim $javaOpts) "" -}} +- name: JAVA_OPTS + value: {{ $javaOpts | quote }} +{{- end -}} +{{- end }} + +{{/* +String form of a possibly-absent scalar value, preserving zero. sprig's +`default` treats 0 as unset, which would let a zero slip past the named +validation below, so absence is detected explicitly instead. Numbers from a +values file arrive as float64, whose toString switches to scientific +notation at 1e6 or higher (1000000 becomes "1e+06"), so integral float64 +values are formatted without an exponent. Non-integral floats keep their +raw form on purpose: the schema already rejects them, and the raw form +fails the named validation instead of being silently rounded. +*/}} +{{- define "hugegraph.optionalScalar" -}} +{{- if not (kindIs "invalid" .) -}} +{{- if and (kindIs "float64" .) (eq (floor .) .) -}}{{- printf "%.0f" . -}}{{- else -}}{{- trim (toString .) -}}{{- end -}} +{{- end -}} +{{- end }} + +{{/* +Effective PD JAVA_OPTS: chart-derived -D system properties, then pd.javaOpts. + +The -D route is the grounded override mechanism: the PD image's +docker-entrypoint.sh forwards JAVA_OPTS via `-j` into +bin/start-hugegraph-pd.sh, which places it on the java command line ahead of +-Dspring.config.location, and Spring system properties outrank the shipped +conf/application.yml (which pins partition.default-shard-count to 1). + +The shard count is always derived, so PD Pods always carry a JAVA_OPTS +variable, which shadows the PD image's `ENV JAVA_OPTS` default +(-XX:MaxRAMPercentage=50, -XX:+UseContainerSupport, -XshowSettings:vm). +That is acceptable: the start script always computes explicit -Xms/-Xmx +heap flags when the separate JAVA_OPTIONS variable is unset, which makes +MaxRAMPercentage moot, and only the -XshowSettings:vm startup diagnostics +are lost. + +JVM auto-sizing is preserved, verified against the PD dist start script: +`-j` lands in USER_OPTION, while the automatic heap sizing branch is gated on +the separate JAVA_OPTIONS variable and appends USER_OPTION after the computed +-Xms/-Xmx flags. A JAVA_OPTS holding only -D flags therefore still gets +automatic heap sizing, and heap flags in pd.javaOpts land later on the +command line, so they win. The derived -D flags come first for the same +reason: an explicit duplicate in pd.javaOpts overrides them. + +Both -D properties seed PD's persisted config at first bootstrap only: +ConfigService.loadConfig persists them when no stored config exists, and +every leader change re-reads the stored values (updatePDConfig), so on an +initialized cluster the flags are inert and the authoritative values live +in PD metadata, changeable only through PD's own config API. PD reconciles +existing shard groups toward the stored value when a partition patrol is +triggered (TaskScheduleService reallocShards). The empty-value derivation +is 3 when store.replicas is at least 3, else 1, because PD clamps a shard +count of 2 to 1 (two shards cannot elect a leader) and its config API +accepts only odd values. store-max-shard-count is rendered only when set, +keeping the image default. All lookups tolerate absent keys so releases +stored before these values existed keep rendering under --reuse-values. + +raft.ip-whitelist.enabled is always rendered, default false: PD resolves its +raft peer allowlist once at boot, which under Kubernetes blocks peers whose +pod IPs were unpublished at that moment or change later, so the switch is +off in-cluster per the upstream design and k8s auth owns that layer. Images +without the property ignore the flag. Set pd.raftIpWhitelistEnabled=true to +restore the image default. + +raft.rpc-timeout is a plain runtime property, applied on every start rather +than seeded at bootstrap; empty preserves the image default. It bounds raft +requests between PDs, and on images that wait for a raft ReadIndex before +they read an id counter, that wait. Images that have raft.rpc-connect-timeout +use that option for the Ping that opens a connection to a peer (default 1000, +set through pd.javaOpts); it does not bound requests on a connection that is +already open. On older images raft.rpc-timeout is the timeout of that Ping as +well, which is what a leader election waited on. +*/}} +{{- define "hugegraph.pd.effectiveJavaOpts" -}} +{{- $pd := .Values.pd -}} +{{- $partition := get $pd "partition" | default dict -}} +{{- $flags := list -}} +{{- $shardCount := include "hugegraph.optionalScalar" (get $partition "defaultShardCount") -}} +{{- if eq $shardCount "" -}} +{{- $shardCount = ternary "3" "1" (ge (int .Values.store.replicas) 3) -}} +{{- end -}} +{{- $flags = append $flags (printf "-Dpartition.default-shard-count=%s" $shardCount) -}} +{{- $maxShard := include "hugegraph.optionalScalar" (get $partition "storeMaxShardCount") -}} +{{- if ne $maxShard "" -}} +{{- $flags = append $flags (printf "-Dpartition.store-max-shard-count=%s" $maxShard) -}} +{{- end -}} +{{- $ipWhitelist := ternary "true" "false" (eq (get $pd "raftIpWhitelistEnabled" | toString) "true") -}} +{{- $flags = append $flags (printf "-Draft.ip-whitelist.enabled=%s" $ipWhitelist) -}} +{{- $rpcTimeout := include "hugegraph.optionalScalar" (get $pd "raftRpcTimeoutMs") -}} +{{- if ne $rpcTimeout "" -}} +{{- $flags = append $flags (printf "-Draft.rpc-timeout=%s" $rpcTimeout) -}} +{{- end -}} +{{- $userOpts := trim (get $pd "javaOpts" | default "") -}} +{{- if ne $userOpts "" -}} +{{- $flags = append $flags $userOpts -}} +{{- end -}} +{{- join " " $flags -}} +{{- end }} + +{{/* +Unspecified probes reserve storageWaitTimeoutSeconds plus 150 seconds. +An explicitly configured threshold is preserved and checked by validateValues. +Kubelet may probe immediately: the budget is (failureThreshold - 1) * periodSeconds. +*/}} +{{- define "hugegraph.server.startupFailureThreshold" -}} +{{- .Values.server.probes.startup.failureThreshold -}} +{{- end }} + +{{/* +Seconds the chart gives the Server image to finish starting, passed as +HG_SERVER_STARTUP_TIMEOUT_S. The image defaults that to 120 seconds, which is +shorter than the storage wait alone, so a Server still coming up kills itself +before Kubernetes has given up on it. The value therefore tracks the startup +probe's guaranteed alive time, (failureThreshold - 1) * periodSeconds, +because the first probe can fail immediately, minus the configured storage +wait the entrypoint runs before the start command, so the start command and +kubelet give up together instead of the image outliving the probe. Floored +at the image's own 120-second default, and capped at the entrypoint's 86400 +maximum rather than rendered into a Pod that refuses to start. +*/}} +{{- define "hugegraph.server.startupProbeBudget" -}} +{{- $startup := .Values.server.probes.startup -}} +{{- add (int ($startup.initialDelaySeconds | default 0)) (mul (sub (int $startup.failureThreshold) 1) (int $startup.periodSeconds)) -}} +{{- end }} + +{{- define "hugegraph.server.startupTimeoutSeconds" -}} +{{- $budget := include "hugegraph.server.startupProbeBudget" . | int -}} +{{- min 86400 (max 120 (sub $budget (int .Values.server.storageWaitTimeoutSeconds))) -}} +{{- end }} + +{{/* +Optional probe tunables, emitted only when explicitly set. Kubernetes defaults +timeoutSeconds to 1 second, which a garbage-collection pause can exceed on a +loaded Server; operators need a supported way to raise it without forking the +chart. Only explicitly configured fields are rendered. +*/}} +{{- define "hugegraph.probeTuning" -}} +{{- if hasKey . "timeoutSeconds" }} +timeoutSeconds: {{ .timeoutSeconds }} +{{- end }} +{{- if hasKey . "initialDelaySeconds" }} +initialDelaySeconds: {{ .initialDelaySeconds }} +{{- end }} +{{- if hasKey . "successThreshold" }} +successThreshold: {{ .successThreshold }} +{{- end }} +{{- end }} + +{{/* +Resolve the ServiceAccount name for a component: an explicit name wins, +otherwise the generated one when create is true, otherwise "default". +*/}} +{{- define "hugegraph.serviceAccountName" -}} +{{- $sa := get .component "serviceAccount" | default dict -}} +{{- if get $sa "name" -}} +{{- get $sa "name" -}} +{{- else if (get $sa "create" | default false) -}} +{{- .name -}} +{{- else -}} +default +{{- end -}} +{{- end }} + +{{/* +The minimum Server replica count that a PDB must remain valid against. +*/}} +{{- define "hugegraph.server.replicaFloor" -}} +{{- if .Values.server.hpa.enabled -}} +{{- .Values.server.hpa.minReplicas -}} +{{- else -}} +{{- .Values.server.replicas -}} +{{- end -}} +{{- end }} + +{{/* +Cross-field validation that JSON Schema draft-07 cannot express. +*/}} +{{- define "hugegraph.validateValues" -}} +{{- include "hugegraph.resolveValues" . -}} +{{- $startupBudget := include "hugegraph.server.startupProbeBudget" . | int -}} +{{- if lt $startupBudget (add (int .Values.server.storageWaitTimeoutSeconds) 120) -}} +{{- fail "server.probes.startup.failureThreshold and periodSeconds must leave at least server.storageWaitTimeoutSeconds + 120 seconds to wait for storage and start Server; increase the probe budget or reduce server.storageWaitTimeoutSeconds" -}} +{{- end -}} +{{- range $comp := list "pd" "store" "server" "hubble" -}} +{{- $compLabels := get (get $.Values $comp | default dict) "podLabels" | default dict -}} +{{- range $reserved := list "app.kubernetes.io/name" "app.kubernetes.io/instance" "app.kubernetes.io/component" -}} +{{- if hasKey $compLabels $reserved -}} +{{- fail (printf "%s.podLabels must not set %s: the chart manages it and the workload selectors, Services and PDBs match on it" $comp $reserved) -}} +{{- end -}} +{{- end -}} +{{/* User pod annotations render after the chart's own, and the Kubernetes + decoder keeps the last duplicate key, so a fixed checksum/* value would + replace the rendered checksum and pin it: rotating a Secret or changing + config would no longer roll the pods. */}} +{{- $compAnnotations := get (get $.Values $comp | default dict) "podAnnotations" | default dict -}} +{{- range $key, $_ := $compAnnotations -}} +{{- if hasPrefix "checksum/" $key -}} +{{- fail (printf "%s.podAnnotations must not set %s: the chart owns the checksum/ annotation prefix, which triggers pod rollouts when resolved Secrets or config change" $comp $key) -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- if and .Values.server.hpa.enabled (gt (int .Values.server.hpa.minReplicas) (int .Values.server.hpa.maxReplicas)) -}} +{{- fail "server.hpa.minReplicas must be less than or equal to server.hpa.maxReplicas" -}} +{{- end -}} +{{- if .Values.server.hpa.enabled -}} +{{- $serverResources := .Values.server.resources | default dict -}} +{{- $serverRequests := get $serverResources "requests" | default dict -}} +{{- if not (hasKey $serverRequests "cpu") -}} +{{- fail "server.resources.requests.cpu is required when server.hpa.enabled=true" -}} +{{- end -}} +{{- $cpuRequest := trim (toString (get $serverRequests "cpu")) -}} +{{- if or (eq $cpuRequest "") (hasPrefix "-" $cpuRequest) (regexMatch "^[+]?((0+([.]0*)?)|([.]0+))(([KMGTPE]i)|[numkMGTPE]|[eE][+-]?[0-9]+)?$" $cpuRequest) -}} +{{- fail "server.resources.requests.cpu must be strictly positive when server.hpa.enabled=true" -}} +{{- end -}} +{{- end -}} +{{/* +Raft and shard membership are persisted; deleting Pods does not reconfigure +them, so an in-place replica shrink permanently loses PD quorum or Store +shard majorities. The guard reads the live StatefulSet, so it fires only on +a real upgrade against a cluster; template-only renders have no live object +and skip it. An operator who has completed the documented manual scale-down +procedure has already scaled the live StatefulSet, so desired equals live +and the upgrade passes. +*/}} +{{- if .Release.IsUpgrade -}} +{{- range $comp := list "pd" "store" -}} +{{- $stsName := "" -}} +{{- if eq $comp "pd" -}}{{- $stsName = include "hugegraph.pd.name" $ -}}{{- else -}}{{- $stsName = include "hugegraph.store.name" $ -}}{{- end -}} +{{- $live := lookup "apps/v1" "StatefulSet" $.Release.Namespace $stsName -}} +{{- if $live -}} +{{- $wasPersistent := false -}} +{{- range $claim := get (get $live "spec") "volumeClaimTemplates" | default list -}} +{{- if eq (dig "metadata" "name" "" $claim) (printf "%s-data" $comp) -}}{{- $wasPersistent = true -}}{{- end -}} +{{- end -}} +{{- if ne $wasPersistent (get (get (get $.Values $comp) "storage") "enabled") -}} +{{- fail (printf "%s.storage.enabled cannot change on an initialized release: StatefulSet volumeClaimTemplates are immutable and changing storage can discard data. Use a fresh release and migrate data" $comp) -}} +{{- end -}} + +{{- $liveReplicas := int (dig "spec" "replicas" 0 $live) -}} +{{- $desired := int (get (get $.Values $comp) "replicas") -}} +{{- if and (gt $liveReplicas 0) (lt $desired $liveReplicas) -}} +{{- $procedure := ternary "Reconfigure the persisted PD voting membership before scaling the live StatefulSet" "Retire leaving Store IDs and migrate their shards before scaling the live StatefulSet" (eq $comp "pd") -}} +{{- fail (printf "%s.replicas cannot shrink from %d to %d through a helm upgrade: persisted membership is not changed by removing Pods. %s; see docs/operations.md (Topology and scheduling). The upgrade passes once live replicas match the value" $comp $liveReplicas $desired $procedure) -}} +{{- end -}} +{{/* +PD raft membership is the persisted voting configuration, and the peer list +the chart renders reaches it only as NodeOptions.setInitialConf, which jraft +applies when bootstrapping a node that has no configuration of its own. On an +initialized group, adding Pods adds non-voting strangers: the extra PD starts, +the peer list changes, and the voting configuration does not. PD exposes the +change through RaftEngine.changePeerList, reachable from the PD client API but +from no REST route, so the chart cannot perform it and does not pretend to. +Growing Store is ordinary scale-out and stays allowed. +*/}} +{{- if and (eq $comp "pd") (gt $liveReplicas 0) (gt $desired $liveReplicas) -}} +{{- fail (printf "pd.replicas cannot grow from %d to %d through a helm upgrade: the rendered peer list reaches raft only as the initial configuration, so new Pods would start without joining the voting configuration. Change the persisted membership through PD first, then scale the live StatefulSet, then upgrade with the matching value; see docs/operations.md (Topology and scheduling) for the supported lifecycle. A fresh install at any replica count is unaffected" $liveReplicas $desired) -}} +{{- end -}} +{{/* +Raft identity. Every PD and Store names itself to its peers as +...svc:, and that address is what the +persisted membership records (PD's voting configuration, each Store shard +group's peer list). The port reaches raft only through the bootstrap +configuration, and the StatefulSet name is the DNS name, so a Pod restarted +on a changed port is a stranger to the group it belongs to: it cannot rejoin, +and once enough Pods have been replaced the group has no quorum. The live +StatefulSet carries the port it was initialized with, so a change is refused +there; a fresh install chooses freely. +*/}} +{{- $desiredRaft := int (get (get (get $.Values $comp) "ports" | default dict) "raft") -}} +{{- range $container := dig "spec" "template" "spec" "containers" list $live -}} +{{- if eq (get $container "name" | default "") $comp -}} +{{- range $port := get $container "ports" | default list -}} +{{- if and (eq (get $port "name" | default "") "raft") (ne (int (get $port "containerPort")) $desiredRaft) -}} +{{- fail (printf "%s.ports.raft cannot change from %d to %d on an initialized release: the raft address .%s..svc: is the identity the persisted raft membership records, and a Pod restarted on the new port cannot rejoin its group%s. Keep the value; changing the port needs a fresh install, or a migration of the persisted membership before the upgrade" $comp (int (get $port "containerPort")) $desiredRaft $stsName (ternary " (a single PD loses service outright)" "" (eq $comp "pd"))) -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{/* +nameOverride also feeds the app.kubernetes.io/name selector label. A +StatefulSet selector is immutable, so an override that keeps the name (one +contained in the release name) but changes that label is refused by the API +server mid-upgrade; refuse it here with the reason instead. +*/}} +{{- $liveName := dig "spec" "selector" "matchLabels" "app.kubernetes.io/name" "" $live -}} +{{- if and $liveName (ne $liveName (include "hugegraph.name" $)) -}} +{{- fail (printf "nameOverride changes the app.kubernetes.io/name selector label of the %s StatefulSet %s from %s to %s, which Kubernetes forbids on an existing StatefulSet. Restore the previous override; a rename needs a fresh install and a data migration" $comp $stsName $liveName (include "hugegraph.name" $)) -}} +{{- end -}} +{{- end -}} +{{/* +The same identity is carried by the StatefulSet name, which nameOverride and +fullnameOverride change. The renamed StatefulSet does not exist yet, so it +cannot be looked up by name; the release's existing StatefulSets are found by +the instance and component labels instead, which do not depend on the name. +*/}} +{{- $released := lookup "apps/v1" "StatefulSet" $.Release.Namespace "" -}} +{{- range $sts := get $released "items" | default list -}} +{{- $labels := dig "metadata" "labels" dict $sts -}} +{{- if and (eq (get $labels "app.kubernetes.io/instance" | default "") $.Release.Name) (eq (get $labels "app.kubernetes.io/component" | default "") $comp) (ne (dig "metadata" "name" "" $sts) $stsName) -}} +{{- fail (printf "release %s already owns the %s StatefulSet %s, but the values now name it %s (nameOverride or fullnameOverride changed): the StatefulSet name is the raft address of every Pod and the prefix of every PersistentVolumeClaim, so the rename would start empty Pods under new identities and leave the data volumes detached. Restore the previous override; a rename needs a fresh install and a data migration" $.Release.Name $comp (dig "metadata" "name" "" $sts) $stsName) -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- $hubbleLive := lookup "apps/v1" "Deployment" .Release.Namespace (include "hugegraph.hubble.name" .) -}} +{{- if and $hubbleLive .Values.hubble.enabled -}} +{{- $wasPersistent := false -}} +{{- $hasDataVolume := false -}} +{{- range $volume := dig "spec" "template" "spec" "volumes" list $hubbleLive -}} +{{- if eq ($volume.name | default "") "data" -}}{{- $hasDataVolume = true -}}{{- end -}} +{{- if and (eq ($volume.name | default "") "data") (hasKey $volume "persistentVolumeClaim") -}}{{- $wasPersistent = true -}}{{- end -}} +{{- end -}} +{{- if ne $wasPersistent .Values.hubble.persistence.enabled -}} +{{- if not $hasDataVolume -}} +{{- fail "hubble.persistence.enabled cannot change on an initialized release: legacy Hubble has no data volume and its UI records are in the container filesystem. Keep hubble.persistence.enabled=false with --reuse-values or an explicit GitOps value; enabling persistence requires a reviewed metadata migration first" -}} +{{- end -}} +{{- fail "hubble.persistence.enabled cannot change on an initialized release: changing storage can discard UI records. Migrate the data into a fresh deployment first" -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{/* +Only validate minAvailable where a PDB is actually rendered. The pd/store PDB +templates require replicas > 1, so a single-replica release never creates one +and must not be failed for a value that has no effect. +*/}} +{{- if and .Values.pd.pdb.enabled (gt (int .Values.pd.replicas) 1) (ge (int .Values.pd.pdb.minAvailable) (int .Values.pd.replicas)) -}} +{{- fail "pd.pdb.minAvailable must be less than pd.replicas, otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} +{{- end -}} +{{- if and .Values.pd.pdb.enabled (gt (int .Values.pd.replicas) 1) (lt (int .Values.pd.pdb.minAvailable) (include "hugegraph.pd.quorum" . | int)) -}} +{{- fail "pd.pdb.minAvailable must be at least the PD Raft majority, floor(replicas/2)+1, otherwise the budget permits voluntary disruptions that drop PD below quorum. Note a PDB only limits voluntary disruption such as drains and evictions; it cannot protect quorum from node failure" -}} +{{- end -}} +{{- if and .Values.store.pdb.enabled (gt (int .Values.store.replicas) 1) (ge (int .Values.store.pdb.minAvailable) (int .Values.store.replicas)) -}} +{{- fail "store.pdb.minAvailable must be less than store.replicas, otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} +{{- end -}} +{{- if and .Values.store.pdb.enabled (gt (int .Values.store.replicas) 1) (lt (int .Values.store.pdb.minAvailable) (sub (int .Values.store.replicas) 1)) -}} +{{- fail "store.pdb.minAvailable must be at least store.replicas - 1: each shard keeps its copies on a subset of the Stores, so permitting more than one concurrent voluntary eviction can remove a shard majority regardless of the Store count" -}} +{{- end -}} +{{/* +PD -D system properties must be empty or a positive integer. The schema +enforces the types; these checks add a named failure for zero, negative, and +nonsense values, and check an explicit shard count against PD's real +constraints: PD's config API accepts only odd shard counts, PD clamps a +count of 2 to 1 (two shards cannot elect a leader), and PD clamps the +effective count to the number of live stores. All lookups tolerate absent +keys for releases stored before the values existed. +*/}} +{{- $pdPartition := get .Values.pd "partition" | default dict -}} +{{- $pdProps := dict + "pd.partition.defaultShardCount" (include "hugegraph.optionalScalar" (get $pdPartition "defaultShardCount")) + "pd.partition.storeMaxShardCount" (include "hugegraph.optionalScalar" (get $pdPartition "storeMaxShardCount")) -}} +{{- range $label, $raw := $pdProps -}} +{{- if and (ne $raw "") (or (not (regexMatch "^[0-9]+$" $raw)) (eq (int $raw) 0)) -}} +{{- fail (printf "%s must be empty or a positive integer" $label) -}} +{{- end -}} +{{- end -}} +{{- $explicitShards := include "hugegraph.optionalScalar" (get $pdPartition "defaultShardCount") -}} +{{- if regexMatch "^[1-9][0-9]*$" $explicitShards -}} +{{- if eq (mod (int $explicitShards) 2) 0 -}} +{{- fail "pd.partition.defaultShardCount must be odd: PD's config API rejects even shard counts, and PD clamps a bootstrap value of 2 to 1 because two shards cannot elect a leader" -}} +{{- end -}} +{{- if gt (int $explicitShards) (int .Values.store.replicas) -}} +{{- fail "pd.partition.defaultShardCount is greater than store.replicas: PD would clamp the effective shard count to the number of live stores, so the extra replicas would silently never be placed. Raise store.replicas or lower the shard count" -}} +{{- end -}} +{{- end -}} +{{- $svc := get .Values.server "service" | default dict -}} +{{- if and (get $svc "nodePort") (not (has (get $svc "type" | default "ClusterIP") (list "NodePort" "LoadBalancer"))) -}} +{{- fail "server.service.nodePort requires server.service.type to be NodePort or LoadBalancer" -}} +{{- end -}} +{{- $advertiseUrl := trim (default "" .Values.server.advertiseUrl) -}} +{{- if and $advertiseUrl (not (or (hasPrefix "http://" $advertiseUrl) (hasPrefix "https://" $advertiseUrl))) -}} +{{- fail "server.advertiseUrl must be an absolute http:// or https:// URL when set" -}} +{{- end -}} +{{- $pdSvc := get .Values.pd "service" | default dict -}} +{{- $pdSvcType := get $pdSvc "type" | default "ClusterIP" -}} +{{- if and (or (get $pdSvc "restNodePort") (get $pdSvc "grpcNodePort")) (not (has $pdSvcType (list "NodePort" "LoadBalancer"))) -}} +{{- fail "pd.service.restNodePort and pd.service.grpcNodePort require pd.service.type to be NodePort or LoadBalancer" -}} +{{- end -}} +{{- $serverPdb := get .Values.server "pdb" | default dict -}} +{{- $serverReplicaFloor := include "hugegraph.server.replicaFloor" . | int -}} +{{- if and (get $serverPdb "enabled" | default false) (gt $serverReplicaFloor 1) (ge (int (get $serverPdb "minAvailable" | default 1)) $serverReplicaFloor) -}} +{{- fail "server.pdb.minAvailable must be less than the active Server replica floor (server.hpa.minReplicas when HPA is enabled, otherwise server.replicas), otherwise the PDB permanently blocks voluntary disruptions such as node drains" -}} +{{- end -}} +{{- $serverIngress := get .Values.server "ingress" | default dict -}} +{{- if get (get .Values.server "securityContext" | default dict) "readOnlyRootFilesystem" | default false -}} +{{- fail "server.securityContext.readOnlyRootFilesystem=true breaks Server: its wrapper rewrites conf/rest-server.properties inside the image at startup and the chart mounts no writable volume there" -}} +{{- end -}} +{{/* +extraEnv entries render after the chart-owned variables and Kubernetes lets +the last duplicate win, so a duplicate name would silently override a +validated contract (for example re-enabling init-store across Server +replicas). Reserved names are rejected instead. JAVA_OPTIONS is reserved +for pd, store, and server because each component's start script skips its +automatic heap sizing and drops the chart's JAVA_OPTS flags entirely when +JAVA_OPTIONS arrives preset in the environment (verified in +start-hugegraph-pd.sh, start-hugegraph-store.sh, and hugegraph-server.sh). +*/}} +{{- $reservedEnv := dict + "pd" (list "HG_PD_GRPC_HOST" "HG_PD_GRPC_PORT" "HG_PD_REST_PORT" "HG_PD_RAFT_ADDRESS" "HG_PD_RAFT_PEERS_LIST" "HG_PD_INITIAL_STORE_LIST" "HG_PD_INITIAL_STORE_COUNT" "HG_PD_DATA_PATH" "HG_PD_AUTH_SECRET_KEY" "JAVA_OPTS" "JAVA_OPTIONS") + "store" (list "HG_STORE_PD_ADDRESS" "HG_STORE_GRPC_HOST" "HG_STORE_GRPC_PORT" "HG_STORE_REST_PORT" "HG_STORE_RAFT_ADDRESS" "HG_STORE_DATA_PATH" "JAVA_OPTS" "JAVA_OPTIONS") + "server" (list "POD_IP" "HG_SERVER_SERVICE_ID" "HG_SERVER_GRAPHSPACE" "SERVICE_ID" "GRAPH_SPACE" "HG_SERVER_BACKEND" "HG_SERVER_PD_PEERS" "HG_SERVER_PD_REST_ENDPOINT" "STORE_REST" "HG_SERVER_INIT_STORE_ENABLED" "HG_SERVER_URLS_TO_PD" "HG_SERVER_DEPLOY_IN_K8S" "HG_SERVER_STARTUP_TIMEOUT_S" "WAIT_STORAGE_TIMEOUT_S" "PD_AUTH_PASSWORD" "PASSWORD" "HG_SERVER_AUTH_TOKEN_SECRET" "JAVA_OPTS" "JAVA_OPTIONS") + "hubble" (list "HG_HUBBLE_PD_PEERS" "HG_HUBBLE_PD_SERVER" "HG_HUBBLE_PD_PASSWORD" "HG_HUBBLE_STORE_TARGETS" "HG_HUBBLE_SERVER_URL" "SPRING_DATASOURCE_URL") -}} +{{- range $component, $reserved := $reservedEnv -}} +{{- $componentValues := get $.Values $component | default dict -}} +{{- range $entry := get $componentValues "extraEnv" | default list -}} +{{- if has (get $entry "name") $reserved -}} +{{- fail (printf "%s.extraEnv must not set the chart-managed variable %s" $component (get $entry "name")) -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- $hubble := get .Values "hubble" | default dict -}} +{{- if get $hubble "enabled" | default false -}} +{{- if and (not .Values.server.auth.enabled) (not (get $hubble "allowWithoutServerAuth" | default false)) -}} +{{- fail "hubble.enabled requires server.auth: current Hubble images authenticate against the cluster and cannot complete their login on an auth-less deployment. Enable server.auth, or set hubble.allowWithoutServerAuth=true for images that support it" -}} +{{- end -}} +{{- $hubbleSvc := get $hubble "service" | default dict -}} +{{- if and (get $hubbleSvc "nodePort") (not (has (get $hubbleSvc "type" | default "ClusterIP") (list "NodePort" "LoadBalancer"))) -}} +{{- fail "hubble.service.nodePort requires hubble.service.type to be NodePort or LoadBalancer" -}} +{{- end -}} +{{- $hubbleImage := get $hubble "image" | default dict -}} +{{- if and (eq (trim (get $hubbleImage "tag" | default "")) "") (eq (trim (get $hubbleImage "digest" | default "")) "") -}} +{{- fail "hubble.image needs a tag or a digest: the chart appVersion tracks the Server release, not Hubble, so there is no meaningful fallback" -}} +{{- end -}} +{{- if get (get $hubble "securityContext" | default dict) "readOnlyRootFilesystem" | default false -}} +{{- fail "hubble.securityContext.readOnlyRootFilesystem=true breaks Hubble: its wrapper writes conf/hugegraph-hubble.properties inside the image at startup and the chart mounts no writable volume there" -}} +{{- end -}} +{{- $hubbleIngress := get $hubble "ingress" | default dict -}} +{{- end -}} +{{- $pdAuth := get .Values.pd "auth" | default dict -}} +{{/* A values set with no pd.auth block at all (a release stored before the + field existed, replayed by --reuse-values) gets the chart default, + autoGenerate; only an explicit autoGenerate=false with nothing else set + is an error. */}} +{{- $pdAutoGen := ternary (get $pdAuth "autoGenerate") true (hasKey $pdAuth "autoGenerate") -}} +{{- if and (not (get $pdAuth "existingSecret" | default "")) (not (get $pdAuth "value" | default "")) (not $pdAutoGen) -}} +{{- fail "pd.auth requires existingSecret, value, or autoGenerate=true: PD refuses to start without a REST secret" -}} +{{- end -}} +{{- $auth := get .Values.server "auth" | default dict -}} +{{- $admin := get $auth "admin" | default dict -}} +{{- $token := get $auth "token" | default dict -}} +{{- if get $auth "enabled" | default false -}} +{{- if and (not (get $admin "existingSecret" | default "")) (not (get $admin "password" | default "")) (not (get $admin "autoGenerate" | default false)) -}} +{{- fail "server.auth.admin requires existingSecret, password, or autoGenerate=true when auth is enabled" -}} +{{- end -}} +{{- if and (not (get $token "existingSecret" | default "")) (not (get $token "value" | default "")) (not (get $token "autoGenerate" | default false)) -}} +{{- fail "server.auth.token requires existingSecret, value, or autoGenerate=true when auth is enabled" -}} +{{- end -}} +{{- end -}} + +{{- end }} + +{{/* +podAntiAffinity snippet for a component label key. +mode: required | preferred | disabled +*/}} +{{/* +NetworkPolicy building blocks: a same-release peer by component, a TCP port +list, and DNS egress by port only, so it works wherever the cluster runs its +resolver (CoreDNS in any namespace, NodeLocal DNSCache). +*/}} +{{- define "hugegraph.netpol.peer" -}} +- podSelector: + matchLabels: + {{- include "hugegraph.selectorLabels" .root | nindent 6 }} + app.kubernetes.io/component: {{ .component }} +{{- end }} + +{{- define "hugegraph.netpol.ports" -}} +{{- $rules := list -}} +{{- range . -}} +{{- $rules = append $rules (printf "- protocol: TCP\n port: %d" (int .)) -}} +{{- end -}} +{{- join "\n" $rules -}} +{{- end }} + +{{- define "hugegraph.netpol.dns" -}} +- ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 +{{- end }} + +{{- define "hugegraph.antiAffinity" -}} +{{- $mode := .mode -}} +{{- $component := .component -}} +{{- $labels := .labels -}} +{{- if eq $mode "required" }} +affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + {{- toYaml $labels | nindent 12 }} + app.kubernetes.io/component: {{ $component }} + topologyKey: kubernetes.io/hostname +{{- else if eq $mode "preferred" }} +affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + {{- toYaml $labels | nindent 14 }} + app.kubernetes.io/component: {{ $component }} + topologyKey: kubernetes.io/hostname +{{- end }} +{{- end }} + +{{/* +Render a container image reference. An explicit image.digest pins immutably and wins +over tag; otherwise fall back to tag, then to the chart appVersion. Takes a dict of +(image, appVersion). +*/}} +{{- define "hugegraph.image" -}} +{{- $img := .image -}} +{{- $digest := trim (get $img "digest" | default "") -}} +{{- if ne $digest "" -}} +{{- printf "%s@%s" $img.repository $digest -}} +{{- else -}} +{{- printf "%s:%s" $img.repository (default .appVersion $img.tag) -}} +{{- end -}} +{{- end }} diff --git a/helm/hugegraph/templates/hubble-deployment.yaml b/helm/hugegraph/templates/hubble-deployment.yaml new file mode 100644 index 0000000000..bd83736de2 --- /dev/null +++ b/helm/hugegraph/templates/hubble-deployment.yaml @@ -0,0 +1,318 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +{{ include "hugegraph.resolveValues" . }} +{{- if get (get .Values "hubble" | default dict) "enabled" | default false }} +{{- include "hugegraph.validateValues" . }} +{{- $customPort := ne (int .Values.hubble.port) 8088 }} +{{- $mode := .Values.hubble.mode | default "pd" }} +{{- if not (has $mode (list "pd" "direct")) }} +{{- fail "hubble.mode must be one of: pd, direct" }} +{{- end }} +{{- $pdMode := eq $mode "pd" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "hugegraph.hubble.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: hubble +spec: + # Hubble keeps its connection metadata in an embedded per-instance H2 + # database, so this Deployment is fixed at one replica and replaces the Pod + # instead of rolling, which also keeps a persistent volume single-attached. + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: hubble + template: + metadata: + labels: + {{- include "hugegraph.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: hubble + {{- with .Values.hubble.podLabels }}{{ toYaml . | nindent 8 }}{{- end }} + annotations: + {{- if $pdMode }} + # Rolls Hubble when the PD REST Secret written into its properties changes. + checksum/pd-auth: {{ include "hugegraph.pd.authChecksum" . | quote }} + {{- end }} + {{- with .Values.hubble.podAnnotations }}{{ toYaml . | nindent 8 }}{{- end }} + spec: + automountServiceAccountToken: {{ get (get .Values.hubble "serviceAccount" | default dict) "automountServiceAccountToken" | default false }} + serviceAccountName: {{ include "hugegraph.serviceAccountName" (dict "component" .Values.hubble "name" (include "hugegraph.hubble.name" .) ) }} + {{- if hasKey .Values.hubble "terminationGracePeriodSeconds" }} + terminationGracePeriodSeconds: {{ .Values.hubble.terminationGracePeriodSeconds }} + {{- end }} + {{- with .Values.hubble.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.hubble.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hubble.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hubble.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hubble.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hubble.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: hubble + image: {{ include "hugegraph.image" (dict "image" .Values.hubble.image "appVersion" $.Chart.AppVersion) | quote }} + imagePullPolicy: {{ .Values.hubble.image.pullPolicy }} + {{- with .Values.hubble.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + # The Hubble image reads conf/hugegraph-hubble.properties only; it + # has no environment mapping for these keys, so the wrapper rewrites + # them before handing off to the image's own start script. The image + # ships no dumb-init; start-hubble.sh -f execs java, which then + # receives signals directly. + command: + - /bin/bash + - -c + args: + - | + set -euo pipefail + CONF=./conf/hugegraph-hubble.properties + if [[ ! -r "${CONF}" ]]; then + echo "missing or unreadable ${CONF}; refusing to write a stub config" >&2 + exit 1 + fi + {{- if $pdMode }} + : "${HG_HUBBLE_PD_PASSWORD:?PD REST secret must not be empty}" + # The value is written into a Java properties file, whose + # parser unescapes backslashes and trims whitespace, so these + # shapes would silently store a credential that differs from + # the Secret. Inline pd.auth values are schema-checked; an + # external existingSecret is only seen here. + case "${HG_HUBBLE_PD_PASSWORD}" in + *\\* | ' '* | *' ') + echo "the PD REST secret must not contain backslashes or" \ + "leading or trailing whitespace; fix the pd.auth" \ + "Secret value" >&2 + exit 1 + ;; + esac + # Byte check under the C locale, independent of the image + # locale: in a UTF-8 locale a POSIX print class accepts + # multibyte characters, which the ISO-8859-1 properties reader + # then decodes into a credential PD never received. Anything + # outside 0x20-0x7E (tabs, newlines, non-ASCII) is refused. + if (( $(printf '%s' "${HG_HUBBLE_PD_PASSWORD}" | LC_ALL=C tr -d '\040-\176' | wc -c) > 0 )); then + echo "the PD REST secret must be printable ASCII (bytes 0x20-0x7E:" \ + "no tabs, newlines or non-ASCII characters); fix the" \ + "pd.auth Secret value" >&2 + exit 1 + fi + {{- end }} + TMP=$(mktemp) + FOUND_PD_ENABLED=false + {{- if $pdMode }} + FOUND_PD_PEERS=false + FOUND_PD_SERVER=false + FOUND_PD_USERNAME=false + FOUND_PD_PASSWORD=false + FOUND_STORE_TARGETS=false + {{- else }} + FOUND_DIRECT_URL=false + {{- end }} + FOUND_SERVER_HOST=false + {{- if $customPort }} + FOUND_HUBBLE_PORT=false + FOUND_SERVER_PORT=false + {{- end }} + while IFS= read -r LINE || [[ -n "${LINE}" ]]; do + case "${LINE}" in + pd.enabled=*) + printf 'pd.enabled={{ $pdMode }}\n' >>"${TMP}" + FOUND_PD_ENABLED=true + ;; + {{- if $pdMode }} + pd.peers=*) + printf 'pd.peers=%s\n' "${HG_HUBBLE_PD_PEERS}" >>"${TMP}" + FOUND_PD_PEERS=true + ;; + pd.server=*) + printf 'pd.server=%s\n' "${HG_HUBBLE_PD_SERVER}" >>"${TMP}" + FOUND_PD_SERVER=true + ;; + operations.pd.username=*) + printf 'operations.pd.username=hubble\n' >>"${TMP}" + FOUND_PD_USERNAME=true + ;; + operations.pd.password=*) + printf 'operations.pd.password=%s\n' "${HG_HUBBLE_PD_PASSWORD}" >>"${TMP}" + FOUND_PD_PASSWORD=true + ;; + operations.store.allowed_targets=*) + printf 'operations.store.allowed_targets=%s\n' \ + "${HG_HUBBLE_STORE_TARGETS}" >>"${TMP}" + FOUND_STORE_TARGETS=true + ;; + {{- else }} + server.direct_url=*) + printf 'server.direct_url=%s\n' "${HG_HUBBLE_SERVER_URL}" >>"${TMP}" + FOUND_DIRECT_URL=true + ;; + {{- end }} + server.host=*) + printf 'server.host=0.0.0.0\n' >>"${TMP}" + FOUND_SERVER_HOST=true + ;; + {{- if $customPort }} + hubble.port=*) + printf 'hubble.port=%s\n' {{ .Values.hubble.port | quote }} >>"${TMP}" + FOUND_HUBBLE_PORT=true + ;; + server.port=*) + printf 'server.port=%s\n' {{ .Values.hubble.port | quote }} >>"${TMP}" + FOUND_SERVER_PORT=true + ;; + {{- end }} + *) + printf '%s\n' "${LINE}" >>"${TMP}" + ;; + esac + done <"${CONF}" + if [[ "${FOUND_PD_ENABLED}" == false ]]; then + printf 'pd.enabled={{ $pdMode }}\n' >>"${TMP}" + fi + {{- if $pdMode }} + if [[ "${FOUND_PD_PEERS}" == false ]]; then + printf 'pd.peers=%s\n' "${HG_HUBBLE_PD_PEERS}" >>"${TMP}" + fi + if [[ "${FOUND_PD_SERVER}" == false ]]; then + printf 'pd.server=%s\n' "${HG_HUBBLE_PD_SERVER}" >>"${TMP}" + fi + # PD REST credential for the operations API. + if [[ "${FOUND_PD_USERNAME}" == false ]]; then + printf 'operations.pd.username=hubble\n' >>"${TMP}" + fi + if [[ "${FOUND_PD_PASSWORD}" == false ]]; then + printf 'operations.pd.password=%s\n' "${HG_HUBBLE_PD_PASSWORD}" >>"${TMP}" + fi + if [[ "${FOUND_STORE_TARGETS}" == false ]]; then + printf 'operations.store.allowed_targets=%s\n' \ + "${HG_HUBBLE_STORE_TARGETS}" >>"${TMP}" + fi + {{- else }} + if [[ "${FOUND_DIRECT_URL}" == false ]]; then + printf 'server.direct_url=%s\n' "${HG_HUBBLE_SERVER_URL}" >>"${TMP}" + fi + {{- end }} + # Current Hubble binds server.host, which defaults to + # localhost; the shipped conf's hubble.host=0.0.0.0 line is + # legacy, is ignored by current images, and is preserved + # verbatim above for images that still read it. + if [[ "${FOUND_SERVER_HOST}" == false ]]; then + printf 'server.host=0.0.0.0\n' >>"${TMP}" + fi + {{- if $customPort }} + if [[ "${FOUND_HUBBLE_PORT}" == false ]]; then + printf 'hubble.port=%s\n' {{ .Values.hubble.port | quote }} >>"${TMP}" + fi + if [[ "${FOUND_SERVER_PORT}" == false ]]; then + printf 'server.port=%s\n' {{ .Values.hubble.port | quote }} >>"${TMP}" + fi + {{- end }} + chmod 600 "${TMP}" + mv "${TMP}" "${CONF}" + exec ./bin/start-hubble.sh -f + ports: + - name: http + containerPort: {{ .Values.hubble.port }} + env: + {{- if $pdMode }} + - name: HG_HUBBLE_PD_PEERS + value: {{ include "hugegraph.pd.grpcPeersList" . | quote }} + - name: HG_HUBBLE_PD_SERVER + value: {{ include "hugegraph.pd.restClientEndpoint" . | quote }} + - name: HG_HUBBLE_PD_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "hugegraph.pd.authSecretName" . | quote }} + key: {{ include "hugegraph.pd.authSecretKey" . | quote }} + - name: HG_HUBBLE_STORE_TARGETS + value: {{ include "hugegraph.store.restOriginsList" . | quote }} + {{- else }} + - name: HG_HUBBLE_SERVER_URL + value: {{ include "hugegraph.server.clientUrl" . | quote }} + {{- end }} + # Hubble's H2 location is fixed to ./db inside the image classpath + # config; Spring Boot's environment binding is the supported way to + # point it into the mounted volume. + - name: SPRING_DATASOURCE_URL + value: "jdbc:h2:file:/hubble-data/db;DB_CLOSE_ON_EXIT=FALSE" + {{- with .Values.hubble.extraEnv }}{{ toYaml . | nindent 12 }}{{- end }} + volumeMounts: + - name: data + mountPath: /hubble-data + startupProbe: + httpGet: + path: /actuator/health + port: http + failureThreshold: {{ .Values.hubble.probes.startup.failureThreshold }} + periodSeconds: {{ .Values.hubble.probes.startup.periodSeconds }} + {{- with include "hugegraph.probeTuning" .Values.hubble.probes.startup }}{{ . | trim | nindent 12 }}{{- end }} + readinessProbe: + httpGet: + path: /actuator/health + port: http + periodSeconds: {{ .Values.hubble.probes.readiness.periodSeconds }} + failureThreshold: {{ .Values.hubble.probes.readiness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.hubble.probes.readiness }}{{ . | trim | nindent 12 }}{{- end }} + # TCP for liveness: /actuator/health aggregates datasource health, + # and a transiently slow persistent volume must not get a healthy + # JVM killed (Recreate + RWO makes an overlapping restart worse). + livenessProbe: + tcpSocket: + port: http + periodSeconds: {{ .Values.hubble.probes.liveness.periodSeconds }} + failureThreshold: {{ .Values.hubble.probes.liveness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.hubble.probes.liveness }}{{ . | trim | nindent 12 }}{{- end }} + {{- with .Values.hubble.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: data + {{- if .Values.hubble.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "hugegraph.hubble.dataName" . }} + {{- else }} + emptyDir: {} + {{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/hubble-ingress.yaml b/helm/hugegraph/templates/hubble-ingress.yaml new file mode 100644 index 0000000000..9b69bbdf94 --- /dev/null +++ b/helm/hugegraph/templates/hubble-ingress.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- $hubble := get .Values "hubble" | default dict }} +{{- if and (get $hubble "enabled" | default false) .Values.hubble.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "hugegraph.hubble.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: hubble + {{- with (get .Values.hubble.ingress "annotations") }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.hubble.ingress.className }} + ingressClassName: {{ .Values.hubble.ingress.className | quote }} + {{- end }} + {{- with .Values.hubble.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range (.Values.hubble.ingress.hosts | default (list (dict "paths" (list (dict "path" "/" "pathType" "Prefix"))))) }} + - {{- with .host }} + host: {{ . | quote }} + {{- end }} + http: + paths: + {{- range (.paths | default (list (dict "path" "/" "pathType" "Prefix"))) }} + - path: {{ .path | quote }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "hugegraph.hubble.name" $ }} + port: + number: {{ $.Values.hubble.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/hubble-pvc.yaml b/helm/hugegraph/templates/hubble-pvc.yaml new file mode 100644 index 0000000000..a1396a6918 --- /dev/null +++ b/helm/hugegraph/templates/hubble-pvc.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- $hubble := get .Values "hubble" | default dict }} +{{- if and (get $hubble "enabled" | default false) .Values.hubble.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "hugegraph.hubble.dataName" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: hubble + # Survive helm uninstall, matching the PD/Store volumeClaimTemplates + # behavior; delete the PVC explicitly to discard the stored connection + # metadata and credentials. + annotations: + helm.sh/resource-policy: keep +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.hubble.persistence.size }} + {{- if eq .Values.hubble.persistence.storageClassName "-" }} + storageClassName: "" + {{- else if .Values.hubble.persistence.storageClassName }} + storageClassName: {{ .Values.hubble.persistence.storageClassName | quote }} + {{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/hubble-service.yaml b/helm/hugegraph/templates/hubble-service.yaml new file mode 100644 index 0000000000..a6210f5a72 --- /dev/null +++ b/helm/hugegraph/templates/hubble-service.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- if get (get .Values "hubble" | default dict) "enabled" | default false }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hugegraph.hubble.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: hubble + {{- $svc := get .Values.hubble "service" | default dict }} + {{- with (get $svc "annotations") }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ get $svc "type" | default "ClusterIP" }} + {{- range $field := list "externalTrafficPolicy" "loadBalancerClass" "loadBalancerIP" "loadBalancerSourceRanges" "externalIPs" }} + {{- with get $svc $field }} + {{ $field }}: {{ toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- if hasKey $svc "allocateLoadBalancerNodePorts" }} + {{- if ne (get $svc "allocateLoadBalancerNodePorts") nil }} + allocateLoadBalancerNodePorts: {{ get $svc "allocateLoadBalancerNodePorts" }} + {{- end }} + {{- end }} + selector: + {{- include "hugegraph.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: hubble + ports: + - name: http + port: {{ .Values.hubble.port }} + targetPort: http + {{- with (get $svc "nodePort") }} + nodePort: {{ . }} + {{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/networkpolicy.yaml b/helm/hugegraph/templates/networkpolicy.yaml new file mode 100644 index 0000000000..e8638d6d74 --- /dev/null +++ b/helm/hugegraph/templates/networkpolicy.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- /* +One NetworkPolicy per component. Each object isolates its own Pods in both +directions and carries all of their allows, so no object ever denies a Pod +before that Pod's allows exist, whatever order the objects are applied in. +Peers match the release and component labels; ports are the values the +containers bind. The helm test Pod is selected by no policy, so its egress +stays open and the Server policy admits it. Outside clients require native extraIngress rules. An exposed Service or +Ingress does not automatically change these rules. +*/}} +{{- $np := get .Values "networkPolicy" | default dict }} +{{- if get $np "enabled" }} +{{- $pd := .Values.pd.ports }} +{{- $store := .Values.store.ports }} +{{- $serverPort := .Values.server.port }} +{{- $hubbleEnabled := .Values.hubble.enabled }} +{{- $hubblePd := and $hubbleEnabled (eq .Values.hubble.mode "pd") }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "hugegraph.pd.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: pd +spec: + podSelector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: pd + policyTypes: + - Ingress + - Egress + ingress: + - from: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "pd") | nindent 8 }} + ports: + {{- include "hugegraph.netpol.ports" (list $pd.raft $pd.grpc) | nindent 8 }} + - from: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "store") | nindent 8 }} + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "server") | nindent 8 }} + {{- if $hubblePd }} + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "hubble") | nindent 8 }} + {{- end }} + ports: + {{- include "hugegraph.netpol.ports" (list $pd.grpc $pd.rest) | nindent 8 }} + {{- with get (get $np "pd" | default dict) "extraIngress" }} + {{- toYaml . | nindent 4 }} + {{- end }} + egress: + - to: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "pd") | nindent 8 }} + ports: + {{- include "hugegraph.netpol.ports" (list $pd.raft $pd.grpc) | nindent 8 }} + {{- include "hugegraph.netpol.dns" . | nindent 4 }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "hugegraph.store.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: store +spec: + podSelector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: store + policyTypes: + - Ingress + - Egress + ingress: + - from: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "store") | nindent 8 }} + ports: + {{- include "hugegraph.netpol.ports" (list $store.raft) | nindent 8 }} + - from: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "server") | nindent 8 }} + ports: + {{- include "hugegraph.netpol.ports" (list $store.grpc $store.rest) | nindent 8 }} + {{- if $hubblePd }} + - from: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "hubble") | nindent 8 }} + ports: + {{- include "hugegraph.netpol.ports" (list $store.rest) | nindent 8 }} + {{- end }} + {{- with get (get $np "store" | default dict) "extraIngress" }} + {{- toYaml . | nindent 4 }} + {{- end }} + egress: + - to: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "pd") | nindent 8 }} + ports: + {{- include "hugegraph.netpol.ports" (list $pd.grpc $pd.rest) | nindent 8 }} + - to: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "store") | nindent 8 }} + ports: + {{- include "hugegraph.netpol.ports" (list $store.raft) | nindent 8 }} + {{- include "hugegraph.netpol.dns" . | nindent 4 }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "hugegraph.server.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + podSelector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: server + policyTypes: + - Ingress + - Egress + ingress: + - from: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "test") | nindent 8 }} + {{- if $hubbleEnabled }} + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "hubble") | nindent 8 }} + {{- end }} + ports: + {{- include "hugegraph.netpol.ports" (list $serverPort) | nindent 8 }} + {{- with get (get $np "server" | default dict) "extraIngress" }} + {{- toYaml . | nindent 4 }} + {{- end }} + egress: + - to: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "pd") | nindent 8 }} + ports: + {{- include "hugegraph.netpol.ports" (list $pd.grpc $pd.rest) | nindent 8 }} + - to: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "store") | nindent 8 }} + ports: + {{- include "hugegraph.netpol.ports" (list $store.grpc $store.rest) | nindent 8 }} + {{- include "hugegraph.netpol.dns" . | nindent 4 }} +{{- if $hubbleEnabled }} +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "hugegraph.hubble.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: hubble +spec: + podSelector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: hubble + policyTypes: + - Ingress + - Egress + {{- with get (get $np "hubble" | default dict) "extraIngress" }} + ingress: + {{- toYaml . | nindent 4 }} + {{- else }} + # No ingress rules under policyTypes Ingress admits nothing. The key is left + # out rather than rendered empty, because the API server drops an empty list + # and Helm would then rewrite the object on every upgrade. kubectl + # port-forward reaches the Pod over loopback, which no policy filters. + {{- end }} + egress: + - to: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "server") | nindent 8 }} + ports: + {{- include "hugegraph.netpol.ports" (list $serverPort) | nindent 8 }} + {{- if $hubblePd }} + - to: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "pd") | nindent 8 }} + ports: + {{- include "hugegraph.netpol.ports" (list $pd.grpc $pd.rest) | nindent 8 }} + - to: + {{- include "hugegraph.netpol.peer" (dict "root" . "component" "store") | nindent 8 }} + ports: + {{- include "hugegraph.netpol.ports" (list $store.rest) | nindent 8 }} + {{- end }} + {{- include "hugegraph.netpol.dns" . | nindent 4 }} + {{- with get (get $np "hubble" | default dict) "extraEgress" }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/pd-auth-secret.yaml b/helm/hugegraph/templates/pd-auth-secret.yaml new file mode 100644 index 0000000000..a699447c49 --- /dev/null +++ b/helm/hugegraph/templates/pd-auth-secret.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- $auth := get .Values.pd "auth" | default dict -}} +{{- $autoGen := ternary (get $auth "autoGenerate") true (hasKey $auth "autoGenerate") -}} +{{- if and (not (get $auth "existingSecret" | default "")) (or (get $auth "value" | default "") $autoGen) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "hugegraph.pd.authSecretName" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + annotations: + helm.sh/resource-policy: keep +type: Opaque +data: + {{ include "hugegraph.pd.authSecretKey" . | quote }}: {{ include "hugegraph.pd.authSecretValue" . | quote }} +{{- end }} diff --git a/helm/hugegraph/templates/pd-pdb.yaml b/helm/hugegraph/templates/pd-pdb.yaml new file mode 100644 index 0000000000..492fad2ca1 --- /dev/null +++ b/helm/hugegraph/templates/pd-pdb.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- if and .Values.pd.pdb.enabled (gt (int .Values.pd.replicas) 1) }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "hugegraph.pd.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: pd +spec: + minAvailable: {{ .Values.pd.pdb.minAvailable }} + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: pd +{{- end }} diff --git a/helm/hugegraph/templates/pd-service-client.yaml b/helm/hugegraph/templates/pd-service-client.yaml new file mode 100644 index 0000000000..f9d1373836 --- /dev/null +++ b/helm/hugegraph/templates/pd-service-client.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- $svc := get .Values.pd "service" | default dict }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hugegraph.pd.clientName" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: pd + {{- with (get $svc "annotations") }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ get $svc "type" | default "ClusterIP" }} + {{- range $field := list "externalTrafficPolicy" "loadBalancerClass" "loadBalancerIP" "loadBalancerSourceRanges" "externalIPs" }} + {{- with get $svc $field }} + {{ $field }}: {{ toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- if hasKey $svc "allocateLoadBalancerNodePorts" }} + {{- if ne (get $svc "allocateLoadBalancerNodePorts") nil }} + allocateLoadBalancerNodePorts: {{ get $svc "allocateLoadBalancerNodePorts" }} + {{- end }} + {{- end }} + selector: + {{- include "hugegraph.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: pd + ports: + - name: rest + port: {{ .Values.pd.ports.rest }} + targetPort: rest + {{- with (get $svc "restNodePort") }} + nodePort: {{ . }} + {{- end }} + - name: grpc + port: {{ .Values.pd.ports.grpc }} + targetPort: grpc + {{- with (get $svc "grpcNodePort") }} + nodePort: {{ . }} + {{- end }} diff --git a/helm/hugegraph/templates/pd-service-headless.yaml b/helm/hugegraph/templates/pd-service-headless.yaml new file mode 100644 index 0000000000..5a42745811 --- /dev/null +++ b/helm/hugegraph/templates/pd-service-headless.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hugegraph.pd.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: pd +spec: + clusterIP: None + # Mandatory: Raft bootstrap needs DNS for not-yet-ready pods + publishNotReadyAddresses: true + selector: + {{- include "hugegraph.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: pd + ports: + - name: grpc + port: {{ .Values.pd.ports.grpc }} + targetPort: grpc + - name: rest + port: {{ .Values.pd.ports.rest }} + targetPort: rest + - name: raft + port: {{ .Values.pd.ports.raft }} + targetPort: raft diff --git a/helm/hugegraph/templates/pd-statefulset.yaml b/helm/hugegraph/templates/pd-statefulset.yaml new file mode 100644 index 0000000000..0546070ac8 --- /dev/null +++ b/helm/hugegraph/templates/pd-statefulset.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "hugegraph.pd.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: pd +spec: + serviceName: {{ include "hugegraph.pd.name" . }} + replicas: {{ .Values.pd.replicas }} + podManagementPolicy: Parallel + {{- with .Values.pd.updateStrategy }} + updateStrategy: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if .Values.pd.storage.enabled }} + {{- with .Values.pd.persistentVolumeClaimRetentionPolicy }} + persistentVolumeClaimRetentionPolicy: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: pd + template: + metadata: + labels: + {{- include "hugegraph.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: pd + {{- with .Values.pd.podLabels }}{{ toYaml . | nindent 8 }}{{- end }} + annotations: + # Rolls PD pods when the resolved REST auth Secret changes, so rotating + # an existingSecret takes effect without a manual restart. + checksum/pd-auth: {{ include "hugegraph.pd.authChecksum" . | quote }} + {{- with .Values.pd.podAnnotations }}{{ toYaml . | nindent 8 }}{{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ get (get .Values.pd "serviceAccount" | default dict) "automountServiceAccountToken" | default false }} + serviceAccountName: {{ include "hugegraph.serviceAccountName" (dict "component" .Values.pd "name" (include "hugegraph.pd.name" .) ) }} + {{- if hasKey .Values.pd "terminationGracePeriodSeconds" }} + terminationGracePeriodSeconds: {{ .Values.pd.terminationGracePeriodSeconds }} + {{- end }} + {{- with .Values.pd.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.pd.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.pd.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.pd.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.pd.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.pd.affinity }} + affinity: + {{- toYaml .Values.pd.affinity | nindent 8 }} + {{- else }} + {{- with include "hugegraph.antiAffinity" (dict "mode" .Values.pd.antiAffinity "component" "pd" "labels" (include "hugegraph.selectorLabels" . | fromYaml)) }}{{ . | trim | nindent 6 }}{{- end }} + {{- end }} + containers: + - name: pd + image: {{ include "hugegraph.image" (dict "image" .Values.pd.image "appVersion" $.Chart.AppVersion) | quote }} + imagePullPolicy: {{ .Values.pd.image.pullPolicy }} + {{- with .Values.pd.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: grpc + containerPort: {{ .Values.pd.ports.grpc }} + - name: rest + containerPort: {{ .Values.pd.ports.rest }} + - name: raft + containerPort: {{ .Values.pd.ports.raft }} + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: HG_PD_GRPC_HOST + value: "$(POD_NAME).{{ include "hugegraph.pd.name" . }}.$(NAMESPACE).svc" + - name: HG_PD_GRPC_PORT + value: {{ .Values.pd.ports.grpc | quote }} + - name: HG_PD_REST_PORT + value: {{ .Values.pd.ports.rest | quote }} + - name: HG_PD_RAFT_ADDRESS + value: "$(POD_NAME).{{ include "hugegraph.pd.name" . }}.$(NAMESPACE).svc:{{ .Values.pd.ports.raft }}" + - name: HG_PD_RAFT_PEERS_LIST + value: {{ include "hugegraph.pd.raftPeersList" . | quote }} + - name: HG_PD_INITIAL_STORE_LIST + value: {{ include "hugegraph.store.initialStoreList" . | quote }} + - name: HG_PD_INITIAL_STORE_COUNT + value: {{ .Values.store.replicas | quote }} + - name: HG_PD_DATA_PATH + value: {{ .Values.pd.dataPath | quote }} + - name: HG_PD_AUTH_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ include "hugegraph.pd.authSecretName" . | quote }} + key: {{ include "hugegraph.pd.authSecretKey" . | quote }} + {{- with .Values.pd.extraEnv }}{{ toYaml . | nindent 12 }}{{- end }} + {{- with include "hugegraph.javaOptsEnv" (include "hugegraph.pd.effectiveJavaOpts" .) }}{{ . | trim | nindent 12 }}{{- end }} + volumeMounts: + - name: pd-data + mountPath: {{ .Values.pd.dataPath }} + startupProbe: + httpGet: + path: {{ include "hugegraph.pd.livenessPath" . }} + port: rest + failureThreshold: {{ .Values.pd.probes.startup.failureThreshold }} + periodSeconds: {{ .Values.pd.probes.startup.periodSeconds }} + {{- with include "hugegraph.probeTuning" .Values.pd.probes.startup }}{{ . | trim | nindent 12 }}{{- end }} + readinessProbe: + httpGet: + path: {{ .Values.pd.readinessPath | default "/v1/ready" }} + port: rest + periodSeconds: {{ .Values.pd.probes.readiness.periodSeconds }} + failureThreshold: {{ .Values.pd.probes.readiness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.pd.probes.readiness }}{{ . | trim | nindent 12 }}{{- end }} + livenessProbe: + httpGet: + path: {{ include "hugegraph.pd.livenessPath" . }} + port: rest + periodSeconds: {{ .Values.pd.probes.liveness.periodSeconds }} + failureThreshold: {{ .Values.pd.probes.liveness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.pd.probes.liveness }}{{ . | trim | nindent 12 }}{{- end }} + {{- with .Values.pd.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if not .Values.pd.storage.enabled }} + volumes: + - name: pd-data + emptyDir: {} + {{- end }} + {{- if .Values.pd.storage.enabled }} + volumeClaimTemplates: + - metadata: + name: pd-data + spec: + accessModes: ["ReadWriteOnce"] + {{- if eq .Values.pd.storage.storageClassName "-" }} + storageClassName: "" + {{- else if .Values.pd.storage.storageClassName }} + storageClassName: {{ .Values.pd.storage.storageClassName | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.pd.storage.size }} + {{- end }} diff --git a/helm/hugegraph/templates/server-auth-token-secret.yaml b/helm/hugegraph/templates/server-auth-token-secret.yaml new file mode 100644 index 0000000000..340d04571d --- /dev/null +++ b/helm/hugegraph/templates/server-auth-token-secret.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- $auth := get .Values.server "auth" | default dict -}} +{{- $token := get $auth "token" | default dict -}} +{{- if and (get $auth "enabled" | default false) (not (get $token "existingSecret" | default "")) (or (get $token "value" | default "") (get $token "autoGenerate" | default false)) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "hugegraph.server.authTokenSecretName" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + annotations: + helm.sh/resource-policy: keep +type: Opaque +data: + {{ include "hugegraph.server.authTokenSecretKey" . | quote }}: {{ include "hugegraph.server.authTokenSecretValue" . | quote }} +{{- end }} diff --git a/helm/hugegraph/templates/server-deployment.yaml b/helm/hugegraph/templates/server-deployment.yaml new file mode 100644 index 0000000000..6c10a8d33a --- /dev/null +++ b/helm/hugegraph/templates/server-deployment.yaml @@ -0,0 +1,332 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +{{ include "hugegraph.resolveValues" . }} +{{- include "hugegraph.validateValues" . }} +{{- $customPort := ne (int .Values.server.port) 8080 }} +{{/* +Distributed HStore requires every Server replica to share graph metadata +through PD. The same registration properties let PD-mode Hubble discover the +Server and let the built-in authenticator create the admin on the PD path, +so the config-rewrite wrapper below runs unconditionally. +*/}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "hugegraph.server.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + {{- if not .Values.server.hpa.enabled }} + replicas: {{ .Values.server.replicas }} + {{- end }} + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: server + template: + metadata: + labels: + {{- include "hugegraph.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: server + {{- with .Values.server.podLabels }}{{ toYaml . | nindent 8 }}{{- end }} + annotations: + {{- if .Values.server.auth.enabled }} + # Rolls Server pods when the resolved auth Secrets change, so rotating + # an existingSecret takes effect without a manual restart. + checksum/auth: {{ include "hugegraph.server.authChecksum" . | quote }} + {{- end }} + # Rolls Server pods when the PD REST Secret its storage wait uses changes. + checksum/pd-auth: {{ include "hugegraph.pd.authChecksum" . | quote }} + {{- with .Values.server.podAnnotations }}{{ toYaml . | nindent 8 }}{{- end }} + spec: + automountServiceAccountToken: {{ get (get .Values.server "serviceAccount" | default dict) "automountServiceAccountToken" | default false }} + serviceAccountName: {{ include "hugegraph.serviceAccountName" (dict "component" .Values.server "name" (include "hugegraph.server.name" .) ) }} + {{- if hasKey .Values.server "terminationGracePeriodSeconds" }} + terminationGracePeriodSeconds: {{ .Values.server.terminationGracePeriodSeconds }} + {{- end }} + {{- with .Values.server.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.server.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.server.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.server.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.server.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.server.affinity }} + affinity: + {{- toYaml .Values.server.affinity | nindent 8 }} + {{- else }} + {{- with include "hugegraph.antiAffinity" (dict "mode" (.Values.server.antiAffinity | default "preferred") "component" "server" "labels" (include "hugegraph.selectorLabels" . | fromYaml)) }}{{ . | trim | nindent 6 }}{{- end }} + {{- end }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: server + image: {{ include "hugegraph.image" (dict "image" .Values.server.image "appVersion" $.Chart.AppVersion) | quote }} + imagePullPolicy: {{ .Values.server.image.pullPolicy }} + {{- with .Values.server.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + command: + - /usr/bin/dumb-init + - -- + - /bin/bash + - -c + args: + - | + set -euo pipefail + {{- if .Values.server.auth.enabled }} + : "${PASSWORD:?auth Secret key 'password' must not be empty}" + # The value is written into a Java properties file, whose parser + # treats CR as a line terminator, unescapes backslashes, and + # trims whitespace around the value, so these shapes would + # inject config lines or silently store a password that differs + # from the Secret. + case "${PASSWORD}" in + *' '*) + # Temporary: the image entrypoint writes auth.admin_pa with + # each space escaped as a backslash and a space, and the + # Server keeps that backslash, so the account gets a + # password the Secret does not hold. Drop this case and the + # space exclusion in the schema pattern once the image + # encoder round-trips spaces (TODO at encode_prop_value in + # docker-entrypoint.sh, apache/hugegraph#3260). + echo "auth Secret key 'password' must not contain spaces: the" \ + "Server image stores each space in the admin password with" \ + "a backslash before it, so the account would answer 401 to" \ + "the Secret value; fix the server.auth.admin Secret value" >&2 + exit 1 + ;; + *\\*) + echo "auth Secret key 'password' must not contain backslashes;" \ + "fix the server.auth.admin Secret value" >&2 + exit 1 + ;; + *:*) + echo "auth Secret key 'password' must not contain a colon: the" \ + "Server splits the decoded Basic-auth credential on every" \ + "colon and answers 400 to such a password; fix the" \ + "server.auth.admin Secret value" >&2 + exit 1 + ;; + esac + # Printable ASCII only, checked byte-wise under the C locale so + # the image locale cannot widen it. The Server decodes the + # Basic-auth header as ASCII, so a non-ASCII password is + # accepted here, applied to the admin account, and then answers + # 401 to every request made with it. Temporary: drop this check, + # the colon case above and the matching schema pattern once the + # Server decodes Basic auth as UTF-8 and splits the credential on + # its first colon only (apache/hugegraph#3284) in the image this + # chart deploys. + if (( $(printf '%s' "${PASSWORD}" | LC_ALL=C tr -d '\040-\176' | wc -c) > 0 )); then + echo "auth Secret key 'password' must be printable ASCII (bytes" \ + "0x20-0x7E: no tabs, newlines or non-ASCII characters): the" \ + "Server decodes Basic-auth credentials as ASCII, so a" \ + "non-ASCII password would be applied and then rejected" \ + "with 401 on every request; fix the server.auth.admin" \ + "Secret value" >&2 + exit 1 + fi + {{- end }} + CONF=./conf/rest-server.properties + if [[ ! -r "${CONF}" ]]; then + echo "missing or unreadable ${CONF}; refusing to write a stub config" >&2 + exit 1 + fi + TMP=$(mktemp) + FOUND_USE_PD=false + FOUND_PD_PEERS=false + FOUND_URLS_TO_PD=false + FOUND_DEPLOY_IN_K8S=false + # An IPv6 POD_IP must be bracketed inside the announcement URL. + # Only the chart's own default is rewritten; an explicit + # server.advertiseUrl is the operator's to format. + if [[ "${POD_IP:-}" == *:* && "${HG_SERVER_URLS_TO_PD:-}" == "http://${POD_IP}:"* ]]; then + HG_SERVER_URLS_TO_PD="http://[${POD_IP}]:${HG_SERVER_URLS_TO_PD##*:}" + fi + {{- if .Values.server.auth.enabled }} + FOUND_AUTH_ADMIN_PA=false + {{- end }} + {{- if $customPort }} + FOUND_RESTSERVER_URL=false + {{- end }} + while IFS= read -r LINE || [[ -n "${LINE}" ]]; do + case "${LINE}" in + usePD=*) + printf 'usePD=true\n' >>"${TMP}" + FOUND_USE_PD=true + ;; + pd.peers=*) + printf 'pd.peers=%s\n' "${HG_SERVER_PD_PEERS}" >>"${TMP}" + FOUND_PD_PEERS=true + ;; + server.urls_to_pd=*) + printf 'server.urls_to_pd=%s\n' "${HG_SERVER_URLS_TO_PD}" >>"${TMP}" + FOUND_URLS_TO_PD=true + ;; + server.deploy_in_k8s=*) + printf 'server.deploy_in_k8s=true\n' >>"${TMP}" + FOUND_DEPLOY_IN_K8S=true + ;; + {{- if .Values.server.auth.enabled }} + auth.admin_pa=*) + printf 'auth.admin_pa=%s\n' "${PASSWORD}" >>"${TMP}" + FOUND_AUTH_ADMIN_PA=true + ;; + {{- end }} + {{- if $customPort }} + restserver.url=*) + printf 'restserver.url=http://0.0.0.0:%s\n' \ + {{ .Values.server.port | quote }} >>"${TMP}" + FOUND_RESTSERVER_URL=true + ;; + {{- end }} + *) + printf '%s\n' "${LINE}" >>"${TMP}" + ;; + esac + done <"${CONF}" + if [[ "${FOUND_USE_PD}" == false ]]; then + printf 'usePD=true\n' >>"${TMP}" + fi + if [[ "${FOUND_PD_PEERS}" == false ]]; then + printf 'pd.peers=%s\n' "${HG_SERVER_PD_PEERS}" >>"${TMP}" + fi + # PD hands this URL to discovery clients such as Hubble. + # HG_SERVER_URLS_TO_PD uses server.advertiseUrl when set, + # otherwise the in-cluster Server Service URL. The k8s + # branch is taken only when server.deploy_in_k8s is true; + # otherwise the announcement falls back to restserver.url, + # whose 0.0.0.0 is never resolvable from another Pod. + if [[ "${FOUND_URLS_TO_PD}" == false ]]; then + printf 'server.urls_to_pd=%s\n' "${HG_SERVER_URLS_TO_PD}" >>"${TMP}" + fi + if [[ "${FOUND_DEPLOY_IN_K8S}" == false ]]; then + printf 'server.deploy_in_k8s=true\n' >>"${TMP}" + fi + {{- if .Values.server.auth.enabled }} + if [[ "${FOUND_AUTH_ADMIN_PA}" == false ]]; then + printf 'auth.admin_pa=%s\n' "${PASSWORD}" >>"${TMP}" + fi + {{- end }} + {{- if $customPort }} + if [[ "${FOUND_RESTSERVER_URL}" == false ]]; then + printf 'restserver.url=http://0.0.0.0:%s\n' \ + {{ .Values.server.port | quote }} >>"${TMP}" + fi + {{- end }} + chmod 600 "${TMP}" + mv "${TMP}" "${CONF}" + exec ./docker-entrypoint.sh + ports: + - name: http + containerPort: {{ .Values.server.port }} + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: HG_SERVER_BACKEND + value: {{ .Values.server.backend | quote }} + {{- if ne (.Values.server.serviceId | default "DEFAULT") "DEFAULT" }} + - name: HG_SERVER_SERVICE_ID + value: {{ .Values.server.serviceId | replace "$" "$$" | quote }} + {{- end }} + {{- if ne (.Values.server.graphSpace | default "DEFAULT") "DEFAULT" }} + - name: HG_SERVER_GRAPHSPACE + value: {{ .Values.server.graphSpace | quote }} + {{- end }} + - name: HG_SERVER_PD_PEERS + value: {{ include "hugegraph.pd.grpcPeersList" . | quote }} + - name: HG_SERVER_PD_REST_ENDPOINT + value: {{ include "hugegraph.pd.restPeersList" . | quote }} + # wait-storage.sh authenticates its PD readiness checks as the + # store service user with this password. + - name: PD_AUTH_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "hugegraph.pd.authSecretName" . | quote }} + key: {{ include "hugegraph.pd.authSecretKey" . | quote }} + - name: STORE_REST + value: {{ include "hugegraph.store.restPrimary" . | quote }} + - name: HG_SERVER_INIT_STORE_ENABLED + value: {{ .Values.server.initStoreEnabled | quote }} + # The image would otherwise give the start command 120 seconds, + # less than the storage wait, and kill a Server that is still + # coming up. Track the startup probe's own budget instead. + - name: WAIT_STORAGE_TIMEOUT_S + value: {{ .Values.server.storageWaitTimeoutSeconds | quote }} + - name: HG_SERVER_STARTUP_TIMEOUT_S + value: {{ include "hugegraph.server.startupTimeoutSeconds" . | quote }} + - name: HG_SERVER_URLS_TO_PD + value: {{ include "hugegraph.server.urlsToPd" . | quote }} + {{- with .Values.server.extraEnv }}{{ toYaml . | nindent 12 }}{{- end }} + {{- with include "hugegraph.javaOptsEnv" .Values.server.javaOpts }}{{ . | trim | nindent 12 }}{{- end }} + {{- if .Values.server.auth.enabled }} + - name: PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "hugegraph.server.authSecretName" . | quote }} + key: {{ include "hugegraph.server.authSecretKey" . | quote }} + - name: HG_SERVER_AUTH_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: {{ include "hugegraph.server.authTokenSecretName" . | quote }} + key: {{ include "hugegraph.server.authTokenSecretKey" . | quote }} + {{- end }} + startupProbe: + httpGet: + path: /versions + port: http + failureThreshold: {{ include "hugegraph.server.startupFailureThreshold" . }} + periodSeconds: {{ .Values.server.probes.startup.periodSeconds }} + {{- with include "hugegraph.probeTuning" .Values.server.probes.startup }}{{ . | trim | nindent 12 }}{{- end }} + readinessProbe: + httpGet: + path: {{ .Values.server.readinessPath | default "/versions" }} + port: http + periodSeconds: {{ .Values.server.probes.readiness.periodSeconds }} + failureThreshold: {{ .Values.server.probes.readiness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.server.probes.readiness }}{{ . | trim | nindent 12 }}{{- end }} + livenessProbe: + httpGet: + path: /versions + port: http + periodSeconds: {{ .Values.server.probes.liveness.periodSeconds }} + failureThreshold: {{ .Values.server.probes.liveness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.server.probes.liveness }}{{ . | trim | nindent 12 }}{{- end }} + {{- with .Values.server.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} diff --git a/helm/hugegraph/templates/server-hpa.yaml b/helm/hugegraph/templates/server-hpa.yaml new file mode 100644 index 0000000000..0ab64c3d62 --- /dev/null +++ b/helm/hugegraph/templates/server-hpa.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- if .Values.server.hpa.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "hugegraph.server.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "hugegraph.server.name" . }} + minReplicas: {{ .Values.server.hpa.minReplicas }} + maxReplicas: {{ .Values.server.hpa.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.server.hpa.targetCPUUtilizationPercentage }} +{{- end }} diff --git a/helm/hugegraph/templates/server-ingress.yaml b/helm/hugegraph/templates/server-ingress.yaml new file mode 100644 index 0000000000..2f88becd4f --- /dev/null +++ b/helm/hugegraph/templates/server-ingress.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- if .Values.server.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "hugegraph.server.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: server + {{- with (get .Values.server.ingress "annotations") }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.server.ingress.className }} + ingressClassName: {{ .Values.server.ingress.className | quote }} + {{- end }} + {{- with .Values.server.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range (.Values.server.ingress.hosts | default (list (dict "paths" (list (dict "path" "/" "pathType" "Prefix"))))) }} + - {{- with .host }} + host: {{ . | quote }} + {{- end }} + http: + paths: + {{- range (.paths | default (list (dict "path" "/" "pathType" "Prefix"))) }} + - path: {{ .path | quote }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "hugegraph.server.name" $ }} + port: + number: {{ $.Values.server.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/server-pdb.yaml b/helm/hugegraph/templates/server-pdb.yaml new file mode 100644 index 0000000000..0fdd176d3a --- /dev/null +++ b/helm/hugegraph/templates/server-pdb.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- $pdb := get .Values.server "pdb" | default dict }} +{{- $replicaFloor := include "hugegraph.server.replicaFloor" . | int }} +{{- if and (get $pdb "enabled" | default false) (gt $replicaFloor 1) }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "hugegraph.server.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + minAvailable: {{ get $pdb "minAvailable" | default 1 }} + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: server +{{- end }} diff --git a/helm/hugegraph/templates/server-secret.yaml b/helm/hugegraph/templates/server-secret.yaml new file mode 100644 index 0000000000..6b16c661de --- /dev/null +++ b/helm/hugegraph/templates/server-secret.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- $auth := get .Values.server "auth" | default dict -}} +{{- $admin := get $auth "admin" | default dict -}} +{{- if and (get $auth "enabled" | default false) (not (get $admin "existingSecret" | default "")) (or (get $admin "password" | default "") (get $admin "autoGenerate" | default false)) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "hugegraph.server.authSecretName" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + annotations: + helm.sh/resource-policy: keep +type: Opaque +data: + {{ include "hugegraph.server.authSecretKey" . | quote }}: {{ include "hugegraph.server.authSecretPassword" . | quote }} +{{- end }} diff --git a/helm/hugegraph/templates/server-service-headless.yaml b/helm/hugegraph/templates/server-service-headless.yaml new file mode 100644 index 0000000000..6f46957514 --- /dev/null +++ b/helm/hugegraph/templates/server-service-headless.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +# Headless Service enumerating the individual Server Pods. The helm test +# hook resolves it to run its graph-bound Gremlin check against every Pod +# (a broken-binding Pod passes readiness, so it appears here), and the +# per-Pod troubleshooting steps in the README can use it too. +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hugegraph.server.headlessName" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: server +spec: + clusterIP: None + # Deliberately not publishNotReadyAddresses: the checks want exactly the + # Pods the client Service routes to. + selector: + {{- include "hugegraph.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: server + ports: + - name: http + port: {{ .Values.server.port }} + targetPort: http diff --git a/helm/hugegraph/templates/server-service.yaml b/helm/hugegraph/templates/server-service.yaml new file mode 100644 index 0000000000..f183946e86 --- /dev/null +++ b/helm/hugegraph/templates/server-service.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hugegraph.server.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: server + {{- $svc := get .Values.server "service" | default dict }} + {{- with (get $svc "annotations") }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ get $svc "type" | default "ClusterIP" }} + {{- range $field := list "externalTrafficPolicy" "loadBalancerClass" "loadBalancerIP" "loadBalancerSourceRanges" "externalIPs" }} + {{- with get $svc $field }} + {{ $field }}: {{ toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- if hasKey $svc "allocateLoadBalancerNodePorts" }} + {{- if ne (get $svc "allocateLoadBalancerNodePorts") nil }} + allocateLoadBalancerNodePorts: {{ get $svc "allocateLoadBalancerNodePorts" }} + {{- end }} + {{- end }} + selector: + {{- include "hugegraph.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: server + ports: + - name: http + port: {{ .Values.server.port }} + targetPort: http + {{- with (get $svc "nodePort") }} + nodePort: {{ . }} + {{- end }} diff --git a/helm/hugegraph/templates/serviceaccount.yaml b/helm/hugegraph/templates/serviceaccount.yaml new file mode 100644 index 0000000000..cb8e08139a --- /dev/null +++ b/helm/hugegraph/templates/serviceaccount.yaml @@ -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. +*/ -}} +{{ include "hugegraph.resolveValues" . }} +{{- $components := list "pd" "store" "server" }} +{{- if get (get .Values "hubble" | default dict) "enabled" | default false }} +{{- $components = append $components "hubble" }} +{{- end }} +{{- range $component := $components }} +{{- $values := index $.Values $component }} +{{- $sa := get $values "serviceAccount" | default dict }} +{{- if and (get $sa "create" | default false) (not (get $sa "name")) }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include (printf "hugegraph.%s.name" $component) $ }} + labels: + {{- include "hugegraph.labels" $ | nindent 4 }} + app.kubernetes.io/component: {{ $component }} + {{- with (get $sa "annotations") }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ get $sa "automountServiceAccountToken" | default false }} +{{- end }} +{{- end }} diff --git a/helm/hugegraph/templates/store-pdb.yaml b/helm/hugegraph/templates/store-pdb.yaml new file mode 100644 index 0000000000..1b70b7f02e --- /dev/null +++ b/helm/hugegraph/templates/store-pdb.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- if and .Values.store.pdb.enabled (gt (int .Values.store.replicas) 1) }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "hugegraph.store.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: store +spec: + minAvailable: {{ .Values.store.pdb.minAvailable }} + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: store +{{- end }} diff --git a/helm/hugegraph/templates/store-service-headless.yaml b/helm/hugegraph/templates/store-service-headless.yaml new file mode 100644 index 0000000000..4bf790155a --- /dev/null +++ b/helm/hugegraph/templates/store-service-headless.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hugegraph.store.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: store +spec: + clusterIP: None + # Mandatory: Raft / self-FQDN resolution before readiness + publishNotReadyAddresses: true + selector: + {{- include "hugegraph.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: store + ports: + - name: grpc + port: {{ .Values.store.ports.grpc }} + targetPort: grpc + - name: raft + port: {{ .Values.store.ports.raft }} + targetPort: raft + - name: rest + port: {{ .Values.store.ports.rest }} + targetPort: rest diff --git a/helm/hugegraph/templates/store-statefulset.yaml b/helm/hugegraph/templates/store-statefulset.yaml new file mode 100644 index 0000000000..81c143ee8a --- /dev/null +++ b/helm/hugegraph/templates/store-statefulset.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "hugegraph.store.name" . }} + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: store +spec: + serviceName: {{ include "hugegraph.store.name" . }} + replicas: {{ .Values.store.replicas }} + podManagementPolicy: Parallel + {{- with .Values.store.updateStrategy }} + updateStrategy: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if .Values.store.storage.enabled }} + {{- with .Values.store.persistentVolumeClaimRetentionPolicy }} + persistentVolumeClaimRetentionPolicy: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + selector: + matchLabels: + {{- include "hugegraph.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: store + template: + metadata: + labels: + {{- include "hugegraph.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: store + {{- with .Values.store.podLabels }}{{ toYaml . | nindent 8 }}{{- end }} + {{- with .Values.store.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ get (get .Values.store "serviceAccount" | default dict) "automountServiceAccountToken" | default false }} + serviceAccountName: {{ include "hugegraph.serviceAccountName" (dict "component" .Values.store "name" (include "hugegraph.store.name" .) ) }} + {{- if hasKey .Values.store "terminationGracePeriodSeconds" }} + terminationGracePeriodSeconds: {{ .Values.store.terminationGracePeriodSeconds }} + {{- end }} + {{- with .Values.store.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.store.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.store.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.store.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.store.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.store.affinity }} + affinity: + {{- toYaml .Values.store.affinity | nindent 8 }} + {{- else }} + {{- with include "hugegraph.antiAffinity" (dict "mode" .Values.store.antiAffinity "component" "store" "labels" (include "hugegraph.selectorLabels" . | fromYaml)) }}{{ . | trim | nindent 6 }}{{- end }} + {{- end }} + initContainers: + - name: wait-for-pd + image: {{ .Values.store.waitImage | quote }} + {{- if .Values.store.__waitImageInherited }} + imagePullPolicy: {{ .Values.store.image.pullPolicy }} + {{- end }} + {{- with .Values.store.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + command: + - sh + - -c + - | + set -eu + REQUIRED={{ include "hugegraph.pd.quorum" . }} + HEALTH_PEERS=$(echo "{{ include "hugegraph.pd.restPeersList" . }}" | tr ',' ' ') + TIMEOUT={{ .Values.store.waitTimeoutSeconds | default 900 }} + DEADLINE=$(( $(date +%s) + TIMEOUT )) + WAIT_PATH={{ .Values.store.waitPath | default "/v1/ready" | quote }} + echo "Waiting for ${REQUIRED} PD peers to answer ${WAIT_PATH} among: ${HEALTH_PEERS}" + until [ "$( + ok=0 + for peer in ${HEALTH_PEERS}; do + if curl -fsS --connect-timeout 2 --max-time 5 "http://${peer}${WAIT_PATH}" >/dev/null 2>&1; then + ok=$((ok+1)) + fi + done + echo "$ok" + )" -ge "${REQUIRED}" ]; do + if [ "$(date +%s)" -ge "${DEADLINE}" ]; then + echo "Timed out after ${TIMEOUT}s waiting for ${REQUIRED} PD peers to answer ${WAIT_PATH} among: ${HEALTH_PEERS}" >&2 + echo "Check PD Pods: kubectl get pods -l app.kubernetes.io/component=pd" >&2 + exit 1 + fi + echo "Waiting for PD peers..." + sleep 5 + done + echo "Enough PD peers answered ${WAIT_PATH}." + {{- with .Values.store.waitResources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + containers: + - name: store + image: {{ include "hugegraph.image" (dict "image" .Values.store.image "appVersion" $.Chart.AppVersion) | quote }} + imagePullPolicy: {{ .Values.store.image.pullPolicy }} + {{- with .Values.store.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: grpc + containerPort: {{ .Values.store.ports.grpc }} + - name: raft + containerPort: {{ .Values.store.ports.raft }} + - name: rest + containerPort: {{ .Values.store.ports.rest }} + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: HG_STORE_PD_ADDRESS + value: {{ include "hugegraph.pd.grpcPeersList" . | quote }} + - name: HG_STORE_GRPC_HOST + value: "$(POD_NAME).{{ include "hugegraph.store.name" . }}.$(NAMESPACE).svc" + - name: HG_STORE_GRPC_PORT + value: {{ .Values.store.ports.grpc | quote }} + - name: HG_STORE_REST_PORT + value: {{ .Values.store.ports.rest | quote }} + - name: HG_STORE_RAFT_ADDRESS + value: "$(POD_NAME).{{ include "hugegraph.store.name" . }}.$(NAMESPACE).svc:{{ .Values.store.ports.raft }}" + - name: HG_STORE_DATA_PATH + value: {{ .Values.store.dataPath | quote }} + {{- with .Values.store.extraEnv }}{{ toYaml . | nindent 12 }}{{- end }} + {{- with include "hugegraph.javaOptsEnv" .Values.store.javaOpts }}{{ . | trim | nindent 12 }}{{- end }} + volumeMounts: + - name: store-data + mountPath: {{ .Values.store.dataPath }} + startupProbe: + httpGet: + path: /v1/health + port: rest + failureThreshold: {{ .Values.store.probes.startup.failureThreshold }} + periodSeconds: {{ .Values.store.probes.startup.periodSeconds }} + {{- with include "hugegraph.probeTuning" .Values.store.probes.startup }}{{ . | trim | nindent 12 }}{{- end }} + readinessProbe: + httpGet: + path: /v1/health + port: rest + periodSeconds: {{ .Values.store.probes.readiness.periodSeconds }} + failureThreshold: {{ .Values.store.probes.readiness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.store.probes.readiness }}{{ . | trim | nindent 12 }}{{- end }} + livenessProbe: + httpGet: + path: /v1/health + port: rest + periodSeconds: {{ .Values.store.probes.liveness.periodSeconds }} + failureThreshold: {{ .Values.store.probes.liveness.failureThreshold }} + {{- with include "hugegraph.probeTuning" .Values.store.probes.liveness }}{{ . | trim | nindent 12 }}{{- end }} + {{- with .Values.store.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if not .Values.store.storage.enabled }} + volumes: + - name: store-data + emptyDir: {} + {{- end }} + {{- if .Values.store.storage.enabled }} + volumeClaimTemplates: + - metadata: + name: store-data + spec: + accessModes: ["ReadWriteOnce"] + {{- if eq .Values.store.storage.storageClassName "-" }} + storageClassName: "" + {{- else if .Values.store.storage.storageClassName }} + storageClassName: {{ .Values.store.storage.storageClassName | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.store.storage.size }} + {{- end }} diff --git a/helm/hugegraph/templates/tests/test-connection.yaml b/helm/hugegraph/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..020c484f6a --- /dev/null +++ b/helm/hugegraph/templates/tests/test-connection.yaml @@ -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. +# + +{{ include "hugegraph.resolveValues" . }} +{{- $smoke := `def ids = [] as Set; +(graph.schema().getVertexLabels() + graph.schema().getEdgeLabels()).each { + ids.addAll(it.indexLabels()) +}; +ids.each { + def index = graph.indexLabel(it); + if (!index.status().ok()) { + throw new IllegalStateException('Index ' + index.name() + ' is ' + + index.status().name() + ' (CREATED expected)') + } +}; +graph.traversal().V().limit(1).count()` }} +# Official Helm chart test hook — only runs with: helm test +# Not an install-time hook. Safe with --wait (no init Job in no-init design). +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "hugegraph.test.name" . }}" + labels: + {{- include "hugegraph.labels" . | nindent 4 }} + app.kubernetes.io/component: test + annotations: + "helm.sh/hook": test + # Not hook-succeeded: Helm deletes a succeeded hook Pod before + # `helm test --logs` reads it, which turns a passing release check into + # exit 1 with "pods not found". The completed Pod stays until the next + # run replaces it, so both forms exit 0 and a failed Pod keeps its logs. + "helm.sh/hook-delete-policy": before-hook-creation +spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 4 }} + {{- end }} + automountServiceAccountToken: false + restartPolicy: Never + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: curl + image: {{ .Values.server.waitImage | quote }} + {{- if .Values.server.__waitImageInherited }} + imagePullPolicy: {{ .Values.server.image.pullPolicy }} + {{- end }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + {{- with .Values.server.testResources }} + resources: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.server.auth.enabled }} + env: + - name: PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "hugegraph.server.authSecretName" . | quote }} + key: {{ include "hugegraph.server.authSecretKey" . | quote }} + {{- end }} + command: + - sh + - -c + - | + set -eu + SVC="{{ include "hugegraph.server.name" . }}" + HEADLESS="{{ include "hugegraph.server.headlessName" . }}" + PORT="{{ .Values.server.port }}" + FLOOR="{{ include "hugegraph.server.replicaFloor" . }}" + DEADLINE_S="${HG_TEST_DEADLINE_S:-150}" + BODY='{{ dict "gremlin" $smoke "aliases" (dict "graph" (.Values.server.testGraphAlias | default "DEFAULT-hugegraph")) | toJson | replace "'" "'\"'\"'" }}' + {{- if .Values.server.testGremlin }} + BUSINESS_BODY='{{ dict "gremlin" .Values.server.testGremlin "aliases" (dict "graph" (.Values.server.testGraphAlias | default "DEFAULT-hugegraph")) | toJson | replace "'" "'\"'\"'" }}' + {{- else }} + BUSINESS_BODY="" + {{- end }} + {{- if .Values.server.auth.enabled }} + AUTH_HEADER=$(printf 'admin:%s' "${PASSWORD}" | base64 | tr -d '\r\n') + request() { + printf 'header = "Authorization: Basic %s"\n' "${AUTH_HEADER}" | + curl --config - -fsS --connect-timeout 3 --max-time 20 "$1" + } + gremlin() { + printf 'header = "Authorization: Basic %s"\n' "${AUTH_HEADER}" | + curl --config - -sS --compressed --connect-timeout 3 --max-time 20 \ + -H 'Content-Type: application/json' -X POST -d "$2" \ + -w '\n%{http_code}\n' "$1" 2>&1 + } + {{- else }} + request() { + curl -fsS --connect-timeout 3 --max-time 20 "$1" + } + gremlin() { + curl -sS --compressed --connect-timeout 3 --max-time 20 \ + -H 'Content-Type: application/json' -X POST -d "$2" \ + -w '\n%{http_code}\n' "$1" 2>&1 + } + {{- end }} + check_gremlin() { + OUT=$(gremlin "$1" "$3") || true + CODE=$(printf '%s\n' "${OUT}" | grep -E '^[0-9][0-9][0-9]$' | tail -1) || CODE="" + if [ "${CODE}" = "200" ]; then + echo "helm test: $2 OK on $1" + return 0 + fi + printf 'helm test: %s failed on %s code=%s body: %.400s\n' \ + "$2" "$1" "${CODE:-no-response}" "${OUT}" + return 1 + } + echo "helm test: GET http://${SVC}:${PORT}/versions" + request "http://${SVC}:${PORT}/versions" + echo + echo "helm test: GET http://${SVC}:${PORT}/graphs" + request "http://${SVC}:${PORT}/graphs" + echo + # A Server that started while PD was rolling can pass readiness and + # serve REST while every Gremlin call on it fails, so the Service + # checks above cannot see a bad replica. Resolve the headless + # Service to the individual Ready Pods and require a graph-bound + # Gremlin answer from every one of them, and from at least FLOOR + # Pods, retrying until the deadline so a transient PD election is + # not misread as a broken Pod. + echo "helm test: Gremlin on every Server Pod via ${HEADLESS} (floor ${FLOOR})" + if [ -z "${BUSINESS_BODY}" ]; then + echo "helm test: business assertions skipped (server.testGremlin is empty)" + fi + START=$(date +%s) + while :; do + # The headless Service answers with every Ready Pod's addresses, + # one per address family on a dual-stack cluster, so counting + # addresses would count each Pod twice and let two Pods satisfy + # a floor of three. Keep one family per Pod: IPv4 when any Pod + # publishes one, otherwise IPv6. + ALL=$(getent ahosts "${HEADLESS}" 2>/dev/null | awk '{print $1}' | sort -u) || ALL="" + IPS=$(printf '%s\n' ${ALL} | grep -v ':' || true) + if [ -z "${IPS}" ]; then + IPS=$(printf '%s\n' ${ALL} | grep ':' || true) + fi + COUNT=$(printf '%s\n' ${IPS} | grep -c . || true) + # Every attempt is a complete sweep. A Pod that answered in an + # earlier attempt is queried again, so the test passes only when + # one attempt sees every Pod healthy at the same time, never by + # adding up successes from different attempts. + FAILED="" + for IP in ${IPS}; do + case "${IP}" in *:*) TARGET="[${IP}]" ;; *) TARGET="${IP}" ;; esac + URL="http://${TARGET}:${PORT}/gremlin" + if ! check_gremlin "${URL}" "Gremlin binding/index-state" "${BODY}"; then + FAILED="${FAILED}${IP}(${CODE:-no-response}) " + elif [ -n "${BUSINESS_BODY}" ] && + ! check_gremlin "${URL}" "business assertion" "${BUSINESS_BODY}"; then + FAILED="${FAILED}${IP}(${CODE:-no-response}) " + fi + done + if [ -z "${FAILED}" ] && [ "${COUNT}" -ge "${FLOOR}" ]; then + echo "helm test: OK (${COUNT} Server Pods, all configured checks passed)" + exit 0 + fi + if [ $(( $(date +%s) - START )) -ge "${DEADLINE_S}" ]; then + echo "helm test: FAILED after ${DEADLINE_S}s: pods=${COUNT} floor=${FLOOR} failing=${FAILED:-none}" >&2 + exit 1 + fi + sleep 5 + done diff --git a/helm/hugegraph/testdata/values-pre-hardening.yaml b/helm/hugegraph/testdata/values-pre-hardening.yaml new file mode 100644 index 0000000000..990f5fe7e0 --- /dev/null +++ b/helm/hugegraph/testdata/values-pre-hardening.yaml @@ -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. +# + +# Values shape shipped before the production-hardening fields were added. +# validate-chart.sh installs this as the chart default values to emulate a +# Helm --reuse-values upgrade whose stored release values lack all new keys. + +fullnameOverride: "" +nameOverride: "" + +imagePullSecrets: [] + +pd: + replicas: 3 + image: + repository: hugegraph/pd + tag: latest + pullPolicy: IfNotPresent + ports: + grpc: 8686 + rest: 8620 + raft: 8610 + dataPath: /hugegraph-pd/pd_data + storage: + size: 10Gi + storageClassName: "" + resources: {} + antiAffinity: required + pdb: + enabled: true + minAvailable: 2 + probes: + startup: + failureThreshold: 30 + periodSeconds: 10 + readiness: + periodSeconds: 10 + failureThreshold: 3 + liveness: + periodSeconds: 20 + failureThreshold: 3 + +store: + replicas: 3 + image: + repository: hugegraph/store + tag: latest + pullPolicy: IfNotPresent + ports: + grpc: 8500 + raft: 8510 + rest: 8520 + dataPath: /hugegraph-store/storage + storage: + size: 50Gi + storageClassName: "" + resources: {} + antiAffinity: required + pdb: + enabled: true + minAvailable: 2 + waitImage: curlimages/curl:8.5.0 + probes: + startup: + failureThreshold: 40 + periodSeconds: 10 + readiness: + periodSeconds: 10 + failureThreshold: 3 + liveness: + periodSeconds: 20 + failureThreshold: 3 + +server: + replicas: 3 + image: + repository: hugegraph/server + tag: latest + pullPolicy: IfNotPresent + port: 8080 + backend: hstore + resources: {} + waitImage: curlimages/curl:8.5.0 + initStoreEnabled: false + auth: + enabled: false + admin: + password: "" + existingSecret: "" + key: password + autoGenerate: true + token: + value: "" + existingSecret: "" + key: token_secret + autoGenerate: true + ingress: + enabled: false + className: "" + hosts: + - host: hugegraph.local + paths: + - path: / + pathType: Prefix + tls: [] + hpa: + enabled: false + minReplicas: 3 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + probes: + startup: + failureThreshold: 30 + periodSeconds: 5 + readiness: + periodSeconds: 10 + failureThreshold: 3 + liveness: + periodSeconds: 20 + failureThreshold: 3 + +networkPolicy: + enabled: false diff --git a/helm/hugegraph/tests/__snapshot__/networkpolicy_test.yaml.snap b/helm/hugegraph/tests/__snapshot__/networkpolicy_test.yaml.snap new file mode 100644 index 0000000000..a79353de84 --- /dev/null +++ b/helm/hugegraph/tests/__snapshot__/networkpolicy_test.yaml.snap @@ -0,0 +1,286 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +matches the reviewed cluster plus Hubble render: + 1: | + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + labels: + app.kubernetes.io/component: pd + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: hugegraph + app.kubernetes.io/version: latest + helm.sh/chart: hugegraph-helm-0.2.0 + name: RELEASE-NAME-hugegraph-pd + spec: + egress: + - ports: + - port: 8610 + protocol: TCP + - port: 8686 + protocol: TCP + to: + - podSelector: + matchLabels: + app.kubernetes.io/component: pd + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/component: pd + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + ports: + - port: 8610 + protocol: TCP + - port: 8686 + protocol: TCP + - from: + - podSelector: + matchLabels: + app.kubernetes.io/component: store + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + - podSelector: + matchLabels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + - podSelector: + matchLabels: + app.kubernetes.io/component: hubble + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + ports: + - port: 8686 + protocol: TCP + - port: 8620 + protocol: TCP + podSelector: + matchLabels: + app.kubernetes.io/component: pd + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + policyTypes: + - Ingress + - Egress + 2: | + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + labels: + app.kubernetes.io/component: store + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: hugegraph + app.kubernetes.io/version: latest + helm.sh/chart: hugegraph-helm-0.2.0 + name: RELEASE-NAME-hugegraph-store + spec: + egress: + - ports: + - port: 8686 + protocol: TCP + - port: 8620 + protocol: TCP + to: + - podSelector: + matchLabels: + app.kubernetes.io/component: pd + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + - ports: + - port: 8510 + protocol: TCP + to: + - podSelector: + matchLabels: + app.kubernetes.io/component: store + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/component: store + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + ports: + - port: 8510 + protocol: TCP + - from: + - podSelector: + matchLabels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + ports: + - port: 8500 + protocol: TCP + - port: 8520 + protocol: TCP + - from: + - podSelector: + matchLabels: + app.kubernetes.io/component: hubble + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + ports: + - port: 8520 + protocol: TCP + podSelector: + matchLabels: + app.kubernetes.io/component: store + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + policyTypes: + - Ingress + - Egress + 3: | + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: hugegraph + app.kubernetes.io/version: latest + helm.sh/chart: hugegraph-helm-0.2.0 + name: RELEASE-NAME-hugegraph-server + spec: + egress: + - ports: + - port: 8686 + protocol: TCP + - port: 8620 + protocol: TCP + to: + - podSelector: + matchLabels: + app.kubernetes.io/component: pd + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + - ports: + - port: 8500 + protocol: TCP + - port: 8520 + protocol: TCP + to: + - podSelector: + matchLabels: + app.kubernetes.io/component: store + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/component: test + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + - podSelector: + matchLabels: + app.kubernetes.io/component: hubble + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + ports: + - port: 8080 + protocol: TCP + podSelector: + matchLabels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + policyTypes: + - Ingress + - Egress + 4: | + apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + labels: + app.kubernetes.io/component: hubble + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: hugegraph + app.kubernetes.io/version: latest + helm.sh/chart: hugegraph-helm-0.2.0 + name: RELEASE-NAME-hugegraph-hubble + spec: + egress: + - ports: + - port: 8080 + protocol: TCP + to: + - podSelector: + matchLabels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + - ports: + - port: 8686 + protocol: TCP + - port: 8620 + protocol: TCP + to: + - podSelector: + matchLabels: + app.kubernetes.io/component: pd + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + - ports: + - port: 8520 + protocol: TCP + to: + - podSelector: + matchLabels: + app.kubernetes.io/component: store + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + podSelector: + matchLabels: + app.kubernetes.io/component: hubble + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/name: hugegraph + policyTypes: + - Ingress + - Egress diff --git a/helm/hugegraph/tests/auth_checksum_lookup_test.yaml b/helm/hugegraph/tests/auth_checksum_lookup_test.yaml new file mode 100644 index 0000000000..5a8a98e7b3 --- /dev/null +++ b/helm/hugegraph/tests/auth_checksum_lookup_test.yaml @@ -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. +# + +# The membership and identity guards read the live StatefulSets through +# lookup. These cases fake that object, so the refused path (a raft port +# change, a rename, a replica shrink) and the allowed path (an unchanged +# upgrade, a fresh install) are both pinned at render time. validateValues +# is included from the Server Deployment, so the refusals render that. +# The lookup half of the rollout checksums, rendered against fake live +# Secrets. A chart-generated Secret contributes the digest of its data, so +# a render that finds the Secret (every upgrade) equals the install render +# (which hashes the value it writes): no roll on a no-change upgrade. An +# operator-supplied existingSecret contributes its resourceVersion. Each +# expected value is sha256 of the parts joined with "|", computed outside +# Helm, as in auth_checksum_source_test.yaml. +suite: Rollout checksum lookup half against live Secrets +templates: + - pd-statefulset.yaml + - server-deployment.yaml +kubernetesProvider: + scheme: + "v1/Secret": + gvr: + version: v1 + resource: secrets + namespaced: true + # The identity guards list StatefulSets; the fake client registers the + # list kind only for kinds that have an object, so one StatefulSet of + # another release is faked (which the guards must ignore). + "apps/v1/StatefulSet": + gvr: + group: apps + version: v1 + resource: statefulsets + namespaced: true + objects: + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: other-hugegraph-pd + namespace: NAMESPACE + labels: + app.kubernetes.io/instance: other + app.kubernetes.io/component: pd + spec: + replicas: "1" + - apiVersion: v1 + kind: Secret + metadata: + name: RELEASE-NAME-pd-auth + namespace: NAMESPACE + resourceVersion: "41" + data: + secret-key: cGQtc2VjcmV0LXN0ZWFkeQ== + - apiVersion: v1 + kind: Secret + metadata: + name: byo-pd-secret + namespace: NAMESPACE + resourceVersion: "100" + data: + secret-key: b3BlcmF0b3ItY2hvc2Vu + - apiVersion: v1 + kind: Secret + metadata: + name: RELEASE-NAME-admin + namespace: NAMESPACE + resourceVersion: "42" + data: + password: YWRtaW4tc3RlYWR5 + - apiVersion: v1 + kind: Secret + metadata: + name: RELEASE-NAME-auth-token + namespace: NAMESPACE + resourceVersion: "43" + data: + token_secret: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY= + - apiVersion: v1 + kind: Secret + metadata: + name: byo-admin + namespace: NAMESPACE + resourceVersion: "7" + data: + password: b3BlcmF0b3ItYWRtaW4= +tests: + - it: hashes the live generated PD Secret by its value, not its resourceVersion + template: pd-statefulset.yaml + asserts: + - equal: + path: spec.template.metadata.annotations["checksum/pd-auth"] + value: c28abf86b4f58b426476d7d7d95dc9bc0aee3f87d64616b5314a823eb3285328 + + - it: renders the same PD checksum when the inline value equals the live value + template: pd-statefulset.yaml + set: + pd.auth.value: pd-secret-steady + asserts: + - equal: + path: spec.template.metadata.annotations["checksum/pd-auth"] + value: c28abf86b4f58b426476d7d7d95dc9bc0aee3f87d64616b5314a823eb3285328 + + - it: hashes an operator-supplied PD Secret by its resourceVersion only + template: pd-statefulset.yaml + set: + pd.auth.existingSecret: byo-pd-secret + asserts: + - equal: + path: spec.template.metadata.annotations["checksum/pd-auth"] + value: c59f1600408fcc703d7ba56aafaa1a3154380c4c9f97f422a603f79139661ec3 + + - it: keeps the generated PD value in the Secret and out of the annotation + template: pd-statefulset.yaml + asserts: + - notMatchRegex: + path: spec.template.metadata.annotations["checksum/pd-auth"] + pattern: "pd-secret-steady|cGQtc2VjcmV0LXN0ZWFkeQ==" + + - it: hashes both live generated Server Secrets by value + template: server-deployment.yaml + asserts: + - equal: + path: spec.template.metadata.annotations["checksum/auth"] + value: d5e9d6f7d7bdafc092bd65571b124c37b6aefdf5af871a1b5c4f999131e6e329 + + - it: mixes an operator-supplied admin Secret resourceVersion with the generated token digest + template: server-deployment.yaml + set: + server.auth.admin.existingSecret: byo-admin + asserts: + - equal: + path: spec.template.metadata.annotations["checksum/auth"] + value: 36d51dce49f4d417c0bad64d99f128a4f7498633dba0486ad6eb432c61196f0d diff --git a/helm/hugegraph/tests/auth_checksum_source_test.yaml b/helm/hugegraph/tests/auth_checksum_source_test.yaml new file mode 100644 index 0000000000..7403504328 --- /dev/null +++ b/helm/hugegraph/tests/auth_checksum_source_test.yaml @@ -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. +# + +# Each rollout checksum picks its revision input by credential source. These +# cases pin the exact digest, so they fail if the parts list changes at all, +# not merely if it stops being 64 hex characters. Each expected value is +# sha256 of the parts joined with "|", computed outside Helm: +# +# inline PD value sha256("t-pd-auth|secret-key|" + sha256(value)) +# PD existingSecret sha256("byo-pd-secret|secret-key") +# +# The existingSecret cases are the ones that prove the selection: the inline +# value is set there too, and it must not reach the annotation, because the +# Pods do not read it. +# +# These cases cover the inline half; auth_checksum_lookup_test.yaml renders +# the lookup half against fake live Secrets (generated: value digest; +# existingSecret: resourceVersion). The live rollout counts belong to the +# lifecycle run. +suite: Rollout checksum revision source +release: + name: t +tests: + - it: hashes the inline PD value with the Secret name and key, and nothing else + template: pd-statefulset.yaml + set: + pd.auth.value: pd-secret-steady + asserts: + - equal: + path: spec.template.metadata.annotations["checksum/pd-auth"] + value: c3e18a4d518ea495ef75e4fec03a68675b23ef0be93714e7a777fbb50a5957c1 + + - it: drops the inline PD digest once an existingSecret supplies the credential + template: pd-statefulset.yaml + set: + pd.auth.existingSecret: byo-pd-secret + pd.auth.value: ignored-by-the-pods + asserts: + - equal: + path: spec.template.metadata.annotations["checksum/pd-auth"] + value: 86fdc89040752c37324032343cd1d9a53ca23a46f724689dd61c2f5f829c0d33 + + - it: hashes both inline Server credentials with their Secret names and keys + template: server-deployment.yaml + set: + server.auth.admin.password: admin-after + server.auth.token.value: 0123456789abcdef0123456789abcdef + asserts: + - equal: + path: spec.template.metadata.annotations["checksum/auth"] + value: d6ce6518e8b740a46bbacf0b55e4094580048c87912c85f21448b701f972fd72 + + - it: selects the admin and token sources independently + template: server-deployment.yaml + set: + server.auth.admin.existingSecret: byo-admin + server.auth.admin.password: ignored-by-the-pods + server.auth.token.value: 0123456789abcdef0123456789abcdef + asserts: + - equal: + path: spec.template.metadata.annotations["checksum/auth"] + value: 7f13191bccb01ef3b2c6ffcce2fc6b6a73a6c6416583dcc4c593a5cfde1a24a0 + + - it: keeps credential plaintext out of the annotation + template: server-deployment.yaml + set: + server.auth.admin.password: admin-plaintext-value + server.auth.token.value: 0123456789abcdef0123456789abcdef + asserts: + - notMatchRegex: + path: spec.template.metadata.annotations["checksum/auth"] + pattern: admin-plaintext-value + - notMatchRegex: + path: spec.template.metadata.annotations["checksum/auth"] + pattern: 0123456789abcdef0123456789abcdef diff --git a/helm/hugegraph/tests/auth_token_length_test.yaml b/helm/hugegraph/tests/auth_token_length_test.yaml new file mode 100644 index 0000000000..0ca92a38c5 --- /dev/null +++ b/helm/hugegraph/tests/auth_token_length_test.yaml @@ -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. +# + +suite: Auth token length guard +templates: + - server-deployment.yaml +tests: + - it: accepts an empty token value, which defers to autoGenerate + asserts: + - hasDocuments: + count: 1 + + - it: accepts a signing key of at least 32 characters + set: + server.auth.token.value: ktestJwtSigningKey2026xyzABCDEFGH12345678 + asserts: + - hasDocuments: + count: 1 + + - it: rejects a signing key shorter than 32 characters + set: + server.auth.token.value: tooshortkey + asserts: + - failedTemplate: {} diff --git a/helm/hugegraph/tests/cluster_preset_resources_test.yaml b/helm/hugegraph/tests/cluster_preset_resources_test.yaml new file mode 100644 index 0000000000..04a59b0c57 --- /dev/null +++ b/helm/hugegraph/tests/cluster_preset_resources_test.yaml @@ -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. +# + +suite: Cluster preset Store memory +values: + - ../values-cluster.yaml +tests: + - it: gives the Store room for the caches the image commits outside its heap + template: store-statefulset.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].resources.limits.memory + value: 8Gi + - equal: + path: spec.template.spec.containers[0].resources.requests.memory + value: 5Gi + + - it: keeps the Store heap flags the memory budget is derived from + template: store-statefulset.yaml + asserts: + - matchRegex: + path: spec.template.spec.containers[0].env[?(@.name=="JAVA_OPTS")].value + pattern: -Xmx1024m + - matchRegex: + path: spec.template.spec.containers[0].env[?(@.name=="JAVA_OPTS")].value + pattern: -XX:MaxDirectMemorySize=512m + + - it: holds Store rollouts on OnDelete because readiness is listener-only + template: store-statefulset.yaml + asserts: + - equal: + path: spec.updateStrategy.type + value: OnDelete + + - it: keeps the PD raft rolling restart automatic + template: pd-statefulset.yaml + asserts: + - equal: + path: spec.updateStrategy.type + value: RollingUpdate diff --git a/helm/hugegraph/tests/credential_contract_test.yaml b/helm/hugegraph/tests/credential_contract_test.yaml new file mode 100644 index 0000000000..5fbddb830b --- /dev/null +++ b/helm/hugegraph/tests/credential_contract_test.yaml @@ -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. +# + +# The credential contract is printable ASCII, no backslash, no leading or +# trailing space; the admin password also takes no colon and no space at +# all. The schema enforces it on inline values (schema aborts +# cannot be matched by this framework, so those cases live in the CI +# reject-invalid-values step); the container wrappers enforce it on an +# existingSecret, which is only seen at Pod start. These cases pin the +# wrapper checks and the hook contract, so a rewrite cannot silently drop +# them. +suite: Credential contract and test hook contract +tests: + - it: checks the admin password byte-wise under the C locale in the Server wrapper + template: server-deployment.yaml + asserts: + - matchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "LC_ALL=C tr -d" + - matchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "decodes Basic-auth credentials as ASCII" + - matchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "fix the server.auth.admin" + - matchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "must not contain a colon" + - matchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "\\*' '\\*\\)\\n[^\\n]*# Temporary" + - matchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "must not contain spaces" + - notMatchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "\\[\\[:print:\\]\\]" + + - it: skips the admin password check when authentication is disabled + template: server-deployment.yaml + set: + hubble.enabled: false + server.auth.enabled: false + asserts: + - notMatchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "LC_ALL=C tr -d" + - notMatchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "must not contain spaces" + + - it: checks the PD REST secret byte-wise under the C locale in the Hubble wrapper + template: hubble-deployment.yaml + set: + hubble.enabled: true + asserts: + - matchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "LC_ALL=C tr -d" + - matchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "fix the pd.auth" + - notMatchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "\\[\\[:print:\\]\\]" + + - it: carries no PD secret check in direct mode, which reads no PD secret + template: hubble-deployment.yaml + set: + hubble.enabled: true + hubble.mode: direct + asserts: + - notMatchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "LC_ALL=C tr -d" + + - it: keeps the succeeded test Pod so helm test --logs can read it + template: tests/test-connection.yaml + asserts: + - equal: + path: metadata.annotations["helm.sh/hook-delete-policy"] + value: before-hook-creation + + - it: sweeps every Pod on every attempt and counts one address family per Pod + template: tests/test-connection.yaml + asserts: + - notMatchRegex: + path: spec.containers[0].command[2] + pattern: "PASSED" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "grep -v ':'" + + - it: ignores unused credential encoding when Server authentication is disabled + set: + hubble.enabled: false + server.auth.enabled: false + server.auth.admin.password: "unused password: value" + server.auth.token.value: unused + template: server-deployment.yaml + asserts: + - isKind: + of: Deployment + - it: existing Secrets take priority over unused inline credentials + set: + server.auth.admin.existingSecret: provided-admin + server.auth.admin.password: "unused password: value" + server.auth.token.existingSecret: provided-token + server.auth.token.value: unused + template: server-deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: PASSWORD + valueFrom: + secretKeyRef: + name: provided-admin + key: password diff --git a/helm/hugegraph/tests/easy_modes_test.yaml b/helm/hugegraph/tests/easy_modes_test.yaml new file mode 100644 index 0000000000..172495d6d5 --- /dev/null +++ b/helm/hugegraph/tests/easy_modes_test.yaml @@ -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. +# + + +suite: easy and strict defaults and explicit overrides +templates: + - server-service.yaml + - hubble-service.yaml + - pd-service-client.yaml + - networkpolicy.yaml + - store-statefulset.yaml + - server-deployment.yaml + - tests/test-connection.yaml +tests: + - it: defaults to NodePort API and UI with internal PD + asserts: + - equal: + path: spec.type + value: NodePort + template: server-service.yaml + - equal: + path: spec.type + value: NodePort + template: hubble-service.yaml + - equal: + path: spec.type + value: ClusterIP + template: pd-service-client.yaml + - hasDocuments: + count: 0 + template: networkpolicy.yaml + - it: strict defaults are internal and isolated + set: + mode: strict + asserts: + - equal: + path: spec.type + value: ClusterIP + template: server-service.yaml + - equal: + path: spec.type + value: ClusterIP + template: hubble-service.yaml + - hasDocuments: + count: 4 + template: networkpolicy.yaml + - it: explicit false and NodePort override strict mode without acknowledgement + set: + mode: strict + networkPolicy.enabled: false + server.service.type: NodePort + hubble.enabled: false + asserts: + - equal: + path: spec.type + value: NodePort + template: server-service.yaml + - hasDocuments: + count: 0 + template: networkpolicy.yaml + - hasDocuments: + count: 0 + template: hubble-service.yaml + - it: supports cluster-specific fixed ports and zero automatic allocation + set: + server.service.nodePort: 8802 + hubble.service.nodePort: 8808 + pd.service.type: NodePort + pd.service.restNodePort: 0 + asserts: + - equal: + path: spec.ports[0].nodePort + value: 8802 + template: server-service.yaml + - equal: + path: spec.ports[0].nodePort + value: 8808 + template: hubble-service.yaml + - it: reuses existing business images for tool containers by default + asserts: + - equal: + path: spec.template.spec.initContainers[0].image + value: hugegraph/store:latest + template: store-statefulset.yaml + - equal: + path: spec.containers[0].image + value: hugegraph/server:latest + template: tests/test-connection.yaml + - equal: + path: spec.template.spec.initContainers[0].command[0] + value: sh + template: store-statefulset.yaml + - equal: + path: spec.containers[0].command[0] + value: sh + template: tests/test-connection.yaml + - it: mirrors component and inherited tool images without changing digest pins + set: + global.imageRegistry: registry.internal/project + store.image.digest: sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.internal/project/hugegraph/store@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + template: store-statefulset.yaml + - equal: + path: spec.template.spec.initContainers[0].image + value: registry.internal/project/hugegraph/store@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + template: store-statefulset.yaml + - equal: + path: spec.containers[0].image + value: registry.internal/project/hugegraph/server:latest + template: tests/test-connection.yaml + - it: qualified individual images override the mirror + set: + global.imageRegistry: registry.internal + store.image.repository: other.internal/store + server.waitImage: tools.internal/http-check:1 + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: other.internal/store:latest + template: store-statefulset.yaml + - equal: + path: spec.template.spec.initContainers[0].image + value: other.internal/store:latest + template: store-statefulset.yaml + - equal: + path: spec.containers[0].image + value: tools.internal/http-check:1 + template: tests/test-connection.yaml + - it: adapts unspecified startup probes to the configured storage wait + set: + server.storageWaitTimeoutSeconds: 600 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 151 + template: server-deployment.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: WAIT_STORAGE_TIMEOUT_S + value: "600" + template: server-deployment.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: HG_SERVER_STARTUP_TIMEOUT_S + value: "150" + template: server-deployment.yaml + + - it: reuses the component pull policy for offline tool containers + set: + store.image.pullPolicy: Never + server.image.pullPolicy: IfNotPresent + asserts: + - equal: + path: spec.template.spec.initContainers[0].imagePullPolicy + value: Never + template: store-statefulset.yaml + - equal: + path: spec.containers[0].imagePullPolicy + value: IfNotPresent + template: tests/test-connection.yaml diff --git a/helm/hugegraph/tests/hubble_exposure_test.yaml b/helm/hugegraph/tests/hubble_exposure_test.yaml new file mode 100644 index 0000000000..d42d5a54c8 --- /dev/null +++ b/helm/hugegraph/tests/hubble_exposure_test.yaml @@ -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. +# + +suite: Hubble Service exposure and mode defaults +templates: + - hubble-service.yaml +tests: + - it: defaults to an automatic NodePort in easy mode + asserts: + - equal: + path: spec.type + value: NodePort + - notExists: + path: spec.ports[0].nodePort + + - it: defaults to ClusterIP in strict mode + set: + mode: strict + asserts: + - equal: + path: spec.type + value: ClusterIP + + - it: honors an explicit NodePort in strict mode without acknowledgement + set: + mode: strict + hubble.service.type: NodePort + hubble.service.nodePort: 8808 + asserts: + - equal: + path: spec.type + value: NodePort + - equal: + path: spec.ports[0].nodePort + value: 8808 + + - it: renders LoadBalancer without acknowledgement + set: + hubble.service.type: LoadBalancer + asserts: + - equal: + path: spec.type + value: LoadBalancer + + - it: honors an explicit ClusterIP in easy mode + set: + hubble.service.type: ClusterIP + asserts: + - equal: + path: spec.type + value: ClusterIP + + - it: treats zero NodePort as automatic allocation + set: + hubble.service.nodePort: 0 + asserts: + - notExists: + path: spec.ports[0].nodePort diff --git a/helm/hugegraph/tests/identity_guard_fresh_install_test.yaml b/helm/hugegraph/tests/identity_guard_fresh_install_test.yaml new file mode 100644 index 0000000000..e5ceed7c64 --- /dev/null +++ b/helm/hugegraph/tests/identity_guard_fresh_install_test.yaml @@ -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. +# + +# The membership and identity guards read the live StatefulSets through +# lookup. These cases fake that object, so the refused path (a raft port +# change, a rename, a replica shrink) and the allowed path (an unchanged +# upgrade, a fresh install) are both pinned at render time. validateValues +# is included from the Server Deployment, so the refusals render that. +# The identity guards read the live StatefulSets; with none present (a +# fresh install, or any template-only render) every port and name is free. +suite: Raft identity guards on a fresh install +templates: + - pd-statefulset.yaml +tests: + - it: lets a fresh install choose any raft port and name + set: + pd.ports.raft: 8611 + fullnameOverride: fresh + asserts: + - equal: + path: metadata.name + value: fresh-pd + - equal: + path: spec.template.spec.containers[0].ports[2].containerPort + value: 8611 diff --git a/helm/hugegraph/tests/identity_guard_selector_test.yaml b/helm/hugegraph/tests/identity_guard_selector_test.yaml new file mode 100644 index 0000000000..8c11add741 --- /dev/null +++ b/helm/hugegraph/tests/identity_guard_selector_test.yaml @@ -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. +# + +# The membership and identity guards read the live StatefulSets through +# lookup. These cases fake that object, so the refused path (a raft port +# change, a rename, a replica shrink) and the allowed path (an unchanged +# upgrade, a fresh install) are both pinned at render time. validateValues +# is included from the Server Deployment, so the refusals render that. +# A nameOverride contained in the release name keeps the StatefulSet name +# (hugegraph.fullname collapses to the release name) but still changes the +# app.kubernetes.io/name selector label, which Kubernetes forbids on an +# existing StatefulSet. The fake StatefulSet carries the name that override +# renders, so only the selector differs. +suite: nameOverride that keeps the StatefulSet name but changes its selector +templates: + - server-deployment.yaml +kubernetesProvider: + scheme: + "apps/v1/StatefulSet": + gvr: + group: apps + version: v1 + resource: statefulsets + namespaced: true + objects: + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-pd + namespace: NAMESPACE + labels: + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: pd + spec: + volumeClaimTemplates: + - metadata: + name: pd-data + replicas: "3" + selector: + matchLabels: + app.kubernetes.io/name: RELEASE + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: pd +tests: + - it: refuses the selector label change with the reason + release: + upgrade: true + set: + nameOverride: NAME + asserts: + - failedTemplate: + errorPattern: "changes the app.kubernetes.io/name selector label of the pd StatefulSet RELEASE-NAME-pd from RELEASE to NAME" + + - it: lets the same override through when the selector already matches + release: + upgrade: true + set: + nameOverride: RELEASE + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-server diff --git a/helm/hugegraph/tests/identity_guard_test.yaml b/helm/hugegraph/tests/identity_guard_test.yaml new file mode 100644 index 0000000000..88e1c41a19 --- /dev/null +++ b/helm/hugegraph/tests/identity_guard_test.yaml @@ -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. +# + +# The membership and identity guards read the live StatefulSets through +# lookup. These cases fake that object, so the refused path (a raft port +# change, a rename, a replica shrink) and the allowed path (an unchanged +# upgrade, a fresh install) are both pinned at render time. validateValues +# is included from the Server Deployment, so the refusals render that. +suite: Raft identity and membership guards against the live StatefulSet +templates: + - server-deployment.yaml + - pd-statefulset.yaml + - store-statefulset.yaml +# The fake objects spell numbers as strings: the fake client cannot deep +# copy Go ints, and the guards convert through `int` either way. +kubernetesProvider: + scheme: + "apps/v1/StatefulSet": + gvr: + group: apps + version: v1 + resource: statefulsets + namespaced: true + objects: + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-pd + namespace: NAMESPACE + labels: + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: pd + spec: + volumeClaimTemplates: + - metadata: + name: pd-data + replicas: "3" + selector: + matchLabels: + app.kubernetes.io/name: hugegraph + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: pd + template: + spec: + containers: + - name: pd + ports: + - name: grpc + containerPort: "8686" + - name: rest + containerPort: "8620" + - name: raft + containerPort: "8610" + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-store + namespace: NAMESPACE + labels: + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: store + spec: + volumeClaimTemplates: + - metadata: + name: store-data + replicas: "3" + selector: + matchLabels: + app.kubernetes.io/name: hugegraph + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: store + template: + spec: + containers: + - name: store + ports: + - name: grpc + containerPort: "8500" + - name: raft + containerPort: "8510" + - name: rest + containerPort: "8520" +tests: + - it: lets an unchanged upgrade through + release: + upgrade: true + template: server-deployment.yaml + asserts: + - equal: + path: spec.replicas + value: 3 + + - it: refuses a PD raft port change on an initialized release + release: + upgrade: true + template: server-deployment.yaml + set: + pd.ports.raft: 8611 + asserts: + - failedTemplate: + errorPattern: "pd.ports.raft cannot change from 8610 to 8611 on an initialized release" + + - it: refuses a Store raft port change on an initialized release + release: + upgrade: true + template: server-deployment.yaml + set: + store.ports.raft: 8511 + asserts: + - failedTemplate: + errorPattern: "store.ports.raft cannot change from 8510 to 8511 on an initialized release" + + - it: leaves the other ports routinely mutable + release: + upgrade: true + template: pd-statefulset.yaml + set: + pd.ports.rest: 8621 + store.ports.grpc: 8501 + asserts: + - equal: + path: spec.template.spec.containers[0].ports[1].containerPort + value: 8621 + + - it: refuses a rename through fullnameOverride on an initialized release + release: + upgrade: true + template: server-deployment.yaml + set: + fullnameOverride: renamed + asserts: + - failedTemplate: + errorPattern: "already owns the pd StatefulSet RELEASE-NAME-hugegraph-pd, but the values now name it renamed-pd" + + - it: refuses a rename through nameOverride on an initialized release + release: + upgrade: true + template: server-deployment.yaml + set: + nameOverride: graph + asserts: + - failedTemplate: + errorPattern: "already owns the (pd|store) StatefulSet RELEASE-NAME-hugegraph-(pd|store), but the values now name it RELEASE-NAME-graph-(pd|store)" + + - it: refuses a Store replica shrink against the live StatefulSet + release: + upgrade: true + template: server-deployment.yaml + set: + store.replicas: 2 + store.pdb.minAvailable: 1 + asserts: + - failedTemplate: + errorPattern: "store.replicas cannot shrink from 3 to 2 through a helm upgrade" + + - it: refuses PD growth against the live StatefulSet + release: + upgrade: true + template: server-deployment.yaml + set: + pd.replicas: 5 + pd.pdb.minAvailable: 3 + asserts: + - failedTemplate: + errorPattern: "pd.replicas cannot grow from 3 to 5 through a helm upgrade" + + - it: ignores existing StatefulSet identity and membership on a fresh install + template: server-deployment.yaml + release: + upgrade: false + set: + fullnameOverride: fresh + pd.replicas: 1 + pd.ports.raft: 8611 + store.ports.raft: 8511 + asserts: + - equal: + path: metadata.name + value: fresh-server diff --git a/helm/hugegraph/tests/image_digest_test.yaml b/helm/hugegraph/tests/image_digest_test.yaml new file mode 100644 index 0000000000..6f69ef7ddc --- /dev/null +++ b/helm/hugegraph/tests/image_digest_test.yaml @@ -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. +# + +suite: Image reference and digest pinning +tests: + - it: keeps the default pd tag on latest until release images are published + template: pd-statefulset.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/pd:latest + + - it: keeps the default store tag on latest until release images are published + template: store-statefulset.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/store:latest + + - it: keeps the default server tag on latest until release images are published + template: server-deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/server:latest + + - it: inherits appVersion for pd when the tag is empty + template: pd-statefulset.yaml + set: + pd.image.tag: "" + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/pd:latest + + - it: inherits appVersion for store when the tag is empty + template: store-statefulset.yaml + set: + store.image.tag: "" + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/store:latest + + - it: overrides appVersion with an explicit store tag + template: store-statefulset.yaml + set: + store.image.tag: pinned-for-test + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/store:pinned-for-test + + - it: inherits appVersion for server when the tag is empty + template: server-deployment.yaml + set: + server.image.tag: "" + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/server:latest + + - it: overrides appVersion with an explicit server tag + template: server-deployment.yaml + set: + server.image.tag: pinned-for-test + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/server:pinned-for-test + + - it: keeps Hubble on its independent tag + template: hubble-deployment.yaml + set: + hubble.enabled: true + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/hubble:latest + + - it: renders repository and tag when no digest is set + template: pd-statefulset.yaml + set: + pd.image.tag: pinned-for-test + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/pd:pinned-for-test + + - it: pins by digest when one is supplied, ignoring the tag + template: pd-statefulset.yaml + set: + pd.image.tag: pinned-for-test + pd.image.digest: sha256:43999a5ccda34883a9e4e458e25cb903ce34adbc66782d9f4a5260d68b2b5a82 + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: hugegraph/pd@sha256:43999a5ccda34883a9e4e458e25cb903ce34adbc66782d9f4a5260d68b2b5a82 + - notMatchRegex: + path: spec.template.spec.containers[0].image + pattern: "helm-dev" + + - it: supports digest pinning on store as well + template: store-statefulset.yaml + set: + store.image.tag: pinned-for-test + store.image.digest: sha256:458d77a18542e8a2f7980b075f2fda32026d582200a8983921354398467ff860 + asserts: + - matchRegex: + path: spec.template.spec.containers[0].image + pattern: "^hugegraph/store@sha256:458d77a1" + + - it: supports digest pinning on server as well + template: server-deployment.yaml + set: + server.image.tag: pinned-for-test + server.image.digest: sha256:30f99c6b9ab605accf96e9c179434b13495547f532dcb89d840231c54f1cc101 + asserts: + - matchRegex: + path: spec.template.spec.containers[0].image + pattern: "^hugegraph/server@sha256:30f99c6b" + + - it: applies the global private registry to pd + template: pd-statefulset.yaml + set: + global.imageRegistry: registry.internal:5000/mirror/ + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.internal:5000/mirror/hugegraph/pd:latest + + - it: lets a fully qualified pd repository override the global registry + template: pd-statefulset.yaml + set: + global.imageRegistry: registry.internal/mirror + pd.image.repository: another.internal/team/pd + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: another.internal/team/pd:latest + + - it: applies the global private registry to store + template: store-statefulset.yaml + set: + global.imageRegistry: registry.internal:5000/mirror/ + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.internal:5000/mirror/hugegraph/store:latest + + - it: lets a fully qualified store repository override the global registry + template: store-statefulset.yaml + set: + global.imageRegistry: registry.internal/mirror + store.image.repository: another.internal/team/store + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: another.internal/team/store:latest + + - it: applies the global private registry to server + template: server-deployment.yaml + set: + global.imageRegistry: registry.internal:5000/mirror/ + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.internal:5000/mirror/hugegraph/server:latest + + - it: lets a fully qualified server repository override the global registry + template: server-deployment.yaml + set: + global.imageRegistry: registry.internal/mirror + server.image.repository: another.internal/team/server + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: another.internal/team/server:latest + + - it: applies the global private registry to hubble + template: hubble-deployment.yaml + set: + global.imageRegistry: registry.internal:5000/mirror/ + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: registry.internal:5000/mirror/hugegraph/hubble:latest + + - it: lets a fully qualified hubble repository override the global registry + template: hubble-deployment.yaml + set: + global.imageRegistry: registry.internal/mirror + hubble.image.repository: another.internal/team/hubble + asserts: + - equal: + path: spec.template.spec.containers[0].image + value: another.internal/team/hubble:latest + + - it: reuses the Store image for PD readiness waiting by default + template: store-statefulset.yaml + asserts: + - equal: + path: spec.template.spec.initContainers[0].image + value: hugegraph/store:latest + + - it: reuses the private Store image including its digest for readiness waiting + template: store-statefulset.yaml + set: + global.imageRegistry: registry.internal/mirror + store.image.digest: sha256:458d77a18542e8a2f7980b075f2fda32026d582200a8983921354398467ff860 + asserts: + - equal: + path: spec.template.spec.initContainers[0].image + value: registry.internal/mirror/hugegraph/store@sha256:458d77a18542e8a2f7980b075f2fda32026d582200a8983921354398467ff860 + + - it: reuses the private Server image for the test hook without a curl image + template: tests/test-connection.yaml + set: + global.imageRegistry: registry.internal/mirror + asserts: + - equal: + path: spec.containers[0].image + value: registry.internal/mirror/hugegraph/server:latest + + - it: honors an explicit qualified wait image over the component and global registry + template: store-statefulset.yaml + set: + global.imageRegistry: registry.internal/mirror + store.waitImage: tools.internal/readiness:v2 + asserts: + - equal: + path: spec.template.spec.initContainers[0].image + value: tools.internal/readiness:v2 + + - it: applies the global registry to an explicitly chosen unqualified test image + template: tests/test-connection.yaml + set: + global.imageRegistry: registry.internal/mirror + server.waitImage: tools/check:v2 + asserts: + - equal: + path: spec.containers[0].image + value: registry.internal/mirror/tools/check:v2 diff --git a/helm/hugegraph/tests/native_network_options_test.yaml b/helm/hugegraph/tests/native_network_options_test.yaml new file mode 100644 index 0000000000..157c7d16b3 --- /dev/null +++ b/helm/hugegraph/tests/native_network_options_test.yaml @@ -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. +# + +suite: Native Service options and catch-all Ingress +templates: + - server-service.yaml + - hubble-service.yaml + - pd-service-client.yaml + - server-ingress.yaml + - hubble-ingress.yaml +tests: + - it: defaults to automatic NodePorts for Server and Hubble + template: server-service.yaml + asserts: + - equal: + path: spec.type + value: NodePort + - notExists: + path: spec.ports[0].nodePort + - it: enables the Hubble NodePort by default + template: hubble-service.yaml + asserts: + - equal: + path: spec.type + value: NodePort + - it: leaves PD reachable only inside the cluster by default + template: pd-service-client.yaml + asserts: + - equal: + path: spec.type + value: ClusterIP + - it: renders a valid custom NodePort below the standard cluster range + template: server-service.yaml + set: + server.service.nodePort: 8802 + asserts: + - equal: + path: spec.ports[0].nodePort + value: 8802 + - it: omits zero NodePort for automatic allocation + template: hubble-service.yaml + set: + hubble.service.nodePort: 0 + asserts: + - notExists: + path: spec.ports[0].nodePort + - it: preserves explicit native load balancer settings including false + template: server-service.yaml + set: + server.service.type: LoadBalancer + server.service.externalTrafficPolicy: Local + server.service.loadBalancerClass: internal.example/controller + server.service.loadBalancerIP: 10.1.2.3 + server.service.loadBalancerSourceRanges: [10.0.0.0/8] + server.service.externalIPs: [10.1.2.4] + server.service.allocateLoadBalancerNodePorts: false + asserts: + - equal: + path: spec.externalTrafficPolicy + value: Local + - equal: + path: spec.loadBalancerClass + value: internal.example/controller + - equal: + path: spec.loadBalancerIP + value: 10.1.2.3 + - equal: + path: spec.loadBalancerSourceRanges + value: [10.0.0.0/8] + - equal: + path: spec.externalIPs + value: [10.1.2.4] + - equal: + path: spec.allocateLoadBalancerNodePorts + value: false + - it: applies native load balancer settings to Hubble + template: hubble-service.yaml + set: + hubble.service.type: LoadBalancer + hubble.service.externalTrafficPolicy: Local + hubble.service.allocateLoadBalancerNodePorts: false + asserts: + - equal: + path: spec.externalTrafficPolicy + value: Local + - equal: + path: spec.allocateLoadBalancerNodePorts + value: false + - it: applies native load balancer settings to PD + template: pd-service-client.yaml + set: + pd.service.type: LoadBalancer + pd.service.externalTrafficPolicy: Local + pd.service.allocateLoadBalancerNodePorts: false + asserts: + - equal: + path: spec.externalTrafficPolicy + value: Local + - equal: + path: spec.allocateLoadBalancerNodePorts + value: false + - it: renders hostless Server ingress using native catch-all semantics + template: server-ingress.yaml + set: + server.ingress.enabled: true + server.ingress.hosts: + - paths: + - path: / + pathType: Prefix + asserts: + - notExists: + path: spec.rules[0].host + - equal: + path: spec.rules[0].http.paths[0].path + value: / + - it: renders an empty host as a Hubble catch-all ingress + template: hubble-ingress.yaml + set: + hubble.ingress.enabled: true + hubble.ingress.hosts: + - host: "" + paths: + - path: / + pathType: Prefix + asserts: + - notExists: + path: spec.rules[0].host + - it: supplies a catch-all rule when ingress hosts are omitted + template: server-ingress.yaml + set: + server.ingress.enabled: true + server.ingress.hosts: [] + asserts: + - notExists: + path: spec.rules[0].host + - equal: + path: spec.rules[0].http.paths[0].backend.service.name + value: RELEASE-NAME-hugegraph-server diff --git a/helm/hugegraph/tests/networkpolicy_test.yaml b/helm/hugegraph/tests/networkpolicy_test.yaml new file mode 100644 index 0000000000..edc7f6e835 --- /dev/null +++ b/helm/hugegraph/tests/networkpolicy_test.yaml @@ -0,0 +1,991 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +suite: NetworkPolicy +templates: + - networkpolicy.yaml +tests: + - it: enables NetworkPolicy by strict mode defaults + set: + mode: strict + asserts: + - hasDocuments: + count: 4 + + - it: honors explicit false over the strict NetworkPolicy default + set: + mode: strict + networkPolicy.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: renders nothing with the base values + asserts: + - hasDocuments: + count: 0 + + - it: renders nothing with the single preset as shipped + values: + - ../values-single.yaml + asserts: + - hasDocuments: + count: 0 + + - it: renders no policy on the ordinary cluster preset + values: + - ../values-cluster.yaml + asserts: + - hasDocuments: + count: 0 + + - it: renders pd, store and server policies on the single preset when enabled + values: + - ../values-single.yaml + set: + networkPolicy.enabled: true + asserts: + - hasDocuments: + count: 4 + + - it: adds a Hubble policy when Hubble is enabled + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + hubble.enabled: true + asserts: + - hasDocuments: + count: 4 + + - it: isolates PD in both directions and admits only its peers and clients + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + hubble.enabled: true + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-pd + asserts: + - equal: + path: spec.podSelector.matchLabels + value: + app.kubernetes.io/name: hugegraph + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: pd + - equal: + path: spec.policyTypes + value: [Ingress, Egress] + - lengthEqual: + path: spec.ingress + count: 2 + - equal: + path: spec.ingress[0] + value: + from: + - podSelector: + matchLabels: + app.kubernetes.io/name: hugegraph + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: pd + ports: + - protocol: TCP + port: 8610 + - protocol: TCP + port: 8686 + - lengthEqual: + path: spec.ingress[1].from + count: 3 + - equal: + path: spec.ingress[1].from[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: store + - equal: + path: spec.ingress[1].from[1].podSelector.matchLabels["app.kubernetes.io/component"] + value: server + - equal: + path: spec.ingress[1].from[2].podSelector.matchLabels["app.kubernetes.io/component"] + value: hubble + - equal: + path: spec.ingress[1].ports + value: + - protocol: TCP + port: 8686 + - protocol: TCP + port: 8620 + - equal: + path: spec.egress[0].to[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: pd + - equal: + path: spec.egress[0].ports + value: + - protocol: TCP + port: 8610 + - protocol: TCP + port: 8686 + - equal: + path: spec.egress[1] + value: + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - lengthEqual: + path: spec.egress + count: 2 + + - it: lets Store peers use raft only and admits Server and Hubble on their ports + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + hubble.enabled: true + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-store + asserts: + - lengthEqual: + path: spec.ingress + count: 3 + - lengthEqual: + path: spec.ingress[0].from + count: 1 + - lengthEqual: + path: spec.ingress[1].from + count: 1 + - lengthEqual: + path: spec.ingress[2].from + count: 1 + - lengthEqual: + path: spec.egress[0].to + count: 1 + - lengthEqual: + path: spec.egress[1].to + count: 1 + - equal: + path: spec.ingress[0].from[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: store + - equal: + path: spec.ingress[0].ports + value: + - protocol: TCP + port: 8510 + - equal: + path: spec.ingress[1].from[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: server + - equal: + path: spec.ingress[1].ports + value: + - protocol: TCP + port: 8500 + - protocol: TCP + port: 8520 + - equal: + path: spec.ingress[2].from[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: hubble + - equal: + path: spec.ingress[2].ports + value: + - protocol: TCP + port: 8520 + - equal: + path: spec.egress[0].to[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: pd + - equal: + path: spec.egress[0].ports + value: + - protocol: TCP + port: 8686 + - protocol: TCP + port: 8620 + - equal: + path: spec.egress[1].to[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: store + - equal: + path: spec.egress[1].ports + value: + - protocol: TCP + port: 8510 + - equal: + path: spec.egress[2].ports + value: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + + - it: admits the test hook and Hubble to the Server and lets it reach PD and Store + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + hubble.enabled: true + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-server + asserts: + - lengthEqual: + path: spec.egress[0].to + count: 1 + - lengthEqual: + path: spec.egress[1].to + count: 1 + - lengthEqual: + path: spec.egress + count: 3 + - lengthEqual: + path: spec.ingress + count: 1 + - lengthEqual: + path: spec.ingress[0].from + count: 2 + - equal: + path: spec.ingress[0].from[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: test + - equal: + path: spec.ingress[0].from[1].podSelector.matchLabels["app.kubernetes.io/component"] + value: hubble + - equal: + path: spec.ingress[0].ports + value: + - protocol: TCP + port: 8080 + - equal: + path: spec.egress[0].ports + value: + - protocol: TCP + port: 8686 + - protocol: TCP + port: 8620 + - equal: + path: spec.egress[1].to[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: store + - equal: + path: spec.egress[1].ports + value: + - protocol: TCP + port: 8500 + - protocol: TCP + port: 8520 + - equal: + path: spec.egress[2].ports + value: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + + - it: lets PD-mode Hubble reach Server, PD and Store REST and nothing reach it + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + hubble.enabled: true + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-hubble + asserts: + - lengthEqual: + path: spec.egress + count: 4 + - notExists: + path: spec.ingress + - contains: + path: spec.policyTypes + content: Ingress + - equal: + path: spec.egress[0].to[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: server + - equal: + path: spec.egress[0].ports + value: + - protocol: TCP + port: 8080 + - equal: + path: spec.egress[1].to[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: pd + - equal: + path: spec.egress[1].ports + value: + - protocol: TCP + port: 8686 + - protocol: TCP + port: 8620 + - equal: + path: spec.egress[2].to[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: store + - equal: + path: spec.egress[2].ports + value: + - protocol: TCP + port: 8520 + - equal: + path: spec.egress[3].ports + value: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + + - it: keeps direct-mode Hubble to the Server only + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + hubble.enabled: true + hubble.mode: direct + asserts: + - lengthEqual: + path: spec.egress + count: 2 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-hubble + - lengthEqual: + path: spec.ingress[1].from + count: 2 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-pd + - equal: + path: spec.ingress[1].from[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: store + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-pd + - equal: + path: spec.ingress[1].from[1].podSelector.matchLabels["app.kubernetes.io/component"] + value: server + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-pd + - lengthEqual: + path: spec.ingress + count: 2 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-store + + - it: leaves every Hubble peer out when Hubble is disabled + values: + - ../values-cluster.yaml + set: + hubble.enabled: false + networkPolicy.enabled: true + asserts: + - hasDocuments: + count: 3 + - notContains: + path: spec.egress + content: + to: + - podSelector: + matchLabels: + app.kubernetes.io/name: hugegraph + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: hubble + - notContains: + path: spec.ingress[1].from + content: + podSelector: + matchLabels: + app.kubernetes.io/name: hugegraph + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: hubble + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-pd + - lengthEqual: + path: spec.ingress + count: 2 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-store + - lengthEqual: + path: spec.ingress[0].from + count: 1 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-server + - equal: + path: spec.ingress[0].from[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: test + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-server + + - it: follows port overrides in ingress and egress + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + pd.ports.raft: 9610 + store.ports.rest: 9520 + server.port: 9080 + asserts: + - equal: + path: spec.ingress[0].ports + value: + - protocol: TCP + port: 9610 + - protocol: TCP + port: 8686 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-pd + - equal: + path: spec.egress[1].ports + value: + - protocol: TCP + port: 8500 + - protocol: TCP + port: 9520 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-server + - equal: + path: spec.ingress[0].ports + value: + - protocol: TCP + port: 9080 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-server + + - it: appends the PD extraIngress when PD is exposed and adds nothing open + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + pd.service.type: NodePort + pd.service.allowInsecureExposure: true + networkPolicy.pd.extraIngress: + - from: + - ipBlock: + cidr: 10.0.0.0/8 + ports: + - port: 8620 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-pd + asserts: + - lengthEqual: + path: spec.ingress + count: 3 + - exists: + path: spec.ingress[0].from + - exists: + path: spec.ingress[1].from + - equal: + path: spec.ingress[2] + value: + from: + - ipBlock: + cidr: 10.0.0.0/8 + ports: + - port: 8620 + + - it: appends the Server extraIngress for a LoadBalancer Service and adds nothing open + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + server.service.type: LoadBalancer + server.service.allowInsecureExposure: true + networkPolicy.server.extraIngress: + - from: + - ipBlock: + cidr: 192.168.0.0/16 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-server + asserts: + - lengthEqual: + path: spec.ingress + count: 2 + - exists: + path: spec.ingress[0].from + - equal: + path: spec.ingress[1].from[0].ipBlock.cidr + value: 192.168.0.0/16 + + - it: appends the Server extraIngress for an Ingress and keeps the test peer first + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + server.ingress.enabled: true + server.ingress.allowPlainHttp: true + networkPolicy.server.extraIngress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ingress-nginx + podSelector: + matchLabels: + app.kubernetes.io/name: ingress-nginx + ports: + - port: 8080 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-server + asserts: + - lengthEqual: + path: spec.ingress + count: 2 + - equal: + path: spec.ingress[0].from[0].podSelector.matchLabels["app.kubernetes.io/component"] + value: test + - equal: + path: spec.ingress[1] + value: + from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ingress-nginx + podSelector: + matchLabels: + app.kubernetes.io/name: ingress-nginx + ports: + - port: 8080 + + - it: appends the Server extraIngress for an advertised URL and adds nothing open + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + server.advertiseUrl: http://hg.example.com:8080 + networkPolicy.server.extraIngress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: proxy + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-server + asserts: + - lengthEqual: + path: spec.ingress + count: 2 + - exists: + path: spec.ingress[1].from + + - it: gives an exposed Hubble exactly the listed ingress rules + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + hubble.enabled: true + hubble.service.type: NodePort + hubble.service.allowInsecureExposure: true + networkPolicy.hubble.extraIngress: + - from: + - ipBlock: + cidr: 172.18.0.0/16 + ports: + - port: 8088 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-hubble + asserts: + - equal: + path: spec.ingress + value: + - from: + - ipBlock: + cidr: 172.18.0.0/16 + ports: + - port: 8088 + + - it: renders no rule without a peer on any document when every exposure is set + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + hubble.enabled: true + pd.service.type: LoadBalancer + pd.service.allowInsecureExposure: true + server.service.type: NodePort + server.service.allowInsecureExposure: true + server.advertiseUrl: http://hg.example.com:8080 + hubble.service.type: NodePort + hubble.service.allowInsecureExposure: true + networkPolicy.pd.extraIngress: + - from: + - ipBlock: + cidr: 10.0.0.0/8 + networkPolicy.server.extraIngress: + - from: + - ipBlock: + cidr: 10.0.0.0/8 + networkPolicy.hubble.extraIngress: + - from: + - ipBlock: + cidr: 10.0.0.0/8 + # advertiseUrl hands the external URL to the in-cluster Hubble too, + # so its egress must name that destination. + networkPolicy.hubble.extraEgress: + - to: + - ipBlock: + cidr: 10.0.0.0/8 + asserts: + - hasDocuments: + count: 4 + - notContains: + path: spec.ingress + content: + ports: + - protocol: TCP + port: 8686 + - protocol: TCP + port: 8620 + - notContains: + path: spec.ingress + content: + ports: + - protocol: TCP + port: 8080 + - notContains: + path: spec.ingress + content: + ports: + - protocol: TCP + port: 8088 + + - it: selects a component on every document and renders no default-deny object + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + hubble.enabled: true + asserts: + - hasDocuments: + count: 4 + - exists: + path: spec.podSelector.matchLabels["app.kubernetes.io/component"] + - exists: + path: spec.podSelector.matchLabels["app.kubernetes.io/instance"] + + - it: follows nameOverride in every selector and fullnameOverride in names + values: + - ../values-cluster.yaml + set: + hubble.enabled: false + networkPolicy.enabled: true + nameOverride: graphdb + fullnameOverride: x + asserts: + - equal: + path: metadata.name + value: x-pd + documentIndex: 0 + - equal: + path: metadata.name + value: x-store + documentIndex: 1 + - equal: + path: metadata.name + value: x-server + documentIndex: 2 + - equal: + path: spec.podSelector.matchLabels["app.kubernetes.io/name"] + value: graphdb + - equal: + path: spec.ingress[0].from[0].podSelector.matchLabels["app.kubernetes.io/name"] + value: graphdb + - equal: + path: spec.egress[0].to[0].podSelector.matchLabels["app.kubernetes.io/name"] + value: graphdb + + - it: keeps names within 63 characters and the full release name in selectors + release: + name: abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabc + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + hubble.enabled: true + asserts: + - matchRegex: + path: metadata.name + pattern: ^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$ + - equal: + path: spec.podSelector.matchLabels["app.kubernetes.io/instance"] + value: abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabc + - equal: + path: spec.egress[0].to[0].podSelector.matchLabels["app.kubernetes.io/instance"] + value: abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabc + + - it: keeps two extraIngress rules in order after the chart rules + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + networkPolicy.pd.extraIngress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - port: 8620 + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: vermeer + ports: + - port: 8686 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-pd + asserts: + - lengthEqual: + path: spec.ingress + count: 4 + - equal: + path: spec.ingress[2].from[0].namespaceSelector.matchLabels["kubernetes.io/metadata.name"] + value: monitoring + - equal: + path: spec.ingress[3].from[0].namespaceSelector.matchLabels["kubernetes.io/metadata.name"] + value: vermeer + + - it: ignores Hubble extra rules while Hubble is disabled + values: + - ../values-cluster.yaml + set: + hubble.enabled: false + networkPolicy.enabled: true + networkPolicy.hubble.extraIngress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ops + networkPolicy.hubble.extraEgress: + - to: + - ipBlock: + cidr: 10.0.0.10/32 + asserts: + - hasDocuments: + count: 3 + - notContains: + path: spec.ingress + content: + from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ops + + - it: treats a whitespace-only advertiseUrl as unset + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + server.advertiseUrl: " " + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-server + asserts: + - lengthEqual: + path: spec.ingress + count: 1 + + - it: renders a component whose networkPolicy key was removed + values: + - ../values-cluster.yaml + set: + hubble.enabled: false + networkPolicy.enabled: true + networkPolicy.pd: null + asserts: + - hasDocuments: + count: 3 + + - it: matches the reviewed cluster plus Hubble render + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + hubble.enabled: true + asserts: + - hasDocuments: + count: 4 + - matchSnapshot: {} + # helm-unittest v1.0.0 skips full comparison for matchSnapshot: {}. + # Guard release metadata explicitly until that plugin issue is resolved. + - equal: + path: metadata.labels["app.kubernetes.io/version"] + value: "latest" + documentIndex: 0 + - equal: + path: metadata.labels["helm.sh/chart"] + value: hugegraph-helm-0.2.0 + documentIndex: 0 + - equal: + path: metadata.labels["app.kubernetes.io/version"] + value: "latest" + documentIndex: 1 + - equal: + path: metadata.labels["helm.sh/chart"] + value: hugegraph-helm-0.2.0 + documentIndex: 1 + - equal: + path: metadata.labels["app.kubernetes.io/version"] + value: "latest" + documentIndex: 2 + - equal: + path: metadata.labels["helm.sh/chart"] + value: hugegraph-helm-0.2.0 + documentIndex: 2 + - equal: + path: metadata.labels["app.kubernetes.io/version"] + value: "latest" + documentIndex: 3 + - equal: + path: metadata.labels["helm.sh/chart"] + value: hugegraph-helm-0.2.0 + documentIndex: 3 + + - it: appends extra rules verbatim + values: + - ../values-cluster.yaml + set: + networkPolicy.enabled: true + hubble.enabled: true + networkPolicy.store.extraIngress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - port: 8520 + networkPolicy.hubble.extraEgress: + - to: + - ipBlock: + cidr: 10.0.0.10/32 + ports: + - port: 9200 + asserts: + - contains: + path: spec.ingress + content: + from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - port: 8520 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-store + - contains: + path: spec.egress + content: + to: + - ipBlock: + cidr: 10.0.0.10/32 + ports: + - port: 9200 + documentSelector: + path: metadata.name + value: RELEASE-NAME-hugegraph-hubble + + - it: rejects a non-boolean enabled flag through the schema + set: + networkPolicy.enabled: "yes" + asserts: + - failedTemplate: + errorPattern: "networkPolicy" + + - it: rejects egress rules on components that take none + set: + networkPolicy.store.extraEgress: + - to: + - ipBlock: + cidr: 10.0.0.0/8 + asserts: + - failedTemplate: + errorPattern: "(additional properties 'extraEgress' not allowed|Additional property extraEgress is not allowed)" + + - it: rejects a non-list extraIngress + set: + networkPolicy.pd.extraIngress: "abc" + asserts: + - failedTemplate: + errorPattern: "extraIngress" + + - it: rejects a null extraIngress rule + set: + networkPolicy.pd.extraIngress: + - null + asserts: + - failedTemplate: + errorPattern: "extraIngress" + + - it: rejects an unknown networkPolicy key + set: + networkPolicy.unknown: 1 + asserts: + - failedTemplate: + errorPattern: "(additional properties 'unknown' not allowed|Additional property unknown is not allowed)" + + - it: rejects the removed ingressControllers key + set: + networkPolicy.ingressControllers: [] + asserts: + - failedTemplate: + errorPattern: "(additional properties 'ingressControllers' not allowed|Additional property ingressControllers is not allowed)" + + - it: rejects a string enabled flag + set: + networkPolicy.enabled: "true" + asserts: + - failedTemplate: + errorPattern: "networkPolicy[./]enabled" + + - it: accepts Kubernetes-valid an extraIngress rule without from + set: + networkPolicy.server.extraIngress: + - ports: + - port: 8080 + asserts: + - notFailedTemplate: {} + - it: accepts Kubernetes-valid an extraIngress rule with an empty from + set: + networkPolicy.server.extraIngress: + - from: [] + asserts: + - notFailedTemplate: {} + - it: accepts Kubernetes-valid an empty extraIngress rule + set: + networkPolicy.pd.extraIngress: + - {} + asserts: + - notFailedTemplate: {} + - it: accepts Kubernetes-valid an empty peer + set: + networkPolicy.pd.extraIngress: + - from: + - {} + asserts: + - notFailedTemplate: {} + - it: accepts Kubernetes-valid a Hubble egress rule without to + set: + networkPolicy.hubble.extraEgress: + - ports: + - port: 9200 + asserts: + - notFailedTemplate: {} diff --git a/helm/hugegraph/tests/notes_test.yaml b/helm/hugegraph/tests/notes_test.yaml new file mode 100644 index 0000000000..a610fa7fa3 --- /dev/null +++ b/helm/hugegraph/tests/notes_test.yaml @@ -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. +# + +# The install notes warn about chart-generated credentials under a +# template-only renderer. The rollout checksums hash the generated values, +# so such a renderer rolls the Pods on every sync; the warning must say so. +suite: Install notes +templates: + - NOTES.txt +tests: + - it: directs template-only pipelines to deterministic credential sources + asserts: + - matchRegexRaw: + pattern: "Chart-generated credentials are in use: pd.auth, server.auth.admin, server.auth.token" + - matchRegexRaw: + pattern: "For GitOps/template-only pipelines use fixed credentials or existingSecret" + - notMatchRegexRaw: + pattern: "without rolling" + + - it: omits the warning when every credential comes from an existing Secret + set: + pd.auth.existingSecret: byo-pd + server.auth.admin.existingSecret: byo-admin + server.auth.token.existingSecret: byo-token + asserts: + - notMatchRegexRaw: + pattern: "Chart-generated credentials" diff --git a/helm/hugegraph/tests/pd_auth_secret_test.yaml b/helm/hugegraph/tests/pd_auth_secret_test.yaml new file mode 100644 index 0000000000..62aed6ff31 --- /dev/null +++ b/helm/hugegraph/tests/pd_auth_secret_test.yaml @@ -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. +# +suite: PD REST auth Secret wiring +tests: + - it: creates a kept release-pd-auth Secret by default + template: pd-auth-secret.yaml + asserts: + - hasDocuments: + count: 1 + - equal: + path: metadata.name + value: RELEASE-NAME-pd-auth + - equal: + path: metadata.annotations["helm.sh/resource-policy"] + value: keep + - isNotEmpty: + path: data["secret-key"] + + - it: renders no Secret when an operator-supplied Secret is named + template: pd-auth-secret.yaml + set: + pd.auth.existingSecret: my-pd-secret + asserts: + - hasDocuments: + count: 0 + + - it: hands PD the secret from the Secret, never a literal value + template: pd-statefulset.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: HG_PD_AUTH_SECRET_KEY + valueFrom: + secretKeyRef: + name: RELEASE-NAME-pd-auth + key: secret-key + - isNotEmpty: + path: spec.template.metadata.annotations["checksum/pd-auth"] + + - it: gives the Server storage wait the same secret as PD_AUTH_PASSWORD + template: server-deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: PD_AUTH_PASSWORD + valueFrom: + secretKeyRef: + name: RELEASE-NAME-pd-auth + key: secret-key + - isNotEmpty: + path: spec.template.metadata.annotations["checksum/pd-auth"] + + - it: gives Hubble the secret and writes it into the properties file + template: hubble-deployment.yaml + set: + hubble.enabled: true + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: HG_HUBBLE_PD_PASSWORD + valueFrom: + secretKeyRef: + name: RELEASE-NAME-pd-auth + key: secret-key + - matchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "operations\\.pd\\.username=hubble" + - matchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "operations\\.pd\\.password=%s" + # The wrapper refuses secret values the Java properties parser would + # reinterpret; inline values are schema-checked, an existingSecret is + # only seen here. + - matchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "printable ASCII" + - matchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "HG_HUBBLE_PD_PASSWORD:\\?" + - isNotEmpty: + path: spec.template.metadata.annotations["checksum/pd-auth"] + + - it: skips the PD secret guard in direct mode, which mounts no PD secret + template: hubble-deployment.yaml + set: + hubble.enabled: true + hubble.mode: direct + asserts: + - notMatchRegex: + path: spec.template.spec.containers[0].args[0] + pattern: "HG_HUBBLE_PD_PASSWORD" + + - it: points PD at the operator-supplied Secret and key + template: pd-statefulset.yaml + set: + pd.auth.existingSecret: my-pd-secret + pd.auth.key: pd-pass + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: HG_PD_AUTH_SECRET_KEY + valueFrom: + secretKeyRef: + name: my-pd-secret + key: pd-pass + + - it: points the Server storage wait at the operator-supplied Secret and key + template: server-deployment.yaml + set: + pd.auth.existingSecret: my-pd-secret + pd.auth.key: pd-pass + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: PD_AUTH_PASSWORD + valueFrom: + secretKeyRef: + name: my-pd-secret + key: pd-pass + + - it: rejects operator overrides of the chart-managed PD_AUTH_PASSWORD + template: server-deployment.yaml + set: + server.extraEnv: + - name: PD_AUTH_PASSWORD + value: x + asserts: + - failedTemplate: + errorPattern: "server.extraEnv must not set the chart-managed variable PD_AUTH_PASSWORD" + + - it: keeps a YAML-coercible Secret key literal + template: server-secret.yaml + set: + server.auth.admin.key: "on" + asserts: + - exists: + path: data.on diff --git a/helm/hugegraph/tests/pd_javaopts_test.yaml b/helm/hugegraph/tests/pd_javaopts_test.yaml new file mode 100644 index 0000000000..fa0d506fc4 --- /dev/null +++ b/helm/hugegraph/tests/pd_javaopts_test.yaml @@ -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. +# + +suite: PD JAVA_OPTS derivation +templates: + - pd-statefulset.yaml +tests: + - it: disables the raft IP whitelist in-cluster by default + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: JAVA_OPTS + value: "-Dpartition.default-shard-count=3 -Draft.ip-whitelist.enabled=false -Draft.rpc-timeout=3000" + + - it: re-enables the whitelist when the operator opts in + set: + pd.raftIpWhitelistEnabled: true + asserts: + - matchRegex: + path: spec.template.spec.containers[0].env[?(@.name=="JAVA_OPTS")].value + pattern: "-Draft\\.ip-whitelist\\.enabled=true" + + - it: derives shard count 1 when store replicas are below 3 + set: + store.replicas: 1 + asserts: + - matchRegex: + path: spec.template.spec.containers[0].env[?(@.name=="JAVA_OPTS")].value + pattern: "-Dpartition\\.default-shard-count=1" + + - it: appends operator javaOpts after the derived flags + set: + pd.javaOpts: "-Xmx2g" + asserts: + - matchRegex: + path: spec.template.spec.containers[0].env[?(@.name=="JAVA_OPTS")].value + pattern: "-Draft\\.rpc-timeout=3000 -Xmx2g$" + - it: renders a custom raft RPC timeout + set: + pd.raftRpcTimeoutMs: 5000 + asserts: + - matchRegex: + path: spec.template.spec.containers[0].env[?(@.name=="JAVA_OPTS")].value + pattern: "-Draft\\.rpc-timeout=5000" + - it: omits the raft RPC timeout when set empty, preserving the image default + set: + pd.raftRpcTimeoutMs: "" + asserts: + - notMatchRegex: + path: spec.template.spec.containers[0].env[?(@.name=="JAVA_OPTS")].value + pattern: "rpc-timeout" diff --git a/helm/hugegraph/tests/pd_readiness_path_test.yaml b/helm/hugegraph/tests/pd_readiness_path_test.yaml new file mode 100644 index 0000000000..ab25522109 --- /dev/null +++ b/helm/hugegraph/tests/pd_readiness_path_test.yaml @@ -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. +# + +suite: PD readiness path and Store wait path +tests: + - it: reads PD readiness from /v1/ready by default, startup and liveness from /v1/health + template: pd-statefulset.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /v1/ready + - equal: + path: spec.template.spec.containers[0].startupProbe.httpGet.path + value: /v1/health + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.path + value: /v1/health + + - it: moves only the PD readiness probe when pd.readinessPath is set back for an older image + template: pd-statefulset.yaml + set: + pd.readinessPath: /v1/health + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /v1/health + - equal: + path: spec.template.spec.containers[0].startupProbe.httpGet.path + value: /v1/health + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.path + value: /v1/health + + - it: polls /v1/ready on each PD peer in the Store wait by default + template: store-statefulset.yaml + asserts: + - matchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: 'WAIT_PATH="/v1/ready"' + - matchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: 'curl -fsS --connect-timeout 2 --max-time 5 "http://\${peer}\${WAIT_PATH}"' + + - it: polls store.waitPath on each PD peer when set back for an older image + template: store-statefulset.yaml + set: + store.waitPath: /v1/health + asserts: + - matchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: 'WAIT_PATH="/v1/health"' + - notMatchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: 'v1/ready' + + - it: leaves the Store's own probes on /v1/health whatever store.waitPath is + template: store-statefulset.yaml + set: + store.waitPath: /v1/ready + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /v1/health + + - it: keeps PD startup and liveness off the raft-aware path with several PDs + template: pd-statefulset.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.httpGet.path + value: /v1/health + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.path + value: /v1/health + + # A single PD has no election to lose, so a permanent step-down has to be + # restartable; older images pass /v1/health forever. Startup follows liveness so the + # boot-to-ready window is charged to the startup budget, not the liveness one. + - it: moves PD startup and liveness to the raft-aware path at one replica + template: pd-statefulset.yaml + set: + pd.replicas: 1 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.httpGet.path + value: /v1/ready + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.path + value: /v1/ready + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /v1/ready + + - it: lets pd.livenessPath override the derived choice in both probes + template: pd-statefulset.yaml + set: + pd.livenessPath: /v1/ready + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.httpGet.path + value: /v1/ready + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.path + value: /v1/ready diff --git a/helm/hugegraph/tests/persistence_hubble_test.yaml b/helm/hugegraph/tests/persistence_hubble_test.yaml new file mode 100644 index 0000000000..63f3850e42 --- /dev/null +++ b/helm/hugegraph/tests/persistence_hubble_test.yaml @@ -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. +# + +suite: Hubble persistence inheritance and temporary data mount +tests: + - it: creates the seventh default persistent volume after three PD and three Store claims + template: hubble-pvc.yaml + asserts: + - hasDocuments: + count: 1 + - isKind: + of: PersistentVolumeClaim + + - it: defaults to the persistent data mount with Recreate rollout + template: hubble-deployment.yaml + asserts: + - equal: + path: spec.replicas + value: 1 + - equal: + path: spec.strategy.type + value: Recreate + - exists: + path: spec.template.spec.volumes[0].persistentVolumeClaim + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].mountPath + value: /hubble-data + + - it: global temporary storage omits the Hubble PVC + template: hubble-pvc.yaml + set: + persistence.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: temporary storage uses the same datasource path on emptyDir + template: hubble-deployment.yaml + set: + persistence.enabled: false + asserts: + - equal: + path: spec.template.spec.volumes[0].emptyDir + value: {} + - notExists: + path: spec.template.spec.volumes[0].persistentVolumeClaim + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].mountPath + value: /hubble-data + - contains: + path: spec.template.spec.containers[0].env + content: + name: SPRING_DATASOURCE_URL + value: jdbc:h2:file:/hubble-data/db;DB_CLOSE_ON_EXIT=FALSE + + - it: explicit Hubble persistence wins over global temporary storage + template: hubble-pvc.yaml + set: + persistence.enabled: false + hubble.persistence.enabled: true + asserts: + - hasDocuments: + count: 1 + + - it: explicit Hubble temporary storage wins over global persistence + template: hubble-pvc.yaml + set: + persistence.enabled: true + hubble.persistence.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: disabled UI never creates a PVC + template: hubble-pvc.yaml + set: + hubble.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: empty storageClass inherits the Kubernetes default + template: hubble-pvc.yaml + set: + hubble.persistence.storageClassName: "" + asserts: + - notExists: + path: spec.storageClassName + + - it: dash storageClass explicitly disables the Kubernetes default + template: hubble-pvc.yaml + set: + hubble.persistence.storageClassName: "-" + asserts: + - equal: + path: spec.storageClassName + value: "" diff --git a/helm/hugegraph/tests/persistence_statefulset_test.yaml b/helm/hugegraph/tests/persistence_statefulset_test.yaml new file mode 100644 index 0000000000..850faeaa30 --- /dev/null +++ b/helm/hugegraph/tests/persistence_statefulset_test.yaml @@ -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. +# + +suite: PD and Store persistence inheritance +templates: + - pd-statefulset.yaml + - store-statefulset.yaml +tests: + - it: defaults to three persistent replicas per stateful component + asserts: + - equal: + path: spec.replicas + value: 3 + - lengthEqual: + path: spec.volumeClaimTemplates + count: 1 + - notExists: + path: spec.template.spec.volumes + + - it: global temporary storage mounts emptyDir without PVC retention settings + set: + persistence.enabled: false + asserts: + - notExists: + path: spec.volumeClaimTemplates + - notExists: + path: spec.persistentVolumeClaimRetentionPolicy + - equal: + path: spec.template.spec.volumes[0].emptyDir + value: {} + + - it: explicit component persistence wins over global temporary storage + set: + persistence.enabled: false + pd.storage.enabled: true + store.storage.enabled: true + asserts: + - lengthEqual: + path: spec.volumeClaimTemplates + count: 1 + - notExists: + path: spec.template.spec.volumes + + - it: explicit component temporary storage wins over global persistence + set: + persistence.enabled: true + pd.storage.enabled: false + store.storage.enabled: false + asserts: + - notExists: + path: spec.volumeClaimTemplates + - equal: + path: spec.template.spec.volumes[0].emptyDir + value: {} + + - it: empty storageClass inherits the Kubernetes default + set: + pd.storage.storageClassName: "" + store.storage.storageClassName: "" + asserts: + - notExists: + path: spec.volumeClaimTemplates[0].spec.storageClassName + + - it: dash storageClass explicitly disables the Kubernetes default + set: + pd.storage.storageClassName: "-" + store.storage.storageClassName: "-" + asserts: + - equal: + path: spec.volumeClaimTemplates[0].spec.storageClassName + value: "" + + - it: named storageClass is preserved + set: + pd.storage.storageClassName: internal-local + store.storage.storageClassName: internal-local + asserts: + - equal: + path: spec.volumeClaimTemplates[0].spec.storageClassName + value: internal-local + + - it: PD override leaves Store persistence inherited independently + template: store-statefulset.yaml + set: + persistence.enabled: true + pd.storage.enabled: false + asserts: + - lengthEqual: + path: spec.volumeClaimTemplates + count: 1 + + - it: Store override leaves PD persistence inherited independently + template: pd-statefulset.yaml + set: + persistence.enabled: false + store.storage.enabled: true + asserts: + - equal: + path: spec.template.spec.volumes[0].emptyDir + value: {} + - notExists: + path: spec.volumeClaimTemplates diff --git a/helm/hugegraph/tests/persistence_upgrade_legacy_hubble_test.yaml b/helm/hugegraph/tests/persistence_upgrade_legacy_hubble_test.yaml new file mode 100644 index 0000000000..16a3c4182d --- /dev/null +++ b/helm/hugegraph/tests/persistence_upgrade_legacy_hubble_test.yaml @@ -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. +# + +suite: Legacy Hubble storage upgrade guard +templates: + - server-deployment.yaml +kubernetesProvider: + scheme: + "apps/v1/StatefulSet": + gvr: + group: apps + version: v1 + resource: statefulsets + namespaced: true + "apps/v1/Deployment": + gvr: + group: apps + version: v1 + resource: deployments + namespaced: true + objects: + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-pd + namespace: NAMESPACE + spec: + replicas: "3" + volumeClaimTemplates: + - metadata: + name: pd-data + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-store + namespace: NAMESPACE + spec: + replicas: "3" + volumeClaimTemplates: + - metadata: + name: store-data + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: RELEASE-NAME-hugegraph-hubble + namespace: NAMESPACE + spec: + template: + spec: + containers: + - name: hubble +tests: + - it: explains how to preserve the volume-less legacy storage mode + release: + upgrade: true + asserts: + - failedTemplate: + errorPattern: "legacy Hubble has no data volume.*hubble.persistence.enabled=false.*--reuse-values" + + - it: accepts an explicit legacy temporary override despite global persistence + release: + upgrade: true + set: + hubble.persistence.enabled: false + asserts: + - isKind: + of: Deployment + + - it: checks an explicit persistent override even when the global flag is false + release: + upgrade: true + set: + persistence.enabled: false + pd.storage.enabled: true + store.storage.enabled: true + hubble.persistence.enabled: true + asserts: + - failedTemplate: + errorPattern: "legacy Hubble has no data volume" + + - it: does not apply the legacy live guard to fresh installations + release: + upgrade: false + asserts: + - isKind: + of: Deployment diff --git a/helm/hugegraph/tests/persistence_upgrade_persistent_test.yaml b/helm/hugegraph/tests/persistence_upgrade_persistent_test.yaml new file mode 100644 index 0000000000..378ae5705c --- /dev/null +++ b/helm/hugegraph/tests/persistence_upgrade_persistent_test.yaml @@ -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. +# + +suite: Persistence upgrade guards from persistent storage +templates: + - server-deployment.yaml +kubernetesProvider: + scheme: + "apps/v1/StatefulSet": + gvr: + group: apps + version: v1 + resource: statefulsets + namespaced: true + "apps/v1/Deployment": + gvr: + group: apps + version: v1 + resource: deployments + namespaced: true + objects: + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-pd + namespace: NAMESPACE + spec: + replicas: "3" + volumeClaimTemplates: + - metadata: + name: pd-data + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-store + namespace: NAMESPACE + spec: + replicas: "3" + volumeClaimTemplates: + - metadata: + name: store-data + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: RELEASE-NAME-hugegraph-hubble + namespace: NAMESPACE + spec: + template: + spec: + volumes: + - name: data + persistentVolumeClaim: + claimName: RELEASE-NAME-hugegraph-hubble-data +tests: + - it: rejects changing pd away from persistent storage on upgrade + release: + upgrade: true + set: + persistence.enabled: true + pd.storage.enabled: false + asserts: + - failedTemplate: + errorPattern: "pd.storage.enabled cannot change on an initialized release" + + - it: rejects changing store away from persistent storage on upgrade + release: + upgrade: true + set: + persistence.enabled: true + store.storage.enabled: false + asserts: + - failedTemplate: + errorPattern: "store.storage.enabled cannot change on an initialized release" + + - it: rejects changing hubble away from persistent storage on upgrade + release: + upgrade: true + set: + persistence.enabled: true + hubble.persistence.enabled: false + asserts: + - failedTemplate: + errorPattern: "hubble.persistence.enabled cannot change on an initialized release" + + - it: accepts an unchanged persistent storage upgrade + release: + upgrade: true + set: + persistence.enabled: true + asserts: + - isKind: + of: Deployment + + - it: fresh installation skips live storage migration checks + release: + upgrade: false + set: + persistence.enabled: false + asserts: + - isKind: + of: Deployment diff --git a/helm/hugegraph/tests/persistence_upgrade_temporary_test.yaml b/helm/hugegraph/tests/persistence_upgrade_temporary_test.yaml new file mode 100644 index 0000000000..a1242a25a5 --- /dev/null +++ b/helm/hugegraph/tests/persistence_upgrade_temporary_test.yaml @@ -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. +# + +suite: Persistence upgrade guards from temporary storage +templates: + - server-deployment.yaml +kubernetesProvider: + scheme: + "apps/v1/StatefulSet": + gvr: + group: apps + version: v1 + resource: statefulsets + namespaced: true + "apps/v1/Deployment": + gvr: + group: apps + version: v1 + resource: deployments + namespaced: true + objects: + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-pd + namespace: NAMESPACE + spec: + replicas: "3" + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: RELEASE-NAME-hugegraph-store + namespace: NAMESPACE + spec: + replicas: "3" + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: RELEASE-NAME-hugegraph-hubble + namespace: NAMESPACE + spec: + template: + spec: + volumes: + - name: data + emptyDir: {} +tests: + - it: rejects changing pd away from temporary storage on upgrade + release: + upgrade: true + set: + persistence.enabled: false + pd.storage.enabled: true + asserts: + - failedTemplate: + errorPattern: "pd.storage.enabled cannot change on an initialized release" + + - it: rejects changing store away from temporary storage on upgrade + release: + upgrade: true + set: + persistence.enabled: false + store.storage.enabled: true + asserts: + - failedTemplate: + errorPattern: "store.storage.enabled cannot change on an initialized release" + + - it: rejects changing hubble away from temporary storage on upgrade + release: + upgrade: true + set: + persistence.enabled: false + hubble.persistence.enabled: true + asserts: + - failedTemplate: + errorPattern: "hubble.persistence.enabled cannot change on an initialized release" + + - it: accepts an unchanged temporary storage upgrade + release: + upgrade: true + set: + persistence.enabled: false + asserts: + - isKind: + of: Deployment + + - it: fresh installation skips live storage migration checks + release: + upgrade: false + set: + persistence.enabled: true + asserts: + - isKind: + of: Deployment diff --git a/helm/hugegraph/tests/practical_notes_test.yaml b/helm/hugegraph/tests/practical_notes_test.yaml new file mode 100644 index 0000000000..5ed67fd888 --- /dev/null +++ b/helm/hugegraph/tests/practical_notes_test.yaml @@ -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. +# + +suite: Practical access notes +templates: + - NOTES.txt +tests: + - it: watches one resource type per command and links current operations docs + asserts: + - matchRegexRaw: + pattern: "kubectl get pods -n NAMESPACE -l app.kubernetes.io/instance=RELEASE-NAME -w" + - matchRegexRaw: + pattern: "kubectl get pvc -n NAMESPACE -w" + - notMatchRegexRaw: + pattern: "kubectl get pods,pvc.*-w" + - notMatchRegexRaw: + pattern: "see README" + - matchRegexRaw: + pattern: "helm/hugegraph/docs/operations.md#credentials" + - it: reports automatic NodePort discovery and seven claims + asserts: + - matchRegexRaw: + pattern: "needs 7 persistent volume claim" + - matchRegexRaw: + pattern: "kubectl get nodes -o wide" + - matchRegexRaw: + pattern: "http://:" + - notMatchRegexRaw: + pattern: "never expose" + - it: explains ClusterIP for strict mode + set: + mode: strict + asserts: + - matchRegexRaw: + pattern: "ClusterIP is reachable inside the cluster" + - it: shows LoadBalancer status discovery + set: + server.service.type: LoadBalancer + asserts: + - matchRegexRaw: + pattern: "status.loadBalancer.ingress" + - it: reports zero Hubble persistence after disabling the UI + set: + hubble.enabled: false + asserts: + - matchRegexRaw: + pattern: "needs 6 persistent volume claim" + - notMatchRegexRaw: + pattern: "Hubble 1 replica" + - it: uses HTTP for an ingress without TLS + set: + server.ingress.enabled: true + server.ingress.hosts: [{host: internal.example, paths: [{path: /, pathType: Prefix}]}] + asserts: + - matchRegexRaw: + pattern: "http://internal.example/" + - notMatchRegexRaw: + pattern: "https://internal.example/" + - it: uses HTTPS for the configured TLS host + set: + server.ingress.enabled: true + server.ingress.hosts: [{host: internal.example, paths: [{path: /, pathType: Prefix}]}] + server.ingress.tls: [{hosts: [internal.example], secretName: example-tls}] + asserts: + - matchRegexRaw: + pattern: "https://internal.example/" + - it: names a strict NodePort entry lacking an external policy rule without failing + set: + mode: strict + server.service.type: NodePort + asserts: + - matchRegexRaw: + pattern: "Server has an external entry, but NetworkPolicy has no external ingress rule" + - matchRegexRaw: + pattern: "Add networkPolicy.server.extraIngress" + + - it: names a strict hostless Ingress lacking an external policy rule + set: + mode: strict + server.ingress.enabled: true + asserts: + - matchRegexRaw: + pattern: "Server has an external entry, but NetworkPolicy has no external ingress rule" + + - it: accepts native allow-all rules without the missing-rule hint + set: + mode: strict + server.service.type: NodePort + server.ingress.enabled: true + networkPolicy.server.extraIngress: [{}] + asserts: + - notMatchRegexRaw: + pattern: "Server has an external entry, but NetworkPolicy has no external ingress rule" + + - it: does not show policy advice when an explicit override disables policies + set: + mode: strict + server.service.type: NodePort + networkPolicy.enabled: false + asserts: + - notMatchRegexRaw: + pattern: "NetworkPolicy has no external ingress rule" + + - it: names the outside discovery egress setting for strict Hubble + set: + mode: strict + server.advertiseUrl: http://external.example:8080 + asserts: + - matchRegexRaw: + pattern: "also add networkPolicy.hubble.extraEgress" + - it: names a strict ClusterIP with externalIPs lacking an external policy rule + set: + mode: strict + server.service.externalIPs: [192.0.2.10] + asserts: + - matchRegexRaw: + pattern: "Server has an external entry, but NetworkPolicy has no external ingress rule" + - matchRegexRaw: + pattern: "allows all sources and ports" diff --git a/helm/hugegraph/tests/selector_label_types_test.yaml b/helm/hugegraph/tests/selector_label_types_test.yaml new file mode 100644 index 0000000000..f53920987d --- /dev/null +++ b/helm/hugegraph/tests/selector_label_types_test.yaml @@ -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. +# + +# Label values must stay strings. An all-digit release name or a nameOverride +# such as "true" would otherwise render as a YAML number or boolean, and the +# API rejects a label map that is not map[string]string. +suite: Selector label values render as strings +templates: + - server-deployment.yaml + - pd-statefulset.yaml +tests: + - it: quotes an all-digit release name + release: + name: "123" + asserts: + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: "123" + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/instance"] + value: "123" + + - it: quotes a boolean-looking nameOverride + set: + nameOverride: "true" + asserts: + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/name"] + value: "true" + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/name"] + value: "true" diff --git a/helm/hugegraph/tests/server_auth_checksum_test.yaml b/helm/hugegraph/tests/server_auth_checksum_test.yaml new file mode 100644 index 0000000000..b25b6a67c8 --- /dev/null +++ b/helm/hugegraph/tests/server_auth_checksum_test.yaml @@ -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. +# + +suite: Server auth checksum and secret wiring +templates: + - server-deployment.yaml +tests: + - it: stamps a checksum/auth annotation on the pod template + asserts: + - matchRegex: + path: spec.template.metadata.annotations["checksum/auth"] + pattern: "^[a-f0-9]{64}$" + + - it: keeps a valid checksum when an external admin Secret is supplied + set: + server.auth.admin.existingSecret: ext-admin-secret + server.auth.admin.key: password + asserts: + - matchRegex: + path: spec.template.metadata.annotations["checksum/auth"] + pattern: "^[a-f0-9]{64}$" + + - it: never places credential material in pod metadata + set: + server.auth.admin.password: sup3rs3cr3tpw + server.auth.token.value: t0k3nvalu3PADDINGtoREACH32charsMIN + asserts: + - notMatchRegex: + path: spec.template.metadata.annotations["checksum/auth"] + pattern: "(sup3rs3cr3tpw|t0k3nvalu3PADDING)" + + - it: sources the admin password from a Secret, never a literal value + set: + server.auth.admin.password: sup3rs3cr3tpw + asserts: + - exists: + path: spec.template.spec.containers[0].env[?(@.name=="PASSWORD")].valueFrom.secretKeyRef + - notExists: + path: spec.template.spec.containers[0].env[?(@.name=="PASSWORD")].value + + - it: points the admin Secret ref at an operator-supplied Secret when set + set: + server.auth.admin.existingSecret: ext-admin-secret + asserts: + - equal: + path: spec.template.spec.containers[0].env[?(@.name=="PASSWORD")].valueFrom.secretKeyRef.name + value: ext-admin-secret + + - it: shares one JWT signing key across replicas via a Secret ref + asserts: + - exists: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_AUTH_TOKEN_SECRET")].valueFrom.secretKeyRef diff --git a/helm/hugegraph/tests/server_discovery_test.yaml b/helm/hugegraph/tests/server_discovery_test.yaml new file mode 100644 index 0000000000..5685d719a6 --- /dev/null +++ b/helm/hugegraph/tests/server_discovery_test.yaml @@ -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. +# + +suite: Server replica discovery announced to PD +templates: + - server-deployment.yaml +tests: + - it: injects the pod IP through the downward API + asserts: + - equal: + path: spec.template.spec.containers[0].env[?(@.name=="POD_IP")].valueFrom.fieldRef.fieldPath + value: status.podIP + + - it: announces each pod's own address to PD by default + asserts: + - matchRegex: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_URLS_TO_PD")].value + pattern: "^http://\\$\\(POD_IP\\):8080$" + + - it: announces the shared advertiseUrl instead when one is set + set: + server.advertiseUrl: http://graph.example.com:30080 + asserts: + - equal: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_URLS_TO_PD")].value + value: http://graph.example.com:30080 + + # The wrapper always registers server.urls_to_pd; the image would let an + # extraEnv value turn that off again or crash-loop the Pod on a bad flag. + - it: refuses an operator extraEnv that would override the PD registration flag + set: + server.extraEnv: + - name: HG_SERVER_DEPLOY_IN_K8S + value: "false" + asserts: + - failedTemplate: + errorPattern: "server.extraEnv must not set the chart-managed variable HG_SERVER_DEPLOY_IN_K8S" + + - it: refuses an operator extraEnv that would shadow POD_IP + set: + server.extraEnv: + - name: POD_IP + value: 10.0.0.1 + asserts: + - failedTemplate: + errorPattern: "POD_IP" + + - it: preserves the image service identity by default + asserts: + - notExists: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_SERVICE_ID")] + - notExists: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_GRAPHSPACE")] + + - it: selects a non-default service identity + set: + server.serviceId: analytics + server.graphSpace: team_a + asserts: + - equal: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_SERVICE_ID")].value + value: analytics + - equal: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_GRAPHSPACE")].value + value: team_a + + - it: refuses an extraEnv that shadows the service ID + set: + server.extraEnv: + - name: HG_SERVER_SERVICE_ID + value: shadow + asserts: + - failedTemplate: + errorPattern: "HG_SERVER_SERVICE_ID" + + - it: refuses an extraEnv that shadows the graph space + set: + server.extraEnv: + - name: HG_SERVER_GRAPHSPACE + value: shadow + asserts: + - failedTemplate: + errorPattern: "HG_SERVER_GRAPHSPACE" + + - it: retains a service name containing spaces + set: + server.serviceId: ops team + server.graphSpace: team_a + asserts: + - equal: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_SERVICE_ID")].value + value: ops team + + - it: refuses an extraEnv that shadows the legacy service ID + set: + server.extraEnv: + - name: SERVICE_ID + value: shadow + asserts: + - failedTemplate: + errorPattern: "SERVICE_ID" + + - it: refuses an extraEnv that shadows the legacy graph space + set: + server.extraEnv: + - name: GRAPH_SPACE + value: shadow + asserts: + - failedTemplate: + errorPattern: "GRAPH_SPACE" + + - it: escapes all dollars so Kubernetes retains a literal service ID + set: + server.serviceId: 'x-$(POD_IP)-$$(POD_IP)-$(UNKNOWN)-$name-$' + asserts: + - equal: + path: spec.template.spec.containers[0].env[?(@.name=="HG_SERVER_SERVICE_ID")].value + value: 'x-$$(POD_IP)-$$$$(POD_IP)-$$(UNKNOWN)-$$name-$$' diff --git a/helm/hugegraph/tests/server_exposure_test.yaml b/helm/hugegraph/tests/server_exposure_test.yaml new file mode 100644 index 0000000000..61bd755edf --- /dev/null +++ b/helm/hugegraph/tests/server_exposure_test.yaml @@ -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. +# + +suite: Server Service exposure and mode defaults +templates: + - server-service.yaml +tests: + - it: defaults to an automatic NodePort in easy mode + asserts: + - equal: + path: spec.type + value: NodePort + - notExists: + path: spec.ports[0].nodePort + + - it: defaults to ClusterIP in strict mode + set: + mode: strict + asserts: + - equal: + path: spec.type + value: ClusterIP + + - it: honors an explicit NodePort in strict mode without acknowledgement + set: + mode: strict + server.service.type: NodePort + server.service.nodePort: 8802 + asserts: + - equal: + path: spec.type + value: NodePort + - equal: + path: spec.ports[0].nodePort + value: 8802 + + - it: renders LoadBalancer without acknowledgement + set: + server.service.type: LoadBalancer + asserts: + - equal: + path: spec.type + value: LoadBalancer + + - it: honors an explicit ClusterIP in easy mode + set: + server.service.type: ClusterIP + asserts: + - equal: + path: spec.type + value: ClusterIP + + - it: treats zero NodePort as automatic allocation + set: + server.service.nodePort: 0 + asserts: + - notExists: + path: spec.ports[0].nodePort diff --git a/helm/hugegraph/tests/server_readiness_path_test.yaml b/helm/hugegraph/tests/server_readiness_path_test.yaml new file mode 100644 index 0000000000..aa4eeecfdf --- /dev/null +++ b/helm/hugegraph/tests/server_readiness_path_test.yaml @@ -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. +# + +suite: Server readiness path +tests: + - it: reads all three Server probes from /versions by default + template: server-deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /versions + - equal: + path: spec.template.spec.containers[0].startupProbe.httpGet.path + value: /versions + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.path + value: /versions + + - it: moves only the Server readiness probe when server.readinessPath targets /readiness + template: server-deployment.yaml + set: + server.readinessPath: /readiness + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: /readiness + - equal: + path: spec.template.spec.containers[0].startupProbe.httpGet.path + value: /versions + - equal: + path: spec.template.spec.containers[0].livenessProbe.httpGet.path + value: /versions diff --git a/helm/hugegraph/tests/server_startup_timeout_test.yaml b/helm/hugegraph/tests/server_startup_timeout_test.yaml new file mode 100644 index 0000000000..e7cafde287 --- /dev/null +++ b/helm/hugegraph/tests/server_startup_timeout_test.yaml @@ -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. +# + +suite: Server startup timeout and explicit probe budgets +templates: + - server-deployment.yaml +tests: + - it: derives a 450 second probe budget for the default 300 second storage wait + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: WAIT_STORAGE_TIMEOUT_S + value: "300" + - contains: + path: spec.template.spec.containers[0].env + content: + name: HG_SERVER_STARTUP_TIMEOUT_S + value: "150" + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 91 + + - it: coordinates an unspecified probe threshold with an extended storage wait + set: + server.storageWaitTimeoutSeconds: 600 + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: WAIT_STORAGE_TIMEOUT_S + value: "600" + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 151 + + - it: retains an explicit sufficient startup threshold + set: + server.probes.startup.failureThreshold: 200 + server.probes.startup.periodSeconds: 10 + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: HG_SERVER_STARTUP_TIMEOUT_S + value: "1690" + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 200 + + - it: refuses a conflicting explicit threshold instead of silently increasing it + set: + server.probes.startup.failureThreshold: 1 + server.probes.startup.periodSeconds: 5 + asserts: + - failedTemplate: + errorPattern: "server.probes.startup" + + - it: accepts the minimum 120 second Server startup budget after storage waiting + set: + server.probes.startup.failureThreshold: 85 + server.probes.startup.periodSeconds: 5 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 85 + - contains: + path: spec.template.spec.containers[0].env + content: + name: HG_SERVER_STARTUP_TIMEOUT_S + value: "120" + + - it: derives and rounds the unspecified threshold for a large probe period + set: + server.probes.startup.periodSeconds: 449 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 3 + + - it: keeps an explicitly configured initial delay + set: + server.probes.startup.initialDelaySeconds: 600 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.initialDelaySeconds + value: 600 + + - it: caps the image start budget at the entrypoint maximum without rewriting the threshold + set: + server.probes.startup.failureThreshold: 100000 + server.probes.startup.periodSeconds: 10 + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: HG_SERVER_STARTUP_TIMEOUT_S + value: "86400" + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 100000 + + - it: refuses an extraEnv override that diverges from the startup probe + set: + server.extraEnv: + - name: HG_SERVER_STARTUP_TIMEOUT_S + value: "60" + asserts: + - failedTemplate: + errorMessage: server.extraEnv must not set the chart-managed variable HG_SERVER_STARTUP_TIMEOUT_S + + - it: refuses a storage wait override through chart-owned environment variables + set: + server.extraEnv: + - name: WAIT_STORAGE_TIMEOUT_S + value: "60" + asserts: + - failedTemplate: + errorPattern: "server.extraEnv must not set the chart-managed variable WAIT_STORAGE_TIMEOUT_S" + + - it: accepts an explicit delayed first probe with a sufficient total budget + set: + server.probes.startup.initialDelaySeconds: 600 + server.probes.startup.failureThreshold: 1 + asserts: + - equal: + path: spec.template.spec.containers[0].startupProbe.failureThreshold + value: 1 + - contains: + path: spec.template.spec.containers[0].env + content: + name: HG_SERVER_STARTUP_TIMEOUT_S + value: "300" diff --git a/helm/hugegraph/tests/statefulset_hardening_test.yaml b/helm/hugegraph/tests/statefulset_hardening_test.yaml new file mode 100644 index 0000000000..aecbdda335 --- /dev/null +++ b/helm/hugegraph/tests/statefulset_hardening_test.yaml @@ -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. +# + +suite: StatefulSet update and PVC retention hardening +templates: + - pd-statefulset.yaml + - store-statefulset.yaml +tests: + - it: declares an explicit updateStrategy + asserts: + - equal: + path: spec.updateStrategy.type + value: RollingUpdate + + - it: retains PVCs on delete and scale-down + asserts: + - equal: + path: spec.persistentVolumeClaimRetentionPolicy.whenDeleted + value: Retain + - equal: + path: spec.persistentVolumeClaimRetentionPolicy.whenScaled + value: Retain + + - it: uses Parallel pod management for raft bring-up + asserts: + - equal: + path: spec.podManagementPolicy + value: Parallel + + - it: honours an OnDelete updateStrategy override + set: + pd.updateStrategy.type: OnDelete + store.updateStrategy.type: OnDelete + asserts: + - equal: + path: spec.updateStrategy.type + value: OnDelete diff --git a/helm/hugegraph/tests/termination_grace_period_test.yaml b/helm/hugegraph/tests/termination_grace_period_test.yaml new file mode 100644 index 0000000000..0c65493b95 --- /dev/null +++ b/helm/hugegraph/tests/termination_grace_period_test.yaml @@ -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. +# + +suite: Workload termination grace period overrides +templates: + - pd-statefulset.yaml + - store-statefulset.yaml + - server-deployment.yaml + - hubble-deployment.yaml +tests: + - it: preserves explicit zero grace periods + set: + hubble.enabled: true + pd.terminationGracePeriodSeconds: 0 + store.terminationGracePeriodSeconds: 0 + server.terminationGracePeriodSeconds: 0 + hubble.terminationGracePeriodSeconds: 0 + asserts: + - equal: + path: spec.template.spec.terminationGracePeriodSeconds + value: 0 diff --git a/helm/hugegraph/tests/test_hook_gremlin_test.yaml b/helm/hugegraph/tests/test_hook_gremlin_test.yaml new file mode 100644 index 0000000000..1e40618b98 --- /dev/null +++ b/helm/hugegraph/tests/test_hook_gremlin_test.yaml @@ -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. +# + +suite: Helm test checks Gremlin on every Server Pod +templates: + - tests/test-connection.yaml + - server-service-headless.yaml +tests: + - it: renders a headless Server Service for per-Pod enumeration + template: server-service-headless.yaml + asserts: + - equal: + path: spec.clusterIP + value: None + - notExists: + path: spec.publishNotReadyAddresses + - equal: + path: spec.selector["app.kubernetes.io/component"] + value: server + - equal: + path: metadata.name + value: RELEASE-NAME-hugegraph-server-headless + + - it: keeps the headless name inside the DNS label limit under a long override + template: server-service-headless.yaml + set: + fullnameOverride: an-extremely-long-fullname-override-that-hits-the-63-char-limit + asserts: + - matchRegex: + path: metadata.name + pattern: "^.{1,63}$" + + - it: runs a graph-bound Gremlin check against the headless endpoints + template: tests/test-connection.yaml + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: "RELEASE-NAME-hugegraph-server-headless" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "/gremlin" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "DEFAULT-hugegraph" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "FLOOR=\"3\"" + + - it: checks the explicitly selected graph in a non-default graph space + template: tests/test-connection.yaml + set: + server.graphSpace: team_a + server.testGraphAlias: team_a-demo + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: '"graph":"team_a-demo"' + - notMatchRegex: + path: spec.containers[0].command[2] + pattern: "DEFAULT-hugegraph" + + - it: preserves the default binding when graphSpace changes without a selected graph + template: tests/test-connection.yaml + set: + server.graphSpace: team_a + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: '"graph":"DEFAULT-hugegraph"' + + - it: preserves a static graph binding with an uppercase local file name + template: tests/test-connection.yaml + set: + server.testGraphAlias: DEFAULT-Demo + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: '"graph":"DEFAULT-Demo"' + + - it: defaults a null alias for both index and business checks + template: tests/test-connection.yaml + set: + server.testGraphAlias: null + server.testGremlin: "assert graph.traversal().V().count().next() == 40" + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: '(?m)^\s*BODY=.*"graph":"DEFAULT-hugegraph"' + - matchRegex: + path: spec.containers[0].command[2] + pattern: '(?m)^\s*BUSINESS_BODY=.*"graph":"DEFAULT-hugegraph"' + + - it: floors the per-Pod count at the HPA minimum when HPA is enabled + template: tests/test-connection.yaml + set: + server.hpa.enabled: true + server.hpa.minReplicas: 5 + server.resources.requests.cpu: 500m + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: "FLOOR=\"5\"" + + - it: checks linked indexes by ID without refreshing the full index cache + template: tests/test-connection.yaml + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: "getVertexLabels.*getEdgeLabels" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "graph.indexLabel" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "index.status.*ok" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "CREATED expected" + - notMatchRegex: + path: spec.containers[0].command[2] + pattern: "getIndexLabels" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "business assertions skipped" + + - it: sends optional business assertions on each Pod + template: tests/test-connection.yaml + set: + server.testGremlin: "assert graph.traversal().V().count().next() == 40" + asserts: + - matchRegex: + path: spec.containers[0].command[2] + pattern: "assert graph.traversal" + - matchRegex: + path: spec.containers[0].command[2] + pattern: "business assertion" + + - it: sends no credentials when authentication is disabled + template: tests/test-connection.yaml + set: + hubble.enabled: false + server.auth.enabled: false + asserts: + - notMatchRegex: + path: spec.containers[0].command[2] + pattern: "Authorization" + - notExists: + path: spec.containers[0].env diff --git a/helm/hugegraph/tests/test_hook_resources_test.yaml b/helm/hugegraph/tests/test_hook_resources_test.yaml new file mode 100644 index 0000000000..e96af6ba22 --- /dev/null +++ b/helm/hugegraph/tests/test_hook_resources_test.yaml @@ -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. +# + +suite: Helm test hook is resource bounded +templates: + - tests/test-connection.yaml +tests: + - it: bounds the test hook by default so quota-managed namespaces accept it + asserts: + - exists: + path: spec.containers[0].resources.limits.cpu + - exists: + path: spec.containers[0].resources.limits.memory + - exists: + path: spec.containers[0].resources.requests.cpu + - exists: + path: spec.containers[0].resources.requests.memory + + - it: lets an operator override the hook resources + set: + server.testResources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 128Mi + asserts: + - equal: + path: spec.containers[0].resources.limits.cpu + value: 500m + + - it: passes imagePullSecrets to the test hook Pod + set: + imagePullSecrets: + - name: registry-creds + asserts: + - contains: + path: spec.imagePullSecrets + content: + name: registry-creds diff --git a/helm/hugegraph/tests/topology_quorum_test.yaml b/helm/hugegraph/tests/topology_quorum_test.yaml new file mode 100644 index 0000000000..1069ed196d --- /dev/null +++ b/helm/hugegraph/tests/topology_quorum_test.yaml @@ -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. +# + +suite: PD topology, quorum math and PDB gating +tests: + - it: points the PD StatefulSet at its own headless Service + template: pd-statefulset.yaml + asserts: + - matchRegex: + path: spec.serviceName + pattern: "-hugegraph-pd$" + + - it: builds a DNS-based raft peer list that tracks pd.replicas + template: pd-statefulset.yaml + set: + pd.replicas: 3 + asserts: + - matchRegex: + path: spec.template.spec.containers[0].env[?(@.name=="HG_PD_RAFT_PEERS_LIST")].value + pattern: "pd-0\\..*svc.*pd-1\\..*svc.*pd-2\\..*svc" + + - it: shrinks the raft peer list for a single-node install + template: pd-statefulset.yaml + set: + pd.replicas: 1 + pd.partition.defaultShardCount: 1 + store.replicas: 1 + asserts: + - notMatchRegex: + path: spec.template.spec.containers[0].env[?(@.name=="HG_PD_RAFT_PEERS_LIST")].value + pattern: "pd-1\\." + + - it: waits for a majority of 2 when PD has 3 replicas + template: store-statefulset.yaml + set: + pd.replicas: 3 + asserts: + - matchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: "REQUIRED=2" + + - it: waits for a majority of 3 when PD has 5 replicas + template: store-statefulset.yaml + set: + pd.replicas: 5 + pd.pdb.minAvailable: 3 + asserts: + - matchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: "REQUIRED=3" + + - it: waits for a majority of 1 when PD is single-node + template: store-statefulset.yaml + set: + pd.replicas: 1 + pd.partition.defaultShardCount: 1 + store.replicas: 1 + asserts: + - matchRegex: + path: spec.template.spec.initContainers[0].command[2] + pattern: "REQUIRED=1" + + - it: emits no PodDisruptionBudget for a single-replica PD + template: pd-pdb.yaml + set: + pd.replicas: 1 + pd.partition.defaultShardCount: 1 + store.replicas: 1 + asserts: + - hasDocuments: + count: 0 + + - it: publishes not-ready addresses so peer DNS resolves before readiness + template: pd-service-headless.yaml + asserts: + - equal: + path: spec.publishNotReadyAddresses + value: true + - equal: + path: spec.clusterIP + value: None + + # The PD replica guard reads the live StatefulSet, which a render never has, + # so a fresh install at any supported replica count must pass. The guarded + # cases, growing or shrinking an initialized group, need a cluster and + # cannot be reached from a render. + - it: leaves a fresh install at five PD replicas alone + template: pd-statefulset.yaml + set: + pd.replicas: 5 + pd.pdb.minAvailable: 3 + asserts: + - equal: + path: spec.replicas + value: 5 diff --git a/helm/hugegraph/tests/upgrade_entry_notice_test.yaml b/helm/hugegraph/tests/upgrade_entry_notice_test.yaml new file mode 100644 index 0000000000..aeb5b31556 --- /dev/null +++ b/helm/hugegraph/tests/upgrade_entry_notice_test.yaml @@ -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. +# + +suite: Upgrade entry default notice +templates: + - NOTES.txt +tests: + - it: reports both inherited NodePort entries during an easy upgrade + release: + upgrade: true + asserts: + - matchRegexRaw: + pattern: "Upgrade notice: chart 0.2.0 changed entry defaults" + - matchRegexRaw: + pattern: "inherits NodePort for Server, Hubble" + - matchRegexRaw: + pattern: "docs/operations.md#storage-and-upgrades" + + - it: keeps fresh-install notes free of an upgrade notice + release: + upgrade: false + asserts: + - notMatchRegexRaw: + pattern: "Upgrade notice:" + + - it: omits the disabled Hubble entry from the upgrade notice + release: + upgrade: true + set: + hubble.enabled: false + asserts: + - matchRegexRaw: + pattern: "inherits NodePort for Server\\." + - notMatchRegexRaw: + pattern: "inherits NodePort for Server, Hubble" + + - it: only names the inherited Hubble entry with an explicit internal Server + release: + upgrade: true + set: + server.service.type: ClusterIP + asserts: + - matchRegexRaw: + pattern: "inherits NodePort for Hubble" + - notMatchRegexRaw: + pattern: "inherits NodePort for Server" + + - it: accepts explicit NodePort entries without the inherited-default notice + release: + upgrade: true + set: + server.service.type: NodePort + hubble.service.type: NodePort + asserts: + - notMatchRegexRaw: + pattern: "Upgrade notice:" + + - it: preserves explicit easy-mode internal entries without the notice + release: + upgrade: true + set: + server.service.type: ClusterIP + hubble.service.type: ClusterIP + asserts: + - notMatchRegexRaw: + pattern: "Upgrade notice:" + + - it: does not report an easy-default change for a strict-mode upgrade + release: + upgrade: true + set: + mode: strict + server.service.type: NodePort + asserts: + - notMatchRegexRaw: + pattern: "Upgrade notice:" diff --git a/helm/hugegraph/tests/validate_values_test.yaml b/helm/hugegraph/tests/validate_values_test.yaml new file mode 100644 index 0000000000..7f83a4779d --- /dev/null +++ b/helm/hugegraph/tests/validate_values_test.yaml @@ -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. +# + +suite: validateValues guard rails +templates: + - server-deployment.yaml +tests: + - it: rejects a PDB that would let PD drop below raft majority + set: + pd.pdb.minAvailable: 1 + asserts: + - failedTemplate: + errorPattern: "pd.pdb.minAvailable must be at least the PD Raft majority" + + - it: rejects a PDB that would permanently block drains + set: + pd.pdb.minAvailable: 3 + asserts: + - failedTemplate: + errorPattern: "must be less than pd.replicas" + + - it: rejects an even shard count + set: + pd.partition.defaultShardCount: 2 + asserts: + - failedTemplate: + errorPattern: "must be odd" + + - it: rejects a shard count above store.replicas + set: + pd.partition.defaultShardCount: 5 + asserts: + - failedTemplate: + errorPattern: "greater than store.replicas" + + - it: rejects operator overrides of the chart-managed JAVA_OPTIONS + set: + server.extraEnv: + - name: JAVA_OPTIONS + value: "-Xmx1g" + asserts: + - failedTemplate: + errorPattern: "must not set the chart-managed variable JAVA_OPTIONS" + + - it: accepts a PD NodePort under NetworkPolicy without pd extraIngress + set: + networkPolicy.enabled: true + pd.service.type: NodePort + pd.service.allowInsecureExposure: true + asserts: + - notFailedTemplate: {} + - it: accepts a PD LoadBalancer under NetworkPolicy without pd extraIngress + set: + networkPolicy.enabled: true + pd.service.type: LoadBalancer + pd.service.allowInsecureExposure: true + asserts: + - notFailedTemplate: {} + - it: accepts a Server NodePort under NetworkPolicy without server extraIngress + set: + networkPolicy.enabled: true + server.service.type: NodePort + server.service.allowInsecureExposure: true + asserts: + - notFailedTemplate: {} + - it: accepts a Server LoadBalancer under NetworkPolicy without server extraIngress + set: + networkPolicy.enabled: true + server.service.type: LoadBalancer + server.service.allowInsecureExposure: true + asserts: + - notFailedTemplate: {} + - it: accepts a Server Ingress under NetworkPolicy without server extraIngress + set: + networkPolicy.enabled: true + server.ingress.enabled: true + server.ingress.allowPlainHttp: true + asserts: + - notFailedTemplate: {} + - it: accepts a Server advertiseUrl under NetworkPolicy without server extraIngress + set: + networkPolicy.enabled: true + server.advertiseUrl: http://hg.example.com:8080 + asserts: + - notFailedTemplate: {} + - it: accepts a Hubble NodePort under NetworkPolicy without hubble extraIngress + set: + networkPolicy.enabled: true + hubble.enabled: true + hubble.service.type: NodePort + hubble.service.allowInsecureExposure: true + asserts: + - notFailedTemplate: {} + - it: accepts a Hubble LoadBalancer under NetworkPolicy without hubble extraIngress + set: + networkPolicy.enabled: true + hubble.enabled: true + hubble.service.type: LoadBalancer + hubble.service.allowInsecureExposure: true + asserts: + - notFailedTemplate: {} + - it: accepts a Hubble Ingress under NetworkPolicy without hubble extraIngress + set: + networkPolicy.enabled: true + hubble.enabled: true + hubble.ingress.enabled: true + hubble.ingress.allowPlainHttp: true + asserts: + - notFailedTemplate: {} + - it: accepts an advertised Server URL under explicit NetworkPolicy + set: + networkPolicy.enabled: true + server.advertiseUrl: http://hg.example.com:8080 + asserts: + - notFailedTemplate: {} + - it: accepts Server exposure without an acknowledgement or source confirmation + set: + networkPolicy.enabled: true + server.service.type: NodePort + asserts: + - notFailedTemplate: {} + - it: accepts PD exposure without an acknowledgement or source confirmation + set: + networkPolicy.enabled: true + pd.service.type: NodePort + asserts: + - notFailedTemplate: {} + - it: leaves exposure alone while NetworkPolicy is off + set: + server.service.type: LoadBalancer + server.service.allowInsecureExposure: true + server.advertiseUrl: http://hg.example.com:8080 + asserts: + - hasDocuments: + count: 1 + + - it: ignores Hubble exposure settings while Hubble is disabled + set: + networkPolicy.enabled: true + hubble.service.type: NodePort + asserts: + - hasDocuments: + count: 1 + + - it: rejects Hubble without Server auth + set: + server.auth.enabled: false + hubble.enabled: true + asserts: + - failedTemplate: + errorPattern: "hubble.enabled requires server.auth" + + - it: accepts a plain-HTTP Hubble Ingress + set: + hubble.ingress.enabled: true + hubble.enabled: true + asserts: + - notFailedTemplate: {} + - it: rejects an empty Hubble image tag + set: + hubble.image.tag: "" + hubble.enabled: true + asserts: + - failedTemplate: + errorPattern: "hubble.image needs a tag or a digest" + + - it: rejects a Store PDB that permits two concurrent evictions + set: + store.replicas: 4 + asserts: + - failedTemplate: + errorPattern: "store.pdb.minAvailable must be at least store.replicas - 1" + + - it: rejects podLabels that overwrite a selector label + set: + store.podLabels: + app.kubernetes.io/component: hacked + asserts: + - failedTemplate: + errorPattern: "store.podLabels must not set app.kubernetes.io/component" + + - it: accepts a TLS-less Server Ingress without the plain-HTTP opt-in + set: + server.ingress.enabled: true + asserts: + - notFailedTemplate: {} + - it: accepts a non-ClusterIP PD Service without the exposure acknowledgement + set: + pd.service.type: NodePort + asserts: + - notFailedTemplate: {} + - it: rejects a read-only root filesystem on Hubble + set: + hubble.enabled: true + hubble.securityContext.readOnlyRootFilesystem: true + asserts: + - failedTemplate: + errorPattern: "readOnlyRootFilesystem=true breaks Hubble" + + - it: rejects a read-only root filesystem on Server + set: + server.securityContext.readOnlyRootFilesystem: true + asserts: + - failedTemplate: + errorPattern: "readOnlyRootFilesystem=true breaks Server" + + - it: rejects a Server pod annotation under the chart-owned checksum prefix + set: + server.podAnnotations: + checksum/auth: pinned + asserts: + - failedTemplate: + errorPattern: "server.podAnnotations must not set checksum/auth" + + - it: rejects a PD pod annotation under the chart-owned checksum prefix + set: + pd.podAnnotations: + checksum/pd-auth: pinned + asserts: + - failedTemplate: + errorPattern: "pd.podAnnotations must not set checksum/pd-auth" + + - it: accepts an advertised Server URL that Hubble's egress policy would block + set: + networkPolicy.enabled: true + hubble.enabled: true + server.advertiseUrl: https://hg.example.com + networkPolicy.server.extraIngress: + - from: + - ipBlock: + cidr: 10.0.0.0/8 + asserts: + - notFailedTemplate: {} + - it: accepts an advertised Server URL once Hubble egress names it + set: + networkPolicy.enabled: true + hubble.enabled: true + server.advertiseUrl: https://hg.example.com + networkPolicy.server.extraIngress: + - from: + - ipBlock: + cidr: 10.0.0.0/8 + networkPolicy.hubble.extraEgress: + - to: + - ipBlock: + cidr: 10.0.0.0/8 + asserts: + - notFailedTemplate: {} diff --git a/helm/hugegraph/values-cluster.yaml b/helm/hugegraph/values-cluster.yaml new file mode 100644 index 0000000000..c9b39bd3d9 --- /dev/null +++ b/helm/hugegraph/values-cluster.yaml @@ -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. +# + +# Multi-node starting point: 3 Server + 3 PD + 3 Store. +# Size this profile again for the real graph, traffic, and failure budget. +# Usage: helm install hg ./helm/hugegraph -f values-cluster.yaml +# Entry and network policy inherit mode (easy by default). +# Add --set mode=strict for ClusterIP and NetworkPolicy, or override either explicitly. + +pd: + replicas: 3 + javaOpts: >- + -Xms256m -Xmx512m + -XX:MaxMetaspaceSize=256m + -XX:MaxDirectMemorySize=256m + -XX:+UseContainerSupport + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: "2" + memory: 2Gi + # Keep PD quorum members on separate nodes. + antiAffinity: required + pdb: + enabled: true + minAvailable: 2 + storage: + size: 10Gi + +store: + replicas: 3 + # Store readiness is listener-only (/v1/health), so an automatic rolling + # update can replace the next Store while the previous one is still + # restoring its partitions. OnDelete stops automatic advancement; it is + # not by itself a safety proof: the manual one-at-a-time procedure with + # its per-group database checks (README, Upgrading) still applies. + updateStrategy: + type: OnDelete + javaOpts: >- + -Xms512m -Xmx1024m + -XX:MaxMetaspaceSize=256m + -XX:MaxDirectMemorySize=512m + -XX:+UseContainerSupport + # Container limits do not configure JVM heap or RocksDB native cache budgets. + # Size this preset for the workload and configure those application budgets + # separately; javaOpts accepts explicit JVM/system-property overrides. + resources: + requests: + cpu: "1" + memory: 5Gi + limits: + cpu: "4" + memory: 8Gi + waitResources: + requests: + cpu: 25m + memory: 32Mi + limits: + cpu: 250m + memory: 64Mi + # Pinned to "required": production must not co-locate shard replicas. + antiAffinity: required + pdb: + enabled: true + minAvailable: 2 + storage: + size: 50Gi + +server: + replicas: 3 + javaOpts: >- + -Xms512m -Xmx1024m + -XX:MaxMetaspaceSize=256m + -XX:MaxDirectMemorySize=256m + -XX:+UseContainerSupport + resources: + requests: + cpu: "1" + memory: 1Gi + limits: + cpu: "4" + memory: 2Gi + hpa: + enabled: false + # Keep at least two Servers through voluntary evictions such as node + # drains; the default leaves the PDB off because Server holds no quorum. + pdb: + enabled: true + minAvailable: 2 + # Auth is on by default (chart-managed admin Secret). Pin it here so a + # production overlay cannot accidentally drop authentication. + auth: + enabled: true + admin: + autoGenerate: true + token: + autoGenerate: true + +# Hubble is enabled by default. Disable with --set hubble.enabled=false. +# The optional resource override below remains available when needed. +# +# hubble: +# enabled: true +# persistence: +# enabled: true +# size: 1Gi +# resources: +# requests: +# cpu: 250m +# memory: 768Mi +# limits: +# cpu: "1" +# memory: 1536Mi + +# Network isolation is selected by mode or an explicit enabled override. diff --git a/helm/hugegraph/values-single.yaml b/helm/hugegraph/values-single.yaml new file mode 100644 index 0000000000..92f4acf4d5 --- /dev/null +++ b/helm/hugegraph/values-single.yaml @@ -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. +# + +# Convenience preset: single-node / local kind-minikube development. +# Usage: helm install hg ./helm/hugegraph -f values-single.yaml + +pd: + replicas: 1 + antiAffinity: disabled + pdb: + enabled: false + storage: + size: 5Gi + +store: + replicas: 1 + antiAffinity: disabled + pdb: + enabled: false + storage: + size: 10Gi + +server: + replicas: 1 + hpa: + enabled: false + auth: + enabled: true + admin: + autoGenerate: true + token: + autoGenerate: true diff --git a/helm/hugegraph/values-strict.yaml b/helm/hugegraph/values-strict.yaml new file mode 100644 index 0000000000..576f79c3bd --- /dev/null +++ b/helm/hugegraph/values-strict.yaml @@ -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. +# + + +# Optional security defaults. Component values and --set override this mode. +mode: strict diff --git a/helm/hugegraph/values.schema.json b/helm/hugegraph/values.schema.json new file mode 100644 index 0000000000..987e650c16 --- /dev/null +++ b/helm/hugegraph/values.schema.json @@ -0,0 +1,1594 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HugeGraph HStore Helm values", + "type": "object", + "required": [ + "pd", + "store", + "server" + ], + "properties": { + "fullnameOverride": { + "type": "string" + }, + "nameOverride": { + "type": "string" + }, + "imagePullSecrets": { + "type": "array", + "items": { + "type": "object" + } + }, + "pd": { + "$ref": "#/definitions/pd" + }, + "store": { + "$ref": "#/definitions/store" + }, + "server": { + "$ref": "#/definitions/server" + }, + "global": { + "type": "object", + "properties": { + "imageRegistry": { + "type": "string" + } + } + }, + "hubble": { + "$ref": "#/definitions/hubble" + }, + "networkPolicy": { + "$ref": "#/definitions/networkPolicy" + }, + "mode": { + "type": "string", + "enum": [ + "easy", + "strict" + ] + }, + "persistence": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + } + } + } + }, + "definitions": { + "networkPolicyPeers": { + "type": "array", + "items": { + "type": "object" + } + }, + "networkPolicyIngressRules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "from": { + "$ref": "#/definitions/networkPolicyPeers" + } + } + } + }, + "networkPolicyEgressRules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "to": { + "$ref": "#/definitions/networkPolicyPeers" + } + } + } + }, + "networkPolicyComponent": { + "type": "object", + "additionalProperties": false, + "properties": { + "extraIngress": { + "$ref": "#/definitions/networkPolicyIngressRules" + } + } + }, + "networkPolicy": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "pd": { + "$ref": "#/definitions/networkPolicyComponent" + }, + "store": { + "$ref": "#/definitions/networkPolicyComponent" + }, + "server": { + "$ref": "#/definitions/networkPolicyComponent" + }, + "hubble": { + "type": "object", + "additionalProperties": false, + "properties": { + "extraIngress": { + "$ref": "#/definitions/networkPolicyIngressRules" + }, + "extraEgress": { + "$ref": "#/definitions/networkPolicyEgressRules" + } + } + } + } + }, + "optionalPositiveInteger": { + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "string", + "pattern": "^([1-9][0-9]*)?$" + } + ] + }, + "image": { + "type": "object", + "additionalProperties": false, + "required": [ + "repository", + "tag", + "pullPolicy" + ], + "properties": { + "repository": { + "type": "string", + "minLength": 1 + }, + "tag": { + "type": "string" + }, + "pullPolicy": { + "type": "string", + "enum": [ + "Always", + "IfNotPresent", + "Never" + ] + }, + "digest": { + "type": "string", + "description": "Optional immutable image digest, for example sha256:... Wins over tag when set." + } + } + }, + "resources": { + "type": "object", + "additionalProperties": false, + "properties": { + "requests": { + "$ref": "#/definitions/resourceList" + }, + "limits": { + "$ref": "#/definitions/resourceList" + } + } + }, + "updateStrategy": { + "type": "object", + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "OnDelete" + } + }, + "required": [ + "type" + ] + }, + "then": { + "not": { + "required": [ + "rollingUpdate" + ] + } + } + } + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "RollingUpdate", + "OnDelete" + ] + }, + "rollingUpdate": { + "type": "object", + "additionalProperties": false, + "properties": { + "partition": { + "type": "integer", + "minimum": 0 + }, + "maxUnavailable": { + "type": "integer", + "const": 1 + } + } + } + } + }, + "persistentVolumeClaimRetentionPolicy": { + "type": "object", + "additionalProperties": false, + "properties": { + "whenDeleted": { + "type": "string", + "enum": [ + "Retain", + "Delete" + ] + }, + "whenScaled": { + "type": "string", + "enum": [ + "Retain", + "Delete" + ] + } + } + }, + "resourceList": { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "number" + ] + }, + "properties": { + "cpu": { + "type": [ + "string", + "number" + ] + }, + "memory": { + "type": [ + "string", + "number" + ] + }, + "ephemeral-storage": { + "type": [ + "string", + "number" + ] + } + } + }, + "storage": { + "type": "object", + "additionalProperties": false, + "required": [ + "size", + "storageClassName" + ], + "properties": { + "size": { + "type": "string", + "minLength": 1 + }, + "storageClassName": { + "type": "string" + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "pdb": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "minAvailable" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "minAvailable": { + "type": "integer", + "minimum": 1 + } + } + }, + "probe": { + "type": "object", + "additionalProperties": false, + "required": [ + "periodSeconds", + "failureThreshold" + ], + "properties": { + "periodSeconds": { + "type": "integer", + "minimum": 1 + }, + "failureThreshold": { + "type": "integer", + "minimum": 1 + }, + "timeoutSeconds": { + "type": "integer", + "minimum": 1 + }, + "initialDelaySeconds": { + "type": "integer", + "minimum": 0 + }, + "successThreshold": { + "type": "integer", + "minimum": 1 + } + } + }, + "probes": { + "type": "object", + "additionalProperties": false, + "required": [ + "startup", + "readiness", + "liveness" + ], + "properties": { + "startup": { + "allOf": [ + { + "$ref": "#/definitions/probe" + }, + { + "properties": { + "successThreshold": { + "maximum": 1 + } + } + } + ] + }, + "readiness": { + "$ref": "#/definitions/probe" + }, + "liveness": { + "allOf": [ + { + "$ref": "#/definitions/probe" + }, + { + "properties": { + "successThreshold": { + "maximum": 1 + } + } + } + ] + } + } + }, + "ports": { + "type": "object", + "additionalProperties": false, + "required": [ + "grpc", + "rest", + "raft" + ], + "properties": { + "grpc": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "rest": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "raft": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + } + }, + "pd": { + "type": "object", + "additionalProperties": false, + "required": [ + "replicas", + "image", + "ports", + "dataPath", + "storage", + "resources", + "antiAffinity", + "pdb", + "probes" + ], + "properties": { + "replicas": { + "type": "integer", + "minimum": 1, + "maximum": 99 + }, + "image": { + "$ref": "#/definitions/image" + }, + "javaOpts": { + "type": "string" + }, + "raftIpWhitelistEnabled": { + "type": "boolean" + }, + "raftRpcTimeoutMs": { + "$ref": "#/definitions/optionalPositiveInteger" + }, + "updateStrategy": { + "$ref": "#/definitions/updateStrategy" + }, + "persistentVolumeClaimRetentionPolicy": { + "$ref": "#/definitions/persistentVolumeClaimRetentionPolicy" + }, + "partition": { + "type": "object", + "additionalProperties": false, + "properties": { + "defaultShardCount": { + "$ref": "#/definitions/optionalPositiveInteger" + }, + "storeMaxShardCount": { + "$ref": "#/definitions/optionalPositiveInteger" + } + } + }, + "ports": { + "$ref": "#/definitions/ports" + }, + "dataPath": { + "type": "string", + "minLength": 1 + }, + "auth": { + "type": "object", + "additionalProperties": false, + "required": [ + "value", + "existingSecret", + "key", + "autoGenerate" + ], + "description": "PD REST Basic-auth secret (auth.secret-key), shared with the Server storage wait and Hubble.", + "properties": { + "value": { + "type": "string", + "pattern": "^([\\x21-\\x5b\\x5d-\\x7e]([\\x20-\\x5b\\x5d-\\x7e]*[\\x21-\\x5b\\x5d-\\x7e])?)?$", + "description": "Plaintext secret. Empty defers to existingSecret or autoGenerate. Printable ASCII with no backslash, and no leading or trailing space: Commons Configuration trims a properties value, so padding would make the stored secret and the effective one differ." + }, + "existingSecret": { + "type": "string" + }, + "key": { + "type": "string", + "minLength": 1 + }, + "autoGenerate": { + "type": "boolean" + } + }, + "anyOf": [ + { + "properties": { + "existingSecret": { + "minLength": 1 + } + } + }, + { + "properties": { + "value": { + "minLength": 1 + }, + "existingSecret": { + "const": "" + } + } + }, + { + "properties": { + "autoGenerate": { + "const": true + }, + "existingSecret": { + "const": "" + } + } + } + ] + }, + "storage": { + "$ref": "#/definitions/storage" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "antiAffinity": { + "type": "string", + "enum": [ + "required", + "preferred", + "disabled" + ] + }, + "pdb": { + "$ref": "#/definitions/pdb" + }, + "probes": { + "$ref": "#/definitions/probes" + }, + "podSecurityContext": { + "type": "object" + }, + "securityContext": { + "type": "object" + }, + "nodeSelector": { + "type": "object" + }, + "tolerations": { + "type": "array" + }, + "affinity": { + "type": "object" + }, + "topologySpreadConstraints": { + "type": "array" + }, + "priorityClassName": { + "type": "string" + }, + "podAnnotations": { + "type": "object" + }, + "podLabels": { + "type": "object" + }, + "extraEnv": { + "type": "array" + }, + "terminationGracePeriodSeconds": { + "type": "integer", + "minimum": 0 + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "annotations": { + "type": "object" + }, + "automountServiceAccountToken": { + "type": "boolean" + } + } + }, + "service": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "ClusterIP", + "NodePort", + "LoadBalancer" + ] + }, + "annotations": { + "type": "object" + }, + "allowInsecureExposure": { + "type": "boolean", + "description": "Legacy compatibility field; does not gate Service exposure." + }, + "restNodePort": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 65535 + }, + "grpcNodePort": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 65535 + }, + "externalTrafficPolicy": { + "type": "string", + "enum": [ + "Cluster", + "Local" + ] + }, + "loadBalancerClass": { + "type": "string" + }, + "loadBalancerIP": { + "type": "string" + }, + "loadBalancerSourceRanges": { + "type": "array", + "items": { + "type": "string" + } + }, + "externalIPs": { + "type": "array", + "items": { + "type": "string" + } + }, + "allocateLoadBalancerNodePorts": { + "type": "boolean" + } + } + }, + "livenessPath": { + "type": "string", + "pattern": "^(/.*)?$", + "description": "HTTP path for the PD startup and liveness probes. Empty derives it from pd.replicas: /v1/health above one replica, /v1/ready at one (apache/hugegraph#3222)" + }, + "readinessPath": { + "type": "string", + "pattern": "^/", + "description": "HTTP path for the PD readinessProbe; /v1/ready (default) on PD images that serve it, /v1/health on older ones" + } + } + }, + "store": { + "type": "object", + "additionalProperties": false, + "required": [ + "replicas", + "image", + "ports", + "dataPath", + "storage", + "resources", + "antiAffinity", + "pdb", + "waitImage", + "probes" + ], + "properties": { + "replicas": { + "type": "integer", + "minimum": 1, + "maximum": 99 + }, + "image": { + "$ref": "#/definitions/image" + }, + "javaOpts": { + "type": "string" + }, + "ports": { + "$ref": "#/definitions/ports" + }, + "dataPath": { + "type": "string", + "minLength": 1 + }, + "storage": { + "$ref": "#/definitions/storage" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "antiAffinity": { + "type": "string", + "enum": [ + "required", + "preferred", + "disabled" + ] + }, + "pdb": { + "$ref": "#/definitions/pdb" + }, + "waitImage": { + "type": "string" + }, + "waitResources": { + "$ref": "#/definitions/resources" + }, + "probes": { + "$ref": "#/definitions/probes" + }, + "podSecurityContext": { + "type": "object" + }, + "securityContext": { + "type": "object" + }, + "waitTimeoutSeconds": { + "type": "integer", + "minimum": 1 + }, + "updateStrategy": { + "$ref": "#/definitions/updateStrategy" + }, + "persistentVolumeClaimRetentionPolicy": { + "$ref": "#/definitions/persistentVolumeClaimRetentionPolicy" + }, + "nodeSelector": { + "type": "object" + }, + "tolerations": { + "type": "array" + }, + "affinity": { + "type": "object" + }, + "topologySpreadConstraints": { + "type": "array" + }, + "priorityClassName": { + "type": "string" + }, + "podAnnotations": { + "type": "object" + }, + "podLabels": { + "type": "object" + }, + "extraEnv": { + "type": "array" + }, + "terminationGracePeriodSeconds": { + "type": "integer", + "minimum": 0 + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "annotations": { + "type": "object" + }, + "automountServiceAccountToken": { + "type": "boolean" + } + } + }, + "waitPath": { + "type": "string", + "pattern": "^/", + "description": "HTTP path the Store init container polls on each PD peer; /v1/ready (default) on PD images that serve it, /v1/health on older ones" + } + } + }, + "server": { + "type": "object", + "additionalProperties": false, + "required": [ + "replicas", + "image", + "port", + "backend", + "resources", + "waitImage", + "initStoreEnabled", + "auth", + "ingress", + "hpa", + "probes" + ], + "properties": { + "serviceId": { + "type": "string", + "minLength": 1, + "pattern": "^[ -~]+$", + "description": "Printable ASCII PD service name (bytes 0x20-0x7E), including spaces and backslashes. DEFAULT preserves the image config." + }, + "graphSpace": { + "type": "string", + "minLength": 1, + "pattern": "^(DEFAULT|[a-z][a-z0-9_]{0,47})$", + "description": "Existing PD graph space: DEFAULT or up to 48 lowercase letters, digits and underscores, starting with a letter. DEFAULT preserves the image config." + }, + "testGraphAlias": { + "type": "string", + "pattern": "^(DEFAULT|[a-z][a-z0-9_]{0,47})-[A-Za-z][A-Za-z0-9_]{0,47}$", + "description": "Existing Gremlin graph binding checked by helm test, as -. Choose the target graph explicitly; server.graphSpace does not create or infer this binding." + }, + "replicas": { + "type": "integer", + "minimum": 1 + }, + "image": { + "$ref": "#/definitions/image" + }, + "javaOpts": { + "type": "string" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "backend": { + "type": "string", + "const": "hstore" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "waitImage": { + "type": "string", + "description": "Image used by the Helm test hook" + }, + "waitResources": { + "$ref": "#/definitions/resources" + }, + "initStoreEnabled": { + "type": "boolean", + "const": false + }, + "auth": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "admin", + "token" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "admin": { + "type": "object", + "additionalProperties": false, + "required": [ + "password", + "existingSecret", + "key", + "autoGenerate" + ], + "properties": { + "password": { + "type": "string", + "description": "Inline admin password. Empty defers to existingSecret or autoGenerate. Printable ASCII with no space, no backslash and no colon, the same contract the Server wrapper enforces on an existingSecret: the Server decodes Basic-auth credentials as ASCII and splits them on every colon, so a non-ASCII password would be applied to the admin account and then answer 401 on every request, and a password with a colon would answer 400; the image entrypoint writes each space into rest-server.properties as a backslash-escaped space, which the Server keeps, and Commons Configuration trims a properties value and unescapes backslashes, so a space or a backslash would make the stored password and the effective one differ." + }, + "existingSecret": { + "type": "string" + }, + "key": { + "type": "string", + "minLength": 1 + }, + "autoGenerate": { + "type": "boolean" + } + } + }, + "token": { + "type": "object", + "additionalProperties": false, + "required": [ + "value", + "existingSecret", + "key", + "autoGenerate" + ], + "properties": { + "value": { + "type": "string", + "description": "JWT signing key. Either empty, which defers to existingSecret or autoGenerate, or at least 32 characters: the Server entrypoint rejects a shorter key and the pods CrashLoop." + }, + "existingSecret": { + "type": "string" + }, + "key": { + "type": "string", + "minLength": 1 + }, + "autoGenerate": { + "type": "boolean" + } + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "enabled": { + "const": true + } + } + }, + "then": { + "properties": { + "admin": { + "anyOf": [ + { + "properties": { + "existingSecret": { + "minLength": 1 + } + } + }, + { + "properties": { + "password": { + "minLength": 1 + }, + "existingSecret": { + "const": "" + } + } + }, + { + "properties": { + "autoGenerate": { + "const": true + }, + "existingSecret": { + "const": "" + } + } + } + ], + "allOf": [ + { + "if": { + "properties": { + "existingSecret": { + "const": "" + } + }, + "required": [ + "existingSecret" + ] + }, + "then": { + "properties": { + "password": { + "pattern": "^[\\x21-\\x39\\x3b-\\x5b\\x5d-\\x7e]*$" + } + } + } + } + ] + }, + "token": { + "anyOf": [ + { + "properties": { + "existingSecret": { + "minLength": 1 + } + } + }, + { + "properties": { + "value": { + "minLength": 1 + }, + "existingSecret": { + "const": "" + } + } + }, + { + "properties": { + "autoGenerate": { + "const": true + }, + "existingSecret": { + "const": "" + } + } + } + ], + "allOf": [ + { + "if": { + "properties": { + "existingSecret": { + "const": "" + } + }, + "required": [ + "existingSecret" + ] + }, + "then": { + "properties": { + "value": { + "anyOf": [ + { + "maxLength": 0 + }, + { + "minLength": 32 + } + ] + } + } + } + } + ] + } + } + } + } + ] + }, + "ingress": { + "$ref": "#/definitions/serverIngress" + }, + "hpa": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "minReplicas", + "maxReplicas", + "targetCPUUtilizationPercentage" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "minReplicas": { + "type": "integer", + "minimum": 1 + }, + "maxReplicas": { + "type": "integer", + "minimum": 1 + }, + "targetCPUUtilizationPercentage": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + }, + "readinessPath": { + "type": "string", + "pattern": "^/", + "description": "HTTP path for the Server readinessProbe; /readiness (apache/hugegraph#3212) on Server images that serve it, /versions (default) on older ones" + }, + "probes": { + "$ref": "#/definitions/serverProbes" + }, + "antiAffinity": { + "type": "string", + "enum": [ + "required", + "preferred", + "disabled" + ] + }, + "podSecurityContext": { + "type": "object" + }, + "securityContext": { + "type": "object" + }, + "pdb": { + "$ref": "#/definitions/pdb" + }, + "nodeSelector": { + "type": "object" + }, + "tolerations": { + "type": "array" + }, + "affinity": { + "type": "object" + }, + "topologySpreadConstraints": { + "type": "array" + }, + "priorityClassName": { + "type": "string" + }, + "podAnnotations": { + "type": "object" + }, + "podLabels": { + "type": "object" + }, + "extraEnv": { + "type": "array" + }, + "terminationGracePeriodSeconds": { + "type": "integer", + "minimum": 0 + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "annotations": { + "type": "object" + }, + "automountServiceAccountToken": { + "type": "boolean" + } + } + }, + "testResources": { + "$ref": "#/definitions/resources" + }, + "testGremlin": { + "type": "string", + "description": "Optional Gremlin assertions run with the hook's Server credential on each Pod. Supply trusted read-only scripts; read-only execution is not enforced." + }, + "service": { + "$ref": "#/definitions/service" + }, + "advertiseUrl": { + "type": "string", + "description": "URL registered with PD via server.urls_to_pd. Empty uses the in-cluster Server Service URL. Set to an externally reachable URL for outside Hubble PD discovery." + }, + "storageWaitTimeoutSeconds": { + "type": "integer", + "minimum": 1, + "maximum": 86400 + } + } + }, + "service": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": [ + "string", + "null" + ], + "enum": [ + "ClusterIP", + "NodePort", + "LoadBalancer", + null + ] + }, + "annotations": { + "type": "object" + }, + "allowInsecureExposure": { + "type": "boolean", + "description": "Legacy compatibility field; does not gate Service exposure." + }, + "nodePort": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 65535 + }, + "externalTrafficPolicy": { + "type": "string", + "enum": [ + "Cluster", + "Local" + ] + }, + "loadBalancerClass": { + "type": "string" + }, + "loadBalancerIP": { + "type": "string" + }, + "loadBalancerSourceRanges": { + "type": "array", + "items": { + "type": "string" + } + }, + "externalIPs": { + "type": "array", + "items": { + "type": "string" + } + }, + "allocateLoadBalancerNodePorts": { + "type": "boolean" + } + } + }, + "ingress": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "className", + "hosts", + "tls" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "className": { + "type": "string" + }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "paths" + ], + "properties": { + "host": { + "type": "string" + }, + "paths": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "pathType" + ], + "properties": { + "path": { + "type": "string" + }, + "pathType": { + "type": "string", + "enum": [ + "Exact", + "Prefix", + "ImplementationSpecific" + ] + } + } + } + } + } + } + }, + "tls": { + "type": "array", + "items": { + "type": "object" + } + }, + "annotations": { + "type": "object" + }, + "allowPlainHttp": { + "type": "boolean", + "description": "Legacy compatibility field; HTTP Ingress needs no acknowledgement." + } + } + }, + "serverIngress": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "className", + "hosts", + "tls" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "className": { + "type": "string" + }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "paths" + ], + "properties": { + "host": { + "type": "string" + }, + "paths": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "pathType" + ], + "properties": { + "path": { + "type": "string" + }, + "pathType": { + "type": "string", + "enum": [ + "Exact", + "Prefix", + "ImplementationSpecific" + ] + } + } + } + } + } + } + }, + "tls": { + "type": "array", + "items": { + "type": "object" + } + }, + "annotations": { + "type": "object" + }, + "allowPlainHttp": { + "type": "boolean", + "description": "Legacy compatibility field; HTTP Ingress needs no acknowledgement." + } + } + }, + "hubble": { + "type": "object", + "additionalProperties": false, + "required": [ + "enabled", + "mode", + "image", + "port", + "persistence", + "resources", + "service", + "ingress", + "probes" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "mode": { + "type": "string", + "enum": [ + "pd", + "direct" + ] + }, + "allowWithoutServerAuth": { + "type": "boolean" + }, + "image": { + "$ref": "#/definitions/image" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "persistence": { + "type": "object", + "additionalProperties": false, + "required": [ + "size", + "storageClassName" + ], + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "size": { + "type": "string", + "minLength": 1 + }, + "storageClassName": { + "type": "string" + } + } + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "podSecurityContext": { + "type": "object" + }, + "securityContext": { + "type": "object" + }, + "nodeSelector": { + "type": "object" + }, + "tolerations": { + "type": "array" + }, + "affinity": { + "type": "object" + }, + "topologySpreadConstraints": { + "type": "array" + }, + "priorityClassName": { + "type": "string" + }, + "podAnnotations": { + "type": "object" + }, + "podLabels": { + "type": "object" + }, + "extraEnv": { + "type": "array" + }, + "terminationGracePeriodSeconds": { + "type": "integer", + "minimum": 0 + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "annotations": { + "type": "object" + }, + "automountServiceAccountToken": { + "type": "boolean" + } + } + }, + "service": { + "$ref": "#/definitions/service" + }, + "ingress": { + "$ref": "#/definitions/ingress" + }, + "probes": { + "$ref": "#/definitions/probes" + } + } + }, + "serverStartupProbe": { + "type": "object", + "additionalProperties": false, + "required": [ + "periodSeconds" + ], + "properties": { + "periodSeconds": { + "type": "integer", + "minimum": 1 + }, + "failureThreshold": { + "type": [ + "integer", + "null" + ], + "minimum": 1 + }, + "timeoutSeconds": { + "type": "integer", + "minimum": 1 + }, + "initialDelaySeconds": { + "type": "integer", + "minimum": 0 + }, + "successThreshold": { + "type": "integer", + "minimum": 1 + } + } + }, + "serverProbes": { + "type": "object", + "additionalProperties": false, + "required": [ + "startup", + "readiness", + "liveness" + ], + "properties": { + "startup": { + "allOf": [ + { + "$ref": "#/definitions/serverStartupProbe" + }, + { + "properties": { + "successThreshold": { + "maximum": 1 + } + } + } + ] + }, + "readiness": { + "$ref": "#/definitions/probe" + }, + "liveness": { + "allOf": [ + { + "$ref": "#/definitions/probe" + }, + { + "properties": { + "successThreshold": { + "maximum": 1 + } + } + } + ] + } + } + } + } +} diff --git a/helm/hugegraph/values.yaml b/helm/hugegraph/values.yaml new file mode 100644 index 0000000000..c81a9ff5c8 --- /dev/null +++ b/helm/hugegraph/values.yaml @@ -0,0 +1,561 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Default values for HugeGraph HStore Helm chart. +# See values-single.yaml and values-cluster.yaml for presets. + +# easy: browser/API NodePorts; strict: internal Services and NetworkPolicy. +# Explicit component parameters always override these mode defaults. +mode: easy +global: + imageRegistry: "" +persistence: + enabled: true + +fullnameOverride: "" +nameOverride: "" + +imagePullSecrets: [] + +pd: + replicas: 3 + image: + repository: hugegraph/pd + # TODO: leave tag empty to inherit Chart.appVersion after the 1.8.0 + # release images are published. Pin a tag or digest for production. + tag: latest + # Optional immutable digest, for example sha256:abc... When set it wins over + # tag and the image is pulled by digest, which is what a release gate should + # assert instead of a mutable tag. + digest: "" + pullPolicy: Always + # Empty preserves the image entrypoint's automatic JVM sizing. The chart + # renders its partition settings below as -D system properties ahead of + # this value, so flags placed here win on conflict. + javaOpts: "" + # Partition sharding, rendered as -D system properties that outrank the + # image's conf/application.yml. PD copies both values into its own + # persisted metadata at first bootstrap only; from then on the stored + # values are authoritative, so changing these later (or scaling + # store.replicas across the derivation boundary) has no effect on an + # initialized cluster. Post-bootstrap changes go through PD's config API + # and take effect on existing shard groups only when a partition patrol + # is triggered; see the README's Partition Sharding section. + partition: + # Shard replicas per partition, seeding PD at first bootstrap only. + # Empty derives 3 when store.replicas is at least 3, else 1, so a fresh + # multi-store install gets store-level HA instead of the image default + # of 1. The derivation skips 2 because PD clamps a shard count of 2 to + # 1 (two shards cannot elect a leader). An explicit value must be odd + # and must not exceed store.replicas. + defaultShardCount: "" + # Maximum shards per store, seeding PD at first bootstrap only. Also + # fixes the initial partition count: store.replicas x this value / + # shard count. The derived shard count of 3 gives the default topology + # 12 partitions instead of 36; raise this value to compensate when more + # partitions are wanted. Empty preserves the image default of 12. + storeMaxShardCount: "" + ports: + grpc: 8686 + rest: 8620 + raft: 8610 + dataPath: /hugegraph-pd/pd_data + # PD REST Basic-auth credential. PD compares the password of every /v1 + # request against auth.secret-key and refuses to start without one + # (HG_PD_AUTH_SECRET_KEY). The chart hands the same value to the Server + # storage wait (PD_AUTH_PASSWORD, user store) and to Hubble + # (operations.pd.password, user hubble). The Secret is kept on uninstall + # like the Server auth Secrets. The value must be printable ASCII with + # no leading whitespace and no backslashes: it is written into Hubble's + # Java properties file, which strips leading whitespace, unescapes + # backslashes, and reads the file as ISO-8859-1. Priority: + # existingSecret > value > autoGenerate. + auth: + # Optional plaintext secret (prefer existingSecret in shared clusters). + value: "" + # Pre-created Secret name. Must contain the key below; chart does not manage it. + existingSecret: "" + key: secret-key + # When existingSecret and value are empty, create a kept release-pd-auth Secret. + autoGenerate: true + storage: + # Unset inherits persistence.enabled; false uses temporary emptyDir. + enabled: + size: 10Gi + storageClassName: "" + resources: {} + # Pod-level and container-level securityContext. Empty by default because the + # published images run as root; set these to satisfy a restricted namespace. + podSecurityContext: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + # required | preferred | disabled. "preferred" schedules on clusters with + # fewer nodes than replicas; production should use "required" (see + # values-cluster.yaml) so one node failure cannot take out PD quorum. + antiAffinity: preferred + # Scheduling. `affinity` takes precedence over the antiAffinity preset. + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + priorityClassName: "" + podAnnotations: {} + podLabels: {} + # Extra environment variables appended to the pd container. + extraEnv: [] + # Gives a JVM with an on-disk store time to shut down cleanly on drain. + terminationGracePeriodSeconds: 300 + serviceAccount: + create: true + name: "" + annotations: {} + # This chart makes no Kubernetes API calls, so no token is mounted. + automountServiceAccountToken: false + # Client Service for PD REST/gRPC. Defaults to ClusterIP for in-cluster + # consumers (Server, Store, in-chart Hubble). Set type to NodePort or + # LoadBalancer when an outside Hubble must reach PD for discovery. + service: + type: ClusterIP + annotations: {} + # Legacy acknowledgement, accepted for compatibility; no render gate. + allowInsecureExposure: false + # Optional fixed NodePorts; require service.type NodePort or LoadBalancer. + restNodePort: + grpcNodePort: + pdb: + enabled: true + minAvailable: 2 + # PD's raft IP whitelist resolves peers once at boot, which under + # Kubernetes blocks peers whose pod IPs were not yet published or change + # later. Off by default in-cluster (upstream switch + # raft.ip-whitelist.enabled); k8s network policy/auth owns that layer. + # Set true to restore the image default. + raftIpWhitelistEnabled: false + # Raft RPC timeout in milliseconds, rendered as -Draft.rpc-timeout. It + # bounds raft requests between PDs. On images that wait for a raft + # ReadIndex before they read an id counter it is also the deadline of that + # wait: id allocation fails after this long while the leader cannot + # confirm a read, for example while a new leader commits its first entry. + # Images that have raft.rpc-connect-timeout (default 1000) use that option + # for the Ping that opens a connection to a peer; set it through + # pd.javaOpts, for example -Draft.rpc-connect-timeout=1000. It does not + # bound requests on a connection that is already open. On older images + # this timeout is also the timeout of that Ping: their default of 10000 + # leaves the cluster leaderless for about a minute where 3000 elects a new + # leader in seconds. Empty preserves the image default. + raftRpcTimeoutMs: 3000 + # Explicit rollout strategy instead of the implicit StatefulSet default. + updateStrategy: + type: RollingUpdate + # Keep graph metadata PVCs across delete and scale-down; data outlives the + # workload by default. Set to Delete for disposable environments. Honored + # on Kubernetes >=1.27 (or with the StatefulSetAutoDeletePVC feature gate); + # older API servers drop the field, which matches Retain behavior anyway. + persistentVolumeClaimRetentionPolicy: + whenDeleted: Retain + whenScaled: Retain + # Startup can take a while during Raft bootstrap + # HTTP path the PD readinessProbe hits. /v1/ready answers 503 while there + # is no raft leader, so a PD outside the quorum stops passing readiness. + # Startup and liveness stay on /v1/health so a PD that merely lost its + # leader is not restarted. + readinessPath: /v1/ready + # HTTP path the PD startup and liveness probes hit. Empty derives it from + # the replica count: /v1/health with more than one PD, so a normal election + # never restarts a follower, and /v1/ready with a single PD, which has no + # election to lose. Older images otherwise pass /v1/health forever after + # losing leadership for good (apache/hugegraph#3222). Startup follows liveness so + # the boot-to-ready window sits inside the startup budget. + # TODO: drop the single-PD readiness derivation once the minimum supported image + # includes the /v1/health fatal Raft error fix for apache/hugegraph#3222. + livenessPath: "" + probes: + startup: + failureThreshold: 30 + periodSeconds: 10 + timeoutSeconds: 5 + readiness: + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 5 + liveness: + periodSeconds: 20 + failureThreshold: 3 + timeoutSeconds: 5 + +store: + replicas: 3 + image: + repository: hugegraph/store + # TODO: leave tag empty to inherit Chart.appVersion after the 1.8.0 + # release images are published. Pin a tag or digest for production. + tag: latest + # Optional immutable digest, for example sha256:abc... When set it wins over + # tag and the image is pulled by digest, which is what a release gate should + # assert instead of a mutable tag. + digest: "" + pullPolicy: Always + # Empty preserves the image entrypoint's automatic JVM sizing. + javaOpts: "" + ports: + grpc: 8500 + raft: 8510 + rest: 8520 + dataPath: /hugegraph-store/storage + storage: + # Unset inherits persistence.enabled; false uses temporary emptyDir. + enabled: + size: 50Gi + storageClassName: "" + resources: {} + podSecurityContext: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + # required | preferred | disabled. "preferred" schedules on clusters with + # fewer nodes than replicas; production should use "required" (see + # values-cluster.yaml) so one node failure cannot co-locate shard replicas. + antiAffinity: preferred + # Scheduling. `affinity` takes precedence over the antiAffinity preset. + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + priorityClassName: "" + podAnnotations: {} + podLabels: {} + # Extra environment variables appended to the store container. + extraEnv: [] + # Gives a JVM with an on-disk store time to shut down cleanly on drain. + terminationGracePeriodSeconds: 300 + serviceAccount: + create: true + name: "" + annotations: {} + # This chart makes no Kubernetes API calls, so no token is mounted. + automountServiceAccountToken: false + pdb: + enabled: true + minAvailable: 2 + # Optional tool image override; empty reuses the Store image. + waitImage: "" + # HTTP path the init container polls on every PD peer; a majority must + # answer 2xx before the Store starts. /v1/ready makes that majority a + # raft quorum instead of a set of live listeners. + waitPath: /v1/ready + # Bound the PD wait so a cluster whose PDs never come up fails visibly + # instead of sitting in Init:0/1 forever. + waitTimeoutSeconds: 900 + # Optional bounds for the PD wait init container. + waitResources: {} + # Explicit rollout strategy instead of the implicit StatefulSet default. + updateStrategy: + type: RollingUpdate + # Keep graph data PVCs across delete and scale-down; data outlives the + # workload by default. Set to Delete for disposable environments. + persistentVolumeClaimRetentionPolicy: + whenDeleted: Retain + whenScaled: Retain + probes: + startup: + failureThreshold: 40 + periodSeconds: 10 + timeoutSeconds: 5 + readiness: + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 5 + liveness: + periodSeconds: 20 + failureThreshold: 3 + timeoutSeconds: 5 + +server: + replicas: 3 + # DEFAULT preserves the image config; non-default values select PD service metadata. + # serviceId accepts printable ASCII, including spaces and backslashes. + # Create a non-default graph space before starting a Server assigned to it. + serviceId: DEFAULT + graphSpace: DEFAULT + image: + repository: hugegraph/server + # TODO: leave tag empty to inherit Chart.appVersion after the 1.8.0 + # release images are published. Pin a tag or digest for production. + tag: latest + # Optional immutable digest, for example sha256:abc... When set it wins over + # tag and the image is pulled by digest, which is what a release gate should + # assert instead of a mutable tag. + digest: "" + pullPolicy: Always + # Empty preserves the image entrypoint's automatic JVM sizing. + javaOpts: "" + port: 8080 + # HTTP path the Server readinessProbe hits. /readiness (apache/hugegraph#3212) + # answers 503 while the Server cannot serve graph traffic, so such a Server + # drops out of the Service instead of answering 500 to every graph request. + # Keep /versions on Server images that do not serve it. Startup and liveness + # stay on /versions so a Server that merely lost its storage is not restarted. + readinessPath: /versions + backend: hstore + resources: {} + # Server is stateless and may scale past the node count via HPA, so the + # default only prefers spreading. Use "required" when replicas are always + # fewer than schedulable nodes. + # required | preferred | disabled + antiAffinity: preferred + # Scheduling. `affinity` takes precedence over the antiAffinity preset. + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + priorityClassName: "" + podAnnotations: {} + podLabels: {} + # Extra environment variables appended to the server container. + extraEnv: [] + # Allows in-flight requests to drain before the Server is stopped. + terminationGracePeriodSeconds: 60 + serviceAccount: + create: true + name: "" + annotations: {} + # This chart makes no Kubernetes API calls, so no token is mounted. + automountServiceAccountToken: false + podSecurityContext: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + # Server has no quorum to preserve, so no PodDisruptionBudget by default. + pdb: + enabled: false + minAvailable: 2 + # Optional Helm test image override; empty reuses the Server image. + waitImage: "" + # Existing Gremlin graph binding checked on every Server Pod. Select the + # target graph explicitly; graphSpace does not create or infer this binding. + testGraphAlias: DEFAULT-hugegraph + # Optional Gremlin assertions, run with the hook's Server credential on every Pod. + # Supply trusted read-only scripts; read-only execution is not enforced. + # Use --set-file server.testGremlin=/path/to/assertions.groovy; assert expected results. + testGremlin: "" + # Resources for the Helm test hook container. Bounded by default so the hook + # cannot run unlimited on a restricted or quota-managed namespace. + testResources: + requests: + cpu: 25m + memory: 32Mi + limits: + cpu: 250m + memory: 64Mi + # Distributed HStore: the init-store gate must be explicitly false, so that + # concurrent Server replicas never initialize the same backend. No + # HG_SERVER_SKIP_INIT and no init Job. + initStoreEnabled: false + # Maximum storage wait; startup probe defaults adapt to this value. + storageWaitTimeoutSeconds: 300 + auth: + # On by default: a chart-managed release-admin Secret supplies the password + # unless admin.existingSecret or admin.password is set. Set enabled=false + # only for trusted networks. + enabled: true + # Admin password: inline value vs Kubernetes Secret name are separate keys. + # Priority: existingSecret > password > autoGenerate. + admin: + # Optional plaintext password (prefer existingSecret in shared clusters). + password: "" + # Pre-created Secret name. Must contain key below; chart does not manage it. + existingSecret: "" + key: password + # When existingSecret and password are empty, create a kept release-admin Secret. + autoGenerate: true + # JWT signing key for auth.token_secret / HG_SERVER_AUTH_TOKEN_SECRET. + # Must be identical on every Server replica or Hubble login fails behind + # the Service. Priority: existingSecret > value > autoGenerate. + token: + # Optional plaintext signing key (prefer existingSecret in shared clusters). + value: "" + # Pre-created Secret name. Chart does not manage it. + existingSecret: "" + key: token_secret + # When existingSecret and value are empty, create a kept release-auth-token Secret. + autoGenerate: true + # URL announced to PD via server.urls_to_pd for discovery clients such as Hubble. + # Empty announces each Server Pod IP so in-cluster clients retain the full replica list. + # Set this to a URL reachable from outside the cluster (NodePort, LoadBalancer, or Ingress) when an external Hubble uses PD mode; all replicas then announce that shared logical endpoint. + advertiseUrl: "" + service: + # Unset inherits mode (easy: NodePort, strict: ClusterIP). + type: + annotations: {} + # Legacy acknowledgement, accepted for compatibility; no render gate. + allowInsecureExposure: false + ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: hugegraph.local + paths: + - path: / + pathType: Prefix + tls: [] + hpa: + enabled: false + minReplicas: 3 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + probes: + startup: + # A first probe can run immediately; (failureThreshold - 1) * periodSeconds + # must cover the storage wait plus at least 120 seconds for Server startup. + # Unset derives a budget from storageWaitTimeoutSeconds. + failureThreshold: + periodSeconds: 5 + timeoutSeconds: 5 + readiness: + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 5 + liveness: + periodSeconds: 20 + failureThreshold: 3 + timeoutSeconds: 5 + +# Hubble UI is enabled by default; disable with hubble.enabled=false. +hubble: + enabled: true + # pd: discover the cluster through PD (enables the cluster operations view). + # direct: talk to the Server client Service only, without PD discovery. + # Current Hubble images require server.auth to be enabled: the UI login + # authenticates against the cluster, so rendering fails otherwise unless + # allowWithoutServerAuth explicitly overrides for images that support it. + mode: pd + allowWithoutServerAuth: false + image: + repository: hugegraph/hubble + # Tracks latest until the next HugeGraph release tag is published. Pin the + # tag (or digest) and switch to IfNotPresent for production. + tag: latest + # Optional immutable digest, for example sha256:abc... When set it wins over + # tag and the image is pulled by digest, which is what a release gate should + # assert instead of a mutable tag. + digest: "" + pullPolicy: Always + port: 8088 + # Keeps Hubble's per-instance H2 metadata across Pod replacement; see the + # README's Hubble section for the single-replica and H2 constraints. + # size and storageClassName apply at install time only; the PVC is kept + # on helm uninstall. + persistence: + # Unset inherits persistence.enabled. + enabled: + size: 1Gi + storageClassName: "" + resources: {} + # When persistence is enabled and the pod runs as non-root, set a matching + # fsGroup here so H2 can write /hubble-data. + podSecurityContext: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + nodeSelector: {} + tolerations: [] + affinity: {} + topologySpreadConstraints: [] + priorityClassName: "" + podAnnotations: {} + podLabels: {} + # Extra environment variables appended to the hubble container. + extraEnv: [] + terminationGracePeriodSeconds: 30 + serviceAccount: + create: true + name: "" + annotations: {} + # This chart makes no Kubernetes API calls, so no token is mounted. + automountServiceAccountToken: false + service: + # Unset inherits mode (easy: NodePort, strict: ClusterIP). + type: + annotations: {} + # Legacy acknowledgement, accepted for compatibility; no render gate. + allowInsecureExposure: false + ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: hubble.local + paths: + - path: / + pathType: Prefix + tls: [] + # Legacy acknowledgement, accepted for compatibility. + allowPlainHttp: false + probes: + startup: + failureThreshold: 30 + periodSeconds: 5 + timeoutSeconds: 5 + readiness: + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 5 + liveness: + periodSeconds: 20 + failureThreshold: 3 + timeoutSeconds: 5 + +# Kubernetes NetworkPolicy: mode default plus explicit per-component rules. +# easy does not isolate clients; strict isolates release traffic and DNS. Only +# enforced when the network plugin implements NetworkPolicy (kind v0.25+, +# k3s, Calico, Cilium); elsewhere the objects are accepted and do nothing. +# Nothing outside the release is admitted unless it is listed in +# .extraIngress as standard NetworkPolicy ingress rules: apps in +# other namespaces, Prometheus, Vermeer, and the Ingress controller. Exposing +# a component does not automatically add callers: configure extraIngress +# and extraEgress using native Kubernetes NetworkPolicy rules. +networkPolicy: + # Unset inherits mode (easy: false, strict: true). + enabled: + pd: + extraIngress: [] + store: + extraIngress: [] + server: + extraIngress: [] + hubble: + extraIngress: [] + # For Hubble's optional outside endpoints (es.urls, prometheus.url set + # through extraEnv). PD, Store and Server reach only the release and DNS. + extraEgress: [] + +# No init Job; see README.md for the HStore initialization contract. +# Install with: helm install ... --wait (no --wait-for-jobs) diff --git a/helm/test_helm_hook.py b/helm/test_helm_hook.py new file mode 100644 index 0000000000..8c3a2d30f4 --- /dev/null +++ b/helm/test_helm_hook.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Execute the rendered Helm hook with local command fixtures, without networking.""" + +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +CHART = ROOT / "helm" / "hugegraph" +MOCK = r''' +import json, os, pathlib, sys +command = pathlib.Path(sys.argv[0]).name +directory = pathlib.Path(os.environ["FIXTURE_DIR"]) +if command == "date": + counter = directory / "time" + value = int(counter.read_text()) + 100 if counter.exists() else 0 + counter.write_text(str(value)) + print(value) +elif command == "getent": + for address in os.environ["FIXTURE_IPS"].split(): + print(address, "STREAM server") +elif command == "curl": + args = sys.argv[1:] + config = sys.stdin.read() if "--config" in args else "" + url = args[-1] + body = json.loads(args[args.index("-d") + 1]) if "-d" in args else None + with (directory / "requests.jsonl").open("a") as output: + output.write(json.dumps({"url": url, "body": body, + "args": args, "config": config}) + "\n") + if body is None: + print("{}") + else: + case = os.environ.get("FIXTURE_CASE", "healthy") + bad_pod = "10.0.0.2" in url + business = body["gremlin"] == os.environ.get("FIXTURE_BUSINESS") + failure = bad_pod and (case in ("binding", "invalid", "creating", "rebuilding") or + (case == "business" and business)) + if failure: + message = {"binding": "Could not rebind graph", + "invalid": "Index by_age is INVALID (CREATED expected)", + "creating": "Index by_age is CREATING (CREATED expected)", + "rebuilding": "Index by_age is REBUILDING (CREATED expected)", + "business": "Assertion failed: expected index results"}[case] + print(json.dumps({"message": message})) + print("400" if case == "binding" else "500") + else: + print('{"result":{"data":[0]}}\n200') +''' + + +class HelmHookTest(unittest.TestCase): + def render(self, directory, legacy_values=False, **values): + chart = CHART + if legacy_values: + # --reuse-values can render with old defaults that predate this key. + chart = directory / "legacy-chart" + shutil.copytree(CHART, chart) + defaults_file = chart / "values.yaml" + defaults = yaml.safe_load(defaults_file.read_text()) + defaults["server"].pop("testGraphAlias") + defaults_file.write_text(yaml.safe_dump(defaults)) + configuration = directory / "values.yaml" + configuration.write_text(yaml.safe_dump(values)) + result = subprocess.run( + ["helm", "template", "fixture", str(chart), "-f", str(configuration), + "--show-only", "templates/tests/test-connection.yaml"], + capture_output=True, text=True) + self.assertEqual(0, result.returncode, result.stderr) + return yaml.safe_load(result.stdout)["spec"]["containers"][0]["command"][2] + + def run_hook(self, case="healthy", addresses=None, legacy_values=False, **values): + temporary = tempfile.TemporaryDirectory(prefix="helm-hook-") + self.addCleanup(temporary.cleanup) + directory = Path(temporary.name) + script = self.render(directory, legacy_values=legacy_values, **values) + commands = directory / "bin" + commands.mkdir() + for name in ("curl", "date", "getent", "sleep"): + executable = commands / name + executable.write_text(f"#!{sys.executable}\n" + MOCK) + executable.chmod(0o755) + business = values.get("server", {}).get("testGremlin", "") + environment = dict(os.environ, PATH=f"{commands}:{os.environ['PATH']}", + FIXTURE_DIR=str(directory), FIXTURE_CASE=case, + FIXTURE_BUSINESS=business, PASSWORD="fixture-secret", + HG_TEST_DEADLINE_S="1", FIXTURE_IPS=addresses or + "10.0.0.1 10.0.0.2 10.0.0.3") + result = subprocess.run(["sh", "-c", script], env=environment, cwd=directory, + capture_output=True, text=True, timeout=15) + requests = [json.loads(line) for line in + (directory / "requests.jsonl").read_text().splitlines()] + return result, requests, directory + + def test_every_pod_is_checked_with_auth(self): + result, requests, _ = self.run_hook() + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertEqual(3, sum(request["body"] is not None for request in requests)) + self.assertTrue(all("Authorization: Basic " in request["config"] for request in requests)) + self.assertIn("business assertions skipped", result.stdout) + + def test_failed_binding_or_unready_cached_index_reports_pod_and_stage(self): + for case, message in (("binding", "Could not rebind graph"), + ("invalid", "Index by_age is INVALID (CREATED expected)"), + ("creating", "Index by_age is CREATING (CREATED expected)"), + ("rebuilding", "Index by_age is REBUILDING (CREATED expected)")): + with self.subTest(case=case): + result, requests, _ = self.run_hook(case) + self.assertNotEqual(0, result.returncode) + self.assertIn("Gremlin binding/index-state failed on http://10.0.0.2:8080/gremlin", + result.stdout) + self.assertIn(message, result.stdout) + self.assertEqual(3, sum(request["body"] is not None for request in requests)) + + def test_business_assertion_runs_on_every_pod_and_reports_failure(self): + business = "assert graph.traversal().V().count().next() == 7; true" + for case, expected in (("healthy", 0), ("business", 1)): + with self.subTest(case=case): + result, requests, _ = self.run_hook(case, server={"testGremlin": business}) + self.assertEqual(expected, result.returncode, result.stdout + result.stderr) + self.assertEqual(3, sum(request["body"] is not None and + request["body"]["gremlin"] == business for request in requests)) + if expected: + self.assertIn("business assertion failed on http://10.0.0.2:8080/gremlin", + result.stdout) + self.assertIn("Assertion failed: expected index results", result.stdout) + + def test_selected_alias_applies_to_index_and_business_checks(self): + business = "assert graph.traversal().V().count().next() == 7; true" + result, requests, _ = self.run_hook( + server={"testGraphAlias": "team_a-demo", "testGremlin": business}) + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + payloads = [request["body"] for request in requests if request["body"]] + self.assertEqual(6, len(payloads)) + self.assertTrue(all(payload["aliases"] == {"graph": "team_a-demo"} + for payload in payloads)) + + def test_missing_or_null_alias_preserves_default_for_both_checks(self): + business = "assert graph.traversal().V().count().next() == 7; true" + for missing in (False, True): + with self.subTest(alias="missing" if missing else "null"): + server = {"testGremlin": business} + if not missing: + server["testGraphAlias"] = None + result, requests, _ = self.run_hook(legacy_values=missing, server=server) + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + payloads = [request["body"] for request in requests if request["body"]] + self.assertEqual(6, len(payloads)) + self.assertEqual([{"graph": "DEFAULT-hugegraph"}] * 6, + [payload["aliases"] for payload in payloads]) + + def test_business_script_is_json_data_and_auth_can_be_disabled(self): + business = '''assert 'single' != "double" +// `touch injected` $(touch injected) ' " +true''' + result, requests, directory = self.run_hook( + server={"testGremlin": business, "auth": {"enabled": False}}, + hubble={"enabled": False}) + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertFalse((directory / "injected").exists()) + self.assertTrue(all(not request["config"] and "--config" not in request["args"] + for request in requests)) + self.assertEqual(3, sum(request["body"] is not None and + request["body"]["gremlin"] == business for request in requests)) + + def test_dual_stack_does_not_count_pods_twice(self): + result, requests, _ = self.run_hook( + addresses="10.0.0.1 10.0.0.2 fd00::1 fd00::2") + self.assertNotEqual(0, result.returncode) + self.assertIn("pods=2 floor=3", result.stderr) + self.assertEqual(2, sum(request["body"] is not None for request in requests)) + + def test_rendered_groovy_checks_cached_ids_without_refreshing_indexes(self): + self.assertTrue(os.environ.get("GROOVY_JAR"), + "Set GROOVY_JAR to the Server Groovy jar before running this suite") + _, requests, directory = self.run_hook() + payload = next(request["body"] for request in requests if request["body"]) + script = directory / "smoke.groovy" + script.write_text(payload["gremlin"]) + result = subprocess.run( + [shutil.which("java"), "-cp", os.environ["GROOVY_JAR"], "groovy.ui.GroovyMain", + str(ROOT / "helm" / "testdata" / "test_index_check.groovy"), str(script)], + capture_output=True, text=True, timeout=30) + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertIn("cached index checks passed", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/helm/test_helm_package.py b/helm/test_helm_package.py new file mode 100644 index 0000000000..0fea178a78 --- /dev/null +++ b/helm/test_helm_package.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Validate the artifact shipped to Helm, beyond source template tests.""" + +from pathlib import Path +import re +import shutil +import subprocess +import tarfile +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +CHART = ROOT / "helm" / "hugegraph" +PACKAGE_BUDGET = 512 * 1024 + + +class HelmPackageTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + if shutil.which("helm") is None: + raise RuntimeError("Helm is required for the packaged chart regression") + temporary = tempfile.TemporaryDirectory(prefix="hugegraph-chart-") + cls.addClassCleanup(temporary.cleanup) + cls.directory = Path(temporary.name) + cls.version = re.search(r"(?m)^version: (.+)$", + (CHART / "Chart.yaml").read_text()).group(1) + subprocess.run(["helm", "package", str(CHART), "--destination", str(cls.directory)], + check=True, capture_output=True, text=True) + packages = list(cls.directory.glob("*.tgz")) + if len(packages) != 1: + raise AssertionError("Expected exactly one packaged chart") + cls.package = packages[0] + with tarfile.open(cls.package, "r:gz") as archive: + cls.files = {} + for member in archive.getmembers(): + if member.isfile(): + name = member.name.removeprefix("hugegraph-helm/") + cls.files[name] = archive.extractfile(member).read() + + def test_distribution_identity(self): + self.assertEqual(f"hugegraph-helm-{self.version}.tgz", self.package.name) + self.assertRegex(self.files["Chart.yaml"].decode(), + r"(?m)^name: hugegraph-helm$") + rendered = subprocess.run( + ["helm", "template", "hugegraph", str(self.package)], + check=True, capture_output=True, text=True).stdout + self.assertIn("name: hugegraph-server\n", rendered) + self.assertIn('app.kubernetes.io/name: "hugegraph"', rendered) + self.assertIn(f"helm.sh/chart: hugegraph-helm-{self.version}", rendered) + + def test_archive_budget(self): + self.assertLess(self.package.stat().st_size, PACKAGE_BUDGET, + "Chart exceeds its package budget; check non-runtime assets") + + def test_no_contributor_or_binary_documentation_files(self): + excluded = ("images/", "tests/", "scripts/", "testdata/") + leaked = [name for name in self.files + if name.startswith(excluded) or name.endswith(".tgz")] + self.assertEqual([], leaked, + "Development assets must not enter the Helm release payload") + + def test_required_configuration_is_preserved(self): + required = ("Chart.yaml", "README.md", "values.yaml", "values.schema.json", + "values-single.yaml", "values-cluster.yaml", "values-strict.yaml", + "docs/operations.md", "docs/distribution.md", "examples/README.md") + for name in required: + with self.subTest(file=name): + self.assertIn(name, self.files) + # Helm normalizes Chart.yaml and values.yaml while packaging. + if name not in ("Chart.yaml", "values.yaml"): + self.assertEqual((CHART / name).read_bytes(), self.files[name]) + + def test_every_runtime_template_is_preserved(self): + for source in (CHART / "templates").rglob("*"): + if source.is_file(): + name = source.relative_to(CHART).as_posix() + with self.subTest(template=name): + self.assertIn(name, self.files) + self.assertEqual(source.read_bytes(), self.files[name]) + + def test_internal_inventory_covers_workloads_and_helpers(self): + example = (CHART / "examples" / "README.md").read_text() + inventory = example.split("## Internal images", 1)[1].split("`global.imageRegistry`", 1)[0] + images = set(re.findall(r"`(hugegraph/[^`]+:[^`]+)`", inventory)) + self.assertEqual(4, len(images), "Document all four default component images") + registry = "registry.internal.example.com" + rendered = subprocess.run( + ["helm", "template", "inventory-check", str(self.package), + "--values", str(CHART / "examples" / "values-internal-registry.yaml")], + check=True, capture_output=True, text=True).stdout + selected = set(re.findall(r'^\s+image:\s*["\']?([^"\'\s]+)', rendered, re.MULTILINE)) + self.assertEqual({f"{registry}/{image}" for image in images}, selected, + "Mirror inventory must cover workloads, init containers and test hooks") + for name in ("README.md", "examples/values-internal-registry.yaml"): + text = (CHART / name).read_text() + for image in images: + self.assertIn(image.split(":", 1)[0], text, + f"{name} omits a required image repository") + + def test_packaged_chart_renders_each_preset(self): + for preset in (None, "values-single.yaml", "values-cluster.yaml", "values-strict.yaml"): + with self.subTest(preset=preset or "default"): + command = ["helm", "template", "package-check", str(self.package)] + if preset: + # Use the values shipped inside the archive, not the source tree. + values = self.directory / preset + values.write_bytes(self.files[preset]) + command.extend(["--values", str(values)]) + rendered = subprocess.run(command, check=True, capture_output=True) + self.assertTrue(b"kind: StatefulSet" in rendered.stdout, + "Packaged chart did not render a StatefulSet") + self.assertTrue(b"kind: Deployment" in rendered.stdout, + "Packaged chart did not render a Deployment") + + +if __name__ == "__main__": + unittest.main() diff --git a/helm/testdata/test_index_check.groovy b/helm/testdata/test_index_check.groovy new file mode 100644 index 0000000000..90257b7360 --- /dev/null +++ b/helm/testdata/test_index_check.groovy @@ -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. + +// Execute the rendered script, with stale cached ID 11 but healthy metadata. +class FixtureTraversal { + def V() { this } + def limit(count) { assert count == 1; this } + def count() { 0 } +} + +class FixtureGraph { + Map statuses + List lookups = [] + def schema() { + return [getVertexLabels: { -> statuses.containsKey(11) ? [[indexLabels: { -> [11] }]] : [] }, + getEdgeLabels: { -> statuses.containsKey(22) ? [[indexLabels: { -> [22] }]] : [] }, + getIndexLabels: { -> throw new AssertionError('metadata enumeration refreshes cache') }] + } + def indexLabel(id) { + assert id instanceof Integer : 'name lookup can refresh the cache' + lookups.add(id) + return [name: { -> "index_${id}" }, + status: { -> [name: { -> statuses[id] }, ok: { -> statuses[id] == 'CREATED' }] }] + } + def traversal() { + return new FixtureTraversal() + } +} + +def source = new File(args[0]).text +[[11: 'CREATED', 22: 'CREATED'], [:]].each { statuses -> + def graph = new FixtureGraph(statuses: statuses) + assert new GroovyShell(new Binding(graph: graph)).evaluate(source) == 0 + assert graph.lookups as Set == statuses.keySet() +} +['CREATING', 'REBUILDING', 'DELETING', 'UNDELETED', 'INVALID', 'CLEARING'].each { status -> + [11, 22].each { indexId -> + def graph = new FixtureGraph(statuses: [11: 'CREATED', 22: 'CREATED']) + graph.statuses[indexId] = status + try { + new GroovyShell(new Binding(graph: graph)).evaluate(source) + throw new AssertionError('non-CREATED cached index must fail') + } catch (IllegalStateException expected) { + assert expected.message == "Index index_${indexId} is ${status} (CREATED expected)" + } + } +} +println 'cached index checks passed' diff --git a/hugegraph-cluster-test/hugegraph-clustertest-dist/src/assembly/static/conf/rest-server.properties.template b/hugegraph-cluster-test/hugegraph-clustertest-dist/src/assembly/static/conf/rest-server.properties.template index 43979ec120..f838fd2a8a 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-dist/src/assembly/static/conf/rest-server.properties.template +++ b/hugegraph-cluster-test/hugegraph-clustertest-dist/src/assembly/static/conf/rest-server.properties.template @@ -19,7 +19,7 @@ # could use '0.0.0.0' or specified (real)IP to expose external network access restserver.url=http://$REST_SERVER_ADDRESS$ # gremlin server url, need to be consistent with host and port in gremlin-server.yaml -#gremlinserver.url=http://$REST_SERVER_ADDRESS$ +gremlinserver.url=http://$GREMLIN_SERVER_ADDRESS$ graphs=./conf/graphs @@ -39,6 +39,8 @@ arthas.disabled_commands=jad auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator # true if using hstore mode usePD=true +cluster=hg +pd.peers=$PD_PEERS_LIST$ # default password auth.admin_pa=pa diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/pom.xml b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/pom.xml index b59648304f..e6b364651a 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/pom.xml +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/pom.xml @@ -30,8 +30,6 @@ - 11 - 11 UTF-8 diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/ClusterConstant.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/ClusterConstant.java index 730bbc53ed..e1bad5bf36 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/ClusterConstant.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/ClusterConstant.java @@ -49,6 +49,7 @@ public class ClusterConstant { public static final String GRAPH_TEMPLATE_FILE = "hugegraph.properties.template"; public static final String GREMLIN_DRIVER_SETTING_FILE = "gremlin-driver-settings.yaml"; public static final String GREMLIN_SERVER_FILE = "gremlin-server.yaml"; + public static final String JVM_MODULE_OPTIONS_FILE = "jvm-module.options"; public static final String REMOTE_SETTING_FILE = "remote.yaml"; public static final String REMOTE_OBJECTS_SETTING_FILE = "remote-objects.yaml"; public static final String EMPTY_SAMPLE_GROOVY_FILE = "scripts/empty-sample.groovy"; @@ -106,7 +107,7 @@ public static String getFileInDir(String path, String fileName) { return ""; } - public static boolean isJava11OrHigher() { + public static boolean isJava17OrHigher() { String version = System.getProperty("java.version"); if (version.startsWith("1.")) { version = version.substring(2, 3); @@ -117,7 +118,7 @@ public static boolean isJava11OrHigher() { } } int versionNumber = Integer.parseInt(version); - return versionNumber >= 11; + return versionNumber >= 17; } public static String getProjectDir() { diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/EnvUtil.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/EnvUtil.java index 4d4bab3831..1c5f4c1357 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/EnvUtil.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/base/EnvUtil.java @@ -18,7 +18,9 @@ package org.apache.hugegraph.ct.base; import java.io.IOException; +import java.net.InetSocketAddress; import java.net.ServerSocket; +import java.net.Socket; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -30,6 +32,7 @@ public class EnvUtil { private static final Logger LOG = HGTestLogger.UTIL_LOG; + private static final int PORT_CONNECT_TIMEOUT_MILLIS = 500; private static final Set ports = new HashSet<>(); public static int getAvailablePort() { @@ -48,6 +51,16 @@ public static int getAvailablePort() { } } + public static boolean isPortOpen(String host, int port) { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(host, port), + PORT_CONNECT_TIMEOUT_MILLIS); + return true; + } catch (IOException ignored) { + return false; + } + } + public static void copyFileToDestination(Path source, Path destination) { try { ensureParentDirectoryExists(destination); diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/ClusterConfig.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/ClusterConfig.java index c71e4b07e1..6e1e9881f0 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/ClusterConfig.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/ClusterConfig.java @@ -59,6 +59,7 @@ public ClusterConfig(int pdCnt, int storeCnt, int serverCnt) { for (int i = 0; i < serverCnt; i++) { ServerConfig serverConfig = new ServerConfig(); + serverConfig.setPDPeersList(pdGrpcList); serverConfigs.add(serverConfig); GraphConfig graphConfig = new GraphConfig(); graphConfig.setPDPeersList(pdGrpcList); diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/GremlinServerConfig.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/GremlinServerConfig.java new file mode 100644 index 0000000000..64950f83e3 --- /dev/null +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/GremlinServerConfig.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.hugegraph.ct.config; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.regex.Pattern; + +public final class GremlinServerConfig { + + private static final Pattern HOST_SETTING = + Pattern.compile("^#?host\\s*:.*$"); + private static final Pattern PORT_SETTING = + Pattern.compile("^#?port\\s*:.*$"); + + private GremlinServerConfig() { + throw new IllegalStateException("Utility class"); + } + + public static void update(Path configPath, String host, int port) { + if (port <= 0 || port > 65535) { + throw new IllegalArgumentException("Invalid Gremlin port: " + port); + } + + try { + List lines = Files.readAllLines(configPath, + StandardCharsets.UTF_8); + int hostSettings = 0; + int portSettings = 0; + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + if (HOST_SETTING.matcher(line).matches()) { + lines.set(i, "host: " + host); + hostSettings++; + } else if (PORT_SETTING.matcher(line).matches()) { + lines.set(i, "port: " + port); + portSettings++; + } + } + + if (hostSettings != 1 || portSettings != 1) { + throw new IllegalStateException( + "Expected one top-level host and port in " + configPath); + } + Files.write(configPath, lines, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException( + "Failed to update Gremlin server config " + configPath, e); + } + } +} diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/ServerConfig.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/ServerConfig.java index 569a11dddf..2840c946b9 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/ServerConfig.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/config/ServerConfig.java @@ -24,6 +24,7 @@ import static org.apache.hugegraph.ct.base.EnvUtil.getAvailablePort; import java.nio.file.Paths; +import java.util.List; import lombok.Getter; @@ -32,16 +33,24 @@ public class ServerConfig extends AbstractConfig { private final int rpcPort; private final int restPort; + private final int gremlinPort; public ServerConfig() { readTemplate(Paths.get(CONFIG_FILE_PATH + SERVER_TEMPLATE_FILE)); this.fileName = SERVER_PROPERTIES; this.rpcPort = getAvailablePort(); this.restPort = getAvailablePort(); + this.gremlinPort = getAvailablePort(); properties.put("REST_SERVER_ADDRESS", LOCALHOST + ":" + this.restPort); + properties.put("GREMLIN_SERVER_ADDRESS", + LOCALHOST + ":" + this.gremlinPort); properties.put("RPC_PORT", String.valueOf(this.rpcPort)); } + public void setPDPeersList(List pdPeersList) { + setProperty("PD_PEERS_LIST", String.join(",", pdPeersList)); + } + public void setServerID(String serverID) { setProperty("SERVER_ID", serverID); } @@ -51,4 +60,3 @@ public void setRole(String role) { } } - diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java index 0c24860929..ccf0582a00 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/AbstractEnv.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import org.apache.hugegraph.ct.base.HGTestLogger; import org.apache.hugegraph.ct.config.ClusterConfig; @@ -28,6 +29,7 @@ import org.apache.hugegraph.ct.config.PDConfig; import org.apache.hugegraph.ct.config.ServerConfig; import org.apache.hugegraph.ct.config.StoreConfig; +import org.apache.hugegraph.ct.node.AbstractNodeWrapper; import org.apache.hugegraph.ct.node.PDNodeWrapper; import org.apache.hugegraph.ct.node.ServerNodeWrapper; import org.apache.hugegraph.ct.node.StoreNodeWrapper; @@ -40,6 +42,8 @@ public abstract class AbstractEnv implements BaseEnv { private static final Logger LOG = HGTestLogger.ENV_LOG; + private static final int NODE_START_TIMEOUT_SECONDS = 120; + private static final int NODE_START_POLL_MILLIS = 1000; protected ClusterConfig clusterConfig; protected List pdNodeWrappers; @@ -71,9 +75,12 @@ protected void init(int pdCnt, int storeCnt, int serverCnt) { } for (int i = 0; i < serverCnt; i++) { - ServerNodeWrapper serverNodeWrapper = new ServerNodeWrapper(cluster_id, i); - serverNodeWrappers.add(serverNodeWrapper); ServerConfig serverConfig = clusterConfig.getServerConfig(i); + ServerNodeWrapper serverNodeWrapper = + new ServerNodeWrapper(cluster_id, i, + serverConfig.getRestPort(), + serverConfig.getGremlinPort()); + serverNodeWrappers.add(serverNodeWrapper); serverConfig.setServerID(serverNodeWrapper.getID()); GraphConfig graphConfig = clusterConfig.getGraphConfig(i); if (i == 0) { @@ -88,34 +95,54 @@ protected void init(int pdCnt, int storeCnt, int serverCnt) { public void startCluster() { for (PDNodeWrapper pdNodeWrapper : pdNodeWrappers) { - pdNodeWrapper.start(); - while (!pdNodeWrapper.isStarted()) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } + startNode(pdNodeWrapper); } for (StoreNodeWrapper storeNodeWrapper : storeNodeWrappers) { - storeNodeWrapper.start(); - while (!storeNodeWrapper.isStarted()) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } + startNode(storeNodeWrapper); } for (ServerNodeWrapper serverNodeWrapper : serverNodeWrappers) { - serverNodeWrapper.start(); - while (!serverNodeWrapper.isStarted()) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } + startNode(serverNodeWrapper); + } + } + + private void startNode(AbstractNodeWrapper nodeWrapper) { + System.out.printf("[cluster-test] starting %s in %s%n", + nodeWrapper.getID(), nodeWrapper.getNodePath()); + nodeWrapper.start(); + waitUntilStarted(nodeWrapper); + } + + private static void waitUntilStarted(AbstractNodeWrapper nodeWrapper) { + long deadline = System.nanoTime() + + TimeUnit.SECONDS.toNanos(NODE_START_TIMEOUT_SECONDS); + while (System.nanoTime() < deadline) { + if (!nodeWrapper.isAlive()) { + nodeWrapper.dumpLog(); + throw new AssertionError(String.format( + "%s failed to start, process status: %s", + nodeWrapper.getID(), nodeWrapper.processStatus())); + } + if (nodeWrapper.isStarted()) { + System.out.printf("[cluster-test] %s started%n", + nodeWrapper.getID()); + return; } + sleepBeforeRetry(); + } + + nodeWrapper.dumpLog(); + throw new AssertionError(String.format( + "%s did not start within %s seconds, process status: %s", + nodeWrapper.getID(), NODE_START_TIMEOUT_SECONDS, + nodeWrapper.processStatus())); + } + + private static void sleepBeforeRetry() { + try { + TimeUnit.MILLISECONDS.sleep(NODE_START_POLL_MILLIS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError("Interrupted while waiting node start", e); } } @@ -131,6 +158,22 @@ public void stopCluster() { } } + public void dumpClusterStatus() { + System.out.println("===== cluster node diagnostics ====="); + dumpNodeStatus(pdNodeWrappers); + dumpNodeStatus(storeNodeWrappers); + dumpNodeStatus(serverNodeWrappers); + } + + private static void dumpNodeStatus( + List nodeWrappers) { + for (AbstractNodeWrapper nodeWrapper : nodeWrappers) { + System.out.printf("[cluster-test] %s process status: %s%n", + nodeWrapper.getID(), nodeWrapper.processStatus()); + nodeWrapper.dumpLog(); + } + } + public ClusterConfig getConf() { return this.clusterConfig; } diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/BaseEnv.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/BaseEnv.java index f6c4ba5fb6..422901959b 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/BaseEnv.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/env/BaseEnv.java @@ -29,6 +29,8 @@ public interface BaseEnv { /* clear the cluster env and all config*/ void stopCluster(); + void dumpClusterStatus(); + ClusterConfig getConf(); void init(); diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/AbstractNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/AbstractNodeWrapper.java index 8236bb1392..15efdfc8b4 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/AbstractNodeWrapper.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/AbstractNodeWrapper.java @@ -20,9 +20,8 @@ import static org.apache.hugegraph.ct.base.ClusterConstant.CT_PACKAGE_PATH; import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.NoSuchFileException; @@ -147,20 +146,40 @@ public void updateConfigPath(String ConfigPath) { @Override public boolean isStarted() { - try (Scanner sc = new Scanner(new FileReader(getLogPath()))) { + if (!isAlive()) { + return false; + } + + try (Scanner sc = new Scanner(Paths.get(getLogPath()), + StandardCharsets.UTF_8.name())) { while (sc.hasNextLine()) { String line = sc.nextLine(); if (line.contains(startLine)) return true; } - } catch (FileNotFoundException ignored) { + } catch (IOException ignored) { } return false; } + protected String moduleOptions(Path options) throws IOException { + if (!Files.isRegularFile(options) || !Files.isReadable(options)) { + throw new IOException("Missing or unreadable JVM module options: " + options); + } + return "@" + options.toAbsolutePath(); + } + public void stop() { if (this.instance == null) { return; } + if (!this.instance.isAlive()) { + System.out.printf("[cluster-test] %s stopped unexpectedly: %s%n", + getID(), processStatus()); + if (dumpRuntimeLogs()) { + deleteDir(); + } + return; + } this.instance.destroy(); try { if (!this.instance.waitFor(20, TimeUnit.SECONDS)) { @@ -170,11 +189,70 @@ public void stop() { Thread.currentThread().interrupt(); LOG.error("Waiting node to shutdown error.", e); } - deleteDir(); + if (dumpRuntimeLogs()) { + deleteDir(); + } + } + + private boolean dumpRuntimeLogs() { + Path startupLog = Paths.get(getLogPath()); + try { + if (this.instance.isAlive() || !Files.isReadable(startupLog)) { + throw new IOException("Node still running or startup log unavailable: " + getID()); + } + // Stream complete text logs into the suite output before deletion. + // The suite's existing checker validates them without an archive. + try (Stream logs = Files.walk(startupLog.getParent())) { + for (Path log : (Iterable) logs.filter(Files::isRegularFile)::iterator) { + if (!log.getFileName().toString().matches(".*(\\.log(\\.[0-9]+)?|\\.out|\\.err|\\.txt)")) { + continue; + } + System.out.println("[cluster-test] runtime log: " + log); + try (Stream lines = Files.lines(log, StandardCharsets.UTF_8)) { + lines.forEach(System.out::println); + } + } + } + return true; + } catch (IOException | UncheckedIOException e) { + // Keep the original files and continue stopping the other nodes. + // This explicit marker makes the suite's log checker fail. + System.out.println("Unable to inspect cluster runtime logs: " + e.getMessage()); + return false; + } } public boolean isAlive() { - return this.instance.isAlive(); + return this.instance != null && this.instance.isAlive(); + } + + public String processStatus() { + if (this.instance == null) { + return "not started"; + } + if (this.instance.isAlive()) { + return "alive"; + } + return "exited with code " + this.instance.exitValue(); + } + + public void dumpLog() { + Path logPath = Paths.get(getLogPath()); + System.out.println("===== " + getID() + " log: " + logPath + " ====="); + if (!Files.exists(logPath)) { + System.out.println("Log file does not exist"); + return; + } + + try { + List lines = Files.readAllLines(logPath, StandardCharsets.UTF_8); + int start = Math.max(0, lines.size() - 200); + for (int i = start; i < lines.size(); i++) { + System.out.println(lines.get(i)); + } + } catch (IOException e) { + System.out.println("Failed to read log file: " + e.getMessage()); + } } protected ProcessBuilder runCmd(List startCmd, File stdoutFile) throws IOException { diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/PDNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/PDNodeWrapper.java index a89c614c4c..43ecc2be6c 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/PDNodeWrapper.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/PDNodeWrapper.java @@ -17,14 +17,16 @@ package org.apache.hugegraph.ct.node; +import static org.apache.hugegraph.ct.base.ClusterConstant.BIN_DIR; import static org.apache.hugegraph.ct.base.ClusterConstant.CONF_DIR; +import static org.apache.hugegraph.ct.base.ClusterConstant.JVM_MODULE_OPTIONS_FILE; import static org.apache.hugegraph.ct.base.ClusterConstant.JAVA_CMD; import static org.apache.hugegraph.ct.base.ClusterConstant.LOG4J_FILE; import static org.apache.hugegraph.ct.base.ClusterConstant.PD_JAR_PREFIX; import static org.apache.hugegraph.ct.base.ClusterConstant.PD_LIB_PATH; import static org.apache.hugegraph.ct.base.ClusterConstant.PD_TEMPLATE_PATH; import static org.apache.hugegraph.ct.base.ClusterConstant.getFileInDir; -import static org.apache.hugegraph.ct.base.ClusterConstant.isJava11OrHigher; +import static org.apache.hugegraph.ct.base.ClusterConstant.isJava17OrHigher; import java.io.File; import java.io.IOException; @@ -62,11 +64,14 @@ public void start() { File stdoutFile = new File(getLogPath()); List startCmd = new ArrayList<>(); startCmd.add(JAVA_CMD); - if (!isJava11OrHigher()) { - LOG.error("Please make sure that the JDK is installed and the version >= 11"); + if (!isJava17OrHigher()) { + LOG.error("Please make sure that the JDK is installed and the version >= 17"); return; } + startCmd.add(moduleOptions(Paths.get(workPath).getParent() + .resolve(BIN_DIR).resolve(JVM_MODULE_OPTIONS_FILE))); + String pdNodeJarPath = getFileInDir(workPath, PD_JAR_PREFIX); startCmd.addAll(Arrays.asList( "-Dname=HugeGraphPD" + this.index, diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java index e16b96781e..99f31dbf4f 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/ServerNodeWrapper.java @@ -17,6 +17,7 @@ package org.apache.hugegraph.ct.node; +import static org.apache.hugegraph.ct.base.ClusterConstant.BIN_DIR; import static org.apache.hugegraph.ct.base.ClusterConstant.CONF_DIR; import static org.apache.hugegraph.ct.base.ClusterConstant.EMPTY_SAMPLE_GROOVY_FILE; import static org.apache.hugegraph.ct.base.ClusterConstant.EXAMPLE_GROOVY_FILE; @@ -24,15 +25,18 @@ import static org.apache.hugegraph.ct.base.ClusterConstant.GREMLIN_DRIVER_SETTING_FILE; import static org.apache.hugegraph.ct.base.ClusterConstant.GREMLIN_SERVER_FILE; import static org.apache.hugegraph.ct.base.ClusterConstant.JAVA_CMD; +import static org.apache.hugegraph.ct.base.ClusterConstant.JVM_MODULE_OPTIONS_FILE; import static org.apache.hugegraph.ct.base.ClusterConstant.LIB_DIR; import static org.apache.hugegraph.ct.base.ClusterConstant.LOG4J_FILE; +import static org.apache.hugegraph.ct.base.ClusterConstant.LOCALHOST; import static org.apache.hugegraph.ct.base.ClusterConstant.PLUGINS_DIR; import static org.apache.hugegraph.ct.base.ClusterConstant.REMOTE_OBJECTS_SETTING_FILE; import static org.apache.hugegraph.ct.base.ClusterConstant.REMOTE_SETTING_FILE; import static org.apache.hugegraph.ct.base.ClusterConstant.SERVER_LIB_PATH; import static org.apache.hugegraph.ct.base.ClusterConstant.SERVER_PACKAGE_PATH; import static org.apache.hugegraph.ct.base.ClusterConstant.SERVER_TEMPLATE_PATH; -import static org.apache.hugegraph.ct.base.ClusterConstant.isJava11OrHigher; +import static org.apache.hugegraph.ct.base.ClusterConstant.isJava17OrHigher; +import static org.apache.hugegraph.ct.base.EnvUtil.isPortOpen; import java.io.BufferedReader; import java.io.File; @@ -46,18 +50,39 @@ import java.util.Collections; import java.util.List; +import org.apache.hugegraph.ct.config.GremlinServerConfig; + public class ServerNodeWrapper extends AbstractNodeWrapper { private static List hgJars = loadHgJarsOnce(); + private final int restPort; + private final int gremlinPort; + public ServerNodeWrapper(int clusterIndex, int index) { + this(clusterIndex, index, -1, -1); + } + + public ServerNodeWrapper(int clusterIndex, int index, int restPort) { + this(clusterIndex, index, restPort, -1); + } + + public ServerNodeWrapper(int clusterIndex, int index, int restPort, + int gremlinPort) { super(clusterIndex, index); + this.restPort = restPort; + this.gremlinPort = gremlinPort; this.fileNames = new ArrayList<>( List.of(LOG4J_FILE, GREMLIN_SERVER_FILE, GREMLIN_DRIVER_SETTING_FILE, REMOTE_SETTING_FILE, REMOTE_OBJECTS_SETTING_FILE)); this.workPath = SERVER_LIB_PATH; createNodeDir(Paths.get(SERVER_TEMPLATE_PATH), getNodePath() + CONF_DIR + File.separator); + if (this.gremlinPort >= 0) { + GremlinServerConfig.update( + Paths.get(getNodePath(), CONF_DIR, GREMLIN_SERVER_FILE), + LOCALHOST, this.gremlinPort); + } this.fileNames = new ArrayList<>(List.of(EMPTY_SAMPLE_GROOVY_FILE, EXAMPLE_GROOVY_FILE)); - this.startLine = "INFO: [HttpServer] Started."; + this.startLine = "Channel started at port "; createNodeDir(Paths.get(SERVER_PACKAGE_PATH), getNodePath()); createLogDir(); } @@ -111,8 +136,8 @@ public void start() { File stdoutFile = new File(getLogPath()); List startCmd = new ArrayList<>(); startCmd.add(JAVA_CMD); - if (!isJava11OrHigher()) { - LOG.error("Please make sure that the JDK is installed and the version >= 11"); + if (!isJava17OrHigher()) { + LOG.error("Please make sure that the JDK is installed and the version >= 17"); return; } @@ -124,9 +149,8 @@ public void start() { startCmd.addAll(Arrays.asList( "-Dname=HugeGraphServer" + this.index, - "--add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED", - "--add-modules=jdk.unsupported", - "--add-exports=java.base/sun.nio.ch=ALL-UNNAMED", + moduleOptions(Paths.get(SERVER_PACKAGE_PATH, BIN_DIR, + JVM_MODULE_OPTIONS_FILE)), "-cp", storeClassPath, "org.apache.hugegraph.dist.HugeGraphServer", "./conf/gremlin-server.yaml", @@ -138,6 +162,14 @@ public void start() { } } + @Override + public boolean isStarted() { + return super.isStarted() && + (this.restPort < 0 || isPortOpen(LOCALHOST, this.restPort)) && + (this.gremlinPort < 0 || + isPortOpen(LOCALHOST, this.gremlinPort)); + } + @Override public String getID() { return "Server" + this.index; diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/StoreNodeWrapper.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/StoreNodeWrapper.java index 1cb0f67eae..ed7225f5a6 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/StoreNodeWrapper.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct/node/StoreNodeWrapper.java @@ -17,14 +17,16 @@ package org.apache.hugegraph.ct.node; +import static org.apache.hugegraph.ct.base.ClusterConstant.BIN_DIR; import static org.apache.hugegraph.ct.base.ClusterConstant.CONF_DIR; +import static org.apache.hugegraph.ct.base.ClusterConstant.JVM_MODULE_OPTIONS_FILE; import static org.apache.hugegraph.ct.base.ClusterConstant.JAVA_CMD; import static org.apache.hugegraph.ct.base.ClusterConstant.LOG4J_FILE; import static org.apache.hugegraph.ct.base.ClusterConstant.STORE_JAR_PREFIX; import static org.apache.hugegraph.ct.base.ClusterConstant.STORE_LIB_PATH; import static org.apache.hugegraph.ct.base.ClusterConstant.STORE_TEMPLATE_PATH; import static org.apache.hugegraph.ct.base.ClusterConstant.getFileInDir; -import static org.apache.hugegraph.ct.base.ClusterConstant.isJava11OrHigher; +import static org.apache.hugegraph.ct.base.ClusterConstant.isJava17OrHigher; import java.io.File; import java.io.IOException; @@ -59,11 +61,14 @@ public void start() { File stdoutFile = new File(getLogPath()); List startCmd = new ArrayList<>(); startCmd.add(JAVA_CMD); - if (!isJava11OrHigher()) { - LOG.error("Please make sure that the JDK is installed and the version >= 11"); + if (!isJava17OrHigher()) { + LOG.error("Please make sure that the JDK is installed and the version >= 17"); return; } + startCmd.add(moduleOptions(Paths.get(workPath).getParent() + .resolve(BIN_DIR).resolve(JVM_MODULE_OPTIONS_FILE))); + String storeNodeJarPath = getFileInDir(workPath, STORE_JAR_PREFIX); startCmd.addAll(Arrays.asList( "-Dname=HugeGraphStore" + this.index, diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/LimitExceedException.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/base/EnvUtilTest.java similarity index 63% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/LimitExceedException.java rename to hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/base/EnvUtilTest.java index 1c2950f11c..00fc93bf8d 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/LimitExceedException.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/base/EnvUtilTest.java @@ -15,19 +15,23 @@ * limitations under the License. */ -package org.apache.hugegraph.exception; +package org.apache.hugegraph.ct.base; -import org.apache.hugegraph.HugeException; +import java.net.ServerSocket; -public class LimitExceedException extends HugeException { +import org.junit.Assert; +import org.junit.Test; - private static final long serialVersionUID = 7384276720045597709L; +public class EnvUtilTest { - public LimitExceedException(String message) { - super(message); - } + @Test + public void testPortOpen() throws Exception { + int port; + try (ServerSocket socket = new ServerSocket(0)) { + port = socket.getLocalPort(); + Assert.assertTrue(EnvUtil.isPortOpen("127.0.0.1", port)); + } - public LimitExceedException(String message, Object... args) { - super(message, args); + Assert.assertFalse(EnvUtil.isPortOpen("127.0.0.1", port)); } } diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/config/GremlinServerConfigTest.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/config/GremlinServerConfigTest.java new file mode 100644 index 0000000000..ef3ffd6538 --- /dev/null +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/config/GremlinServerConfigTest.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.hugegraph.ct.config; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class GremlinServerConfigTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testUpdateCommentedHostAndPort() throws Exception { + File config = temporaryFolder.newFile("gremlin-server.yaml"); + String content = "#host: 127.0.0.1\n" + + "#port: 8182\n" + + "evaluationTimeout: 30000\n"; + Files.write(config.toPath(), content.getBytes(StandardCharsets.UTF_8)); + + GremlinServerConfig.update(config.toPath(), "127.0.0.1", 12345); + + List updated = Files.readAllLines(config.toPath(), + StandardCharsets.UTF_8); + Assert.assertTrue(updated.toString(), + updated.contains("host: 127.0.0.1")); + Assert.assertTrue(updated.toString(), updated.contains("port: 12345")); + Assert.assertFalse(updated.toString(), updated.contains("port: 8182")); + } + + @Test + public void testRejectInvalidPortsWithoutChangingConfig() throws Exception { + File config = temporaryFolder.newFile("invalid-port.yaml"); + String content = "host: 127.0.0.1\nport: 8182\n"; + Files.writeString(config.toPath(), content, StandardCharsets.UTF_8); + + for (int port : new int[]{-1, 0, 65536}) { + Assert.assertThrows(IllegalArgumentException.class, () -> { + GremlinServerConfig.update(config.toPath(), "127.0.0.1", port); + }); + Assert.assertEquals(content, Files.readString(config.toPath(), + StandardCharsets.UTF_8)); + } + } + + @Test + public void testRequireOneTopLevelHostAndPort() throws Exception { + File config = temporaryFolder.newFile("ambiguous-endpoint.yaml"); + for (String content : new String[]{ + "host: 127.0.0.1\n", + "port: 8182\n", + "host: 127.0.0.1\n#host: 0.0.0.0\nport: 8182\n", + "host: 127.0.0.1\nport: 8182\n#port: 8183\n"}) { + Files.writeString(config.toPath(), content, StandardCharsets.UTF_8); + Assert.assertThrows(IllegalStateException.class, () -> { + GremlinServerConfig.update(config.toPath(), "127.0.0.1", 12345); + }); + Assert.assertEquals(content, Files.readString(config.toPath(), + StandardCharsets.UTF_8)); + } + } + +} diff --git a/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/node/AbstractNodeWrapperTest.java b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/node/AbstractNodeWrapperTest.java new file mode 100644 index 0000000000..485810112b --- /dev/null +++ b/hugegraph-cluster-test/hugegraph-clustertest-minicluster/src/test/java/org/apache/hugegraph/ct/node/AbstractNodeWrapperTest.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.hugegraph.ct.node; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class AbstractNodeWrapperTest { + + private static final String START_LINE = "node is ready"; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testStartedRequiresLiveProcess() throws Exception { + TestNodeWrapper wrapper = new TestNodeWrapper(newNodeDirectory()); + Files.write(Paths.get(wrapper.getLogPath()), + START_LINE.getBytes(StandardCharsets.UTF_8)); + + Assert.assertFalse(wrapper.isStarted()); + + Process process = startSleeperProcess(); + wrapper.attach(process); + try { + Assert.assertTrue(process.isAlive()); + Assert.assertTrue(wrapper.isStarted()); + } finally { + process.destroyForcibly(); + process.waitFor(10, TimeUnit.SECONDS); + } + } + + @Test + public void testStopDumpsLogOfUnexpectedlyExitedProcess() throws Exception { + TestNodeWrapper wrapper = new TestNodeWrapper(newNodeDirectory()); + String failure = "fatal startup failure"; + Files.write(Paths.get(wrapper.getLogPath()), + failure.getBytes(StandardCharsets.UTF_8)); + Process process = startExitedProcess(); + wrapper.attach(process); + + PrintStream originalOut = System.out; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try { + System.setOut(new PrintStream(output, true, + StandardCharsets.UTF_8.name())); + wrapper.stop(); + } finally { + System.setOut(originalOut); + } + + String diagnostics = output.toString(StandardCharsets.UTF_8.name()); + Assert.assertTrue(diagnostics, diagnostics.contains(failure)); + } + + @Test + public void testModuleOptionsRejectMissingFile() throws Exception { + TestNodeWrapper wrapper = new TestNodeWrapper(newNodeDirectory()); + Path options = temporaryFolder.newFile("jvm-module.options").toPath(); + Assert.assertEquals("@" + options.toAbsolutePath(), wrapper.moduleOptions(options)); + Files.delete(options); + Assert.assertThrows(IOException.class, () -> wrapper.moduleOptions(options)); + } + + @Test + public void testStopForwardsStartupLogBeforeCleanup() throws Exception { + File nodeDirectory = newNodeDirectory(); + TestNodeWrapper wrapper = new TestNodeWrapper(nodeDirectory); + Files.writeString(Paths.get(wrapper.getLogPath()), + "startup diagnostic\n" + "operation succeeded\n".repeat(250)); + wrapper.attach(startSleeperProcess()); + PrintStream originalOut = System.out; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try { + System.setOut(new PrintStream(output, true, StandardCharsets.UTF_8.name())); + wrapper.stop(); + } finally { + System.setOut(originalOut); + } + Assert.assertTrue(output.toString(StandardCharsets.UTF_8.name()).contains("startup diagnostic")); + Assert.assertFalse(nodeDirectory.exists()); + } + + @Test + public void testStopRetainsDirectoryWhenLogIsUnavailable() throws Exception { + for (boolean malformed : new boolean[]{false, true}) { + File nodeDirectory = newNodeDirectory(); + TestNodeWrapper wrapper = new TestNodeWrapper(nodeDirectory); + if (malformed) { + Files.write(Paths.get(wrapper.getLogPath()), new byte[]{(byte) 0xff}); + } + wrapper.attach(startSleeperProcess()); + PrintStream originalOut = System.out; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try { + System.setOut(new PrintStream(output, true, StandardCharsets.UTF_8.name())); + wrapper.stop(); + } finally { + System.setOut(originalOut); + } + Assert.assertFalse(wrapper.isAlive()); + Assert.assertTrue(nodeDirectory.exists()); + Assert.assertTrue(output.toString(StandardCharsets.UTF_8.name()) + .contains("Unable to inspect cluster runtime logs:")); + } + } + + private File newNodeDirectory() throws Exception { + File nodeDirectory = temporaryFolder.newFolder(); + File logDirectory = new File(nodeDirectory, "logs"); + Assert.assertTrue(logDirectory.mkdir()); + return nodeDirectory; + } + + private static Process startSleeperProcess() throws Exception { + String java = javaExecutable(); + String classpath = testClassesPath(); + return new ProcessBuilder(java, "-cp", classpath, + Sleeper.class.getName()).start(); + } + + private static Process startExitedProcess() throws Exception { + Process process = new ProcessBuilder(javaExecutable(), "-version").start(); + Assert.assertTrue(process.waitFor(10, TimeUnit.SECONDS)); + return process; + } + + private static String javaExecutable() { + String executable = System.getProperty("os.name").startsWith("Windows") ? + "java.exe" : "java"; + return Paths.get(System.getProperty("java.home"), "bin", executable) + .toString(); + } + + private static String testClassesPath() throws URISyntaxException { + return Paths.get(AbstractNodeWrapperTest.class.getProtectionDomain() + .getCodeSource() + .getLocation() + .toURI()) + .toString(); + } + + public static class Sleeper { + + public static void main(String[] args) throws Exception { + TimeUnit.SECONDS.sleep(30); + } + } + + private static class TestNodeWrapper extends AbstractNodeWrapper { + + private File nodeDirectory; + + private TestNodeWrapper(File nodeDirectory) { + this.nodeDirectory = nodeDirectory; + this.startLine = START_LINE; + } + + private void attach(Process process) { + this.instance = process; + } + + @Override + public void start() { + throw new UnsupportedOperationException(); + } + + @Override + public String getID() { + return "TestNode"; + } + + @Override + public String getNodePath() { + if (this.nodeDirectory == null) { + return System.getProperty("java.io.tmpdir") + File.separator; + } + return this.nodeDirectory.getAbsolutePath() + File.separator; + } + } +} diff --git a/hugegraph-cluster-test/hugegraph-clustertest-test/pom.xml b/hugegraph-cluster-test/hugegraph-clustertest-test/pom.xml index 735ea66b43..1ab0fd3b7f 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-test/pom.xml +++ b/hugegraph-cluster-test/hugegraph-clustertest-test/pom.xml @@ -29,8 +29,6 @@ hugegraph-clustertest-test - 11 - 11 UTF-8 @@ -40,6 +38,11 @@ ${revision} compile + + + org.apache.hugegraph + hugegraph-common + org.apache.hugegraph @@ -81,7 +84,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 simple-cluster-test diff --git a/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/MultiClusterTest/BaseMultiClusterTest.java b/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/MultiClusterTest/BaseMultiClusterTest.java index 9e90933026..0f2b0d492a 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/MultiClusterTest/BaseMultiClusterTest.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/MultiClusterTest/BaseMultiClusterTest.java @@ -38,7 +38,7 @@ * MultiNode Test generate the cluster env with 3 pd node + 3 store node + 3 server node. * Or you can set different num of nodes by using env = new MultiNodeEnv(pdNum, storeNum, serverNum) * All nodes are deployed in ports generated randomly, the application of nodes are stored - * in /apache-hugegraph-ct-1.7.0, you can visit each node with rest api. + * in /apache-hugegraph-ct-1.8.0, you can visit each node with rest api. */ public class BaseMultiClusterTest { @@ -115,8 +115,21 @@ protected static String assertResponseStatus(int status, public static Response createAndAssert(RestClient client, String path, String body, int status) { - Response r = client.post(path, body); - assertResponseStatus(status, r); - return r; + try { + Response r = client.post(path, body); + assertResponseStatus(status, r); + return r; + } catch (RuntimeException | AssertionError e) { + System.out.printf("[cluster-test] POST %s%s failed: %s%n", + client.target().getUri(), path, e.getMessage()); + try { + env.dumpClusterStatus(); + } catch (Throwable diagnosticError) { + if (diagnosticError != e) { + e.addSuppressed(diagnosticError); + } + } + throw e; + } } } diff --git a/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/SimpleClusterTest/BaseSimpleTest.java b/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/SimpleClusterTest/BaseSimpleTest.java index f0f0c33461..996856f86c 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/SimpleClusterTest/BaseSimpleTest.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/SimpleClusterTest/BaseSimpleTest.java @@ -45,7 +45,7 @@ /** * Simple Test generate the cluster env with 1 pd node + 1 store node + 1 server node. * All nodes are deployed in ports generated randomly; The application of nodes is stored - * in /apache-hugegraph-ct-1.7.0, you can visit each node with rest api. + * in /apache-hugegraph-ct-1.8.0, you can visit each node with rest api. */ public class BaseSimpleTest { @@ -74,7 +74,10 @@ public static void initEnv() { public static void clearEnv() throws InterruptedException { env.stopCluster(); Thread.sleep(2000); - client.close(); + if (client != null) { + client.close(); + client = null; + } } protected String execCmd(String[] cmds) throws IOException { diff --git a/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/SimpleClusterTest/SimpleClusterGraphTest.java b/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/SimpleClusterTest/SimpleClusterGraphTest.java new file mode 100644 index 0000000000..97ac4ed4da --- /dev/null +++ b/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/SimpleClusterTest/SimpleClusterGraphTest.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.hugegraph.SimpleClusterTest; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; + +import jakarta.ws.rs.core.Response; + +/** + * Writes graph data through the server and reads it back, so the request path + * crosses Server, the HStore backend and the Store node of the simple cluster. + */ +public class SimpleClusterGraphTest extends BaseSimpleTest { + + private static final String SCHEMA = URL_PREFIX + "/schema"; + private static final String VERTICES = URL_PREFIX + "/graph/vertices"; + private static final String EDGES = URL_PREFIX + "/graph/edges"; + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @BeforeClass + public static void initSchema() { + createPropertyKey("ct_name", "TEXT"); + createPropertyKey("ct_age", "INT"); + createPropertyKey("ct_weight", "DOUBLE"); + createAndAssert(SCHEMA + "/vertexlabels", "{" + + "\"name\": \"ct_person\"," + + "\"id_strategy\": \"PRIMARY_KEY\"," + + "\"properties\": [\"ct_name\", \"ct_age\"]," + + "\"primary_keys\": [\"ct_name\"]," + + "\"check_exist\": false" + + "}", 201); + createAndAssert(SCHEMA + "/edgelabels", "{" + + "\"name\": \"ct_knows\"," + + "\"source_label\": \"ct_person\"," + + "\"target_label\": \"ct_person\"," + + "\"properties\": [\"ct_weight\"]," + + "\"check_exist\": false" + + "}", 201); + } + + @Test + public void testVertexAndEdgeRoundTripThroughStore() throws IOException { + String alice = createPerson("ct-alice", 30); + String bob = createPerson("ct-bob", 31); + + Map vertex = read(assertResponseStatus(200, client.get(VERTICES, json(alice)))); + Assert.assertEquals(alice, vertex.get("id")); + Assert.assertEquals("ct_person", vertex.get("label")); + Map props = (Map) vertex.get("properties"); + Assert.assertEquals("ct-alice", props.get("ct_name")); + Assert.assertEquals(30, props.get("ct_age")); + + Response r = client.post(EDGES, "{" + + "\"label\": \"ct_knows\"," + + "\"outV\": " + json(alice) + "," + + "\"inV\": " + json(bob) + "," + + "\"outVLabel\": \"ct_person\"," + + "\"inVLabel\": \"ct_person\"," + + "\"properties\": {\"ct_weight\": 0.5}" + + "}"); + String edgeId = (String) read(assertResponseStatus(201, r)).get("id"); + + Map edge = read(assertResponseStatus(200, client.get(EDGES, edgeId))); + Assert.assertEquals(alice, edge.get("outV")); + Assert.assertEquals(bob, edge.get("inV")); + Assert.assertEquals(0.5, ((Map) edge.get("properties")).get("ct_weight")); + + // Adjacency is served from the Store side, so query it by direction + List out = edges(alice, "OUT"); + Assert.assertEquals(1, out.size()); + Assert.assertEquals(edgeId, ((Map) out.get(0)).get("id")); + Assert.assertEquals(1, edges(bob, "IN").size()); + Assert.assertEquals(0, edges(bob, "OUT").size()); + } + + @Test + public void testInvalidGraphRequestsAreRejected() { + Response r = client.post(VERTICES, "{" + + "\"label\": \"ct_undefined_label\"," + + "\"properties\": {\"ct_name\": \"x\"}" + + "}"); + assertResponseStatus(400, r); + + r = client.post(VERTICES, "{" + + "\"label\": \"ct_person\"," + + "\"properties\": {\"ct_name\": \"x\", \"ct_age\": \"old\"}" + + "}"); + assertResponseStatus(400, r); + + // A primary key vertex can't be created without its primary key value + r = client.post(VERTICES, "{" + + "\"label\": \"ct_person\"," + + "\"properties\": {\"ct_age\": 1}" + + "}"); + assertResponseStatus(400, r); + + r = client.get(VERTICES, json("ct-person-never-written")); + assertResponseStatus(404, r); + } + + private static void createPropertyKey(String name, String dataType) { + createAndAssert(SCHEMA + "/propertykeys", "{" + + "\"name\": \"" + name + "\"," + + "\"data_type\": \"" + dataType + "\"," + + "\"cardinality\": \"SINGLE\"," + + "\"check_exist\": false," + + "\"properties\": []" + + "}", 202); + } + + private static String createPerson(String name, int age) throws IOException { + Response r = client.post(VERTICES, "{" + + "\"label\": \"ct_person\"," + + "\"properties\": {" + + "\"ct_name\": \"" + name + "\"," + + "\"ct_age\": " + age + + "}}"); + return (String) read(assertResponseStatus(201, r)).get("id"); + } + + private static List edges(String vertexId, String direction) { + Response r = client.get(EDGES, ImmutableMap.of("vertex_id", json(vertexId), + "direction", direction)); + try { + return (List) read(assertResponseStatus(200, r)).get("edges"); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + private static Map read(String content) throws IOException { + return MAPPER.readValue(content, Map.class); + } + + private static String json(String id) { + return "\"" + id + "\""; + } +} diff --git a/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/SimpleClusterTest/SimpleClusterSuiteTest.java b/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/SimpleClusterTest/SimpleClusterSuiteTest.java index 7f24d8b46c..cc007a0802 100644 --- a/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/SimpleClusterTest/SimpleClusterSuiteTest.java +++ b/hugegraph-cluster-test/hugegraph-clustertest-test/src/main/java/org/apache/hugegraph/SimpleClusterTest/SimpleClusterSuiteTest.java @@ -26,6 +26,7 @@ @Suite.SuiteClasses({ SimpleClusterDeployTest.class, SimpleClusterFileTest.class, + SimpleClusterGraphTest.class, }) @Slf4j public class SimpleClusterSuiteTest { diff --git a/hugegraph-cluster-test/pom.xml b/hugegraph-cluster-test/pom.xml index cd54ac0ffe..cf51dfd1f3 100644 --- a/hugegraph-cluster-test/pom.xml +++ b/hugegraph-cluster-test/pom.xml @@ -39,12 +39,29 @@ - 11 - 11 UTF-8 apache-${release.name}-ct-${project.version} + + + + + org.apache.hugegraph + hugegraph-common + ${revision} + + + + org.apache.commons + commons-text + 1.10.0 + + + + com.google.code.findbugs @@ -96,7 +113,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 simple-cluster-test @@ -120,7 +136,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 multi-cluster-test diff --git a/hugegraph-commons/AGENTS.md b/hugegraph-commons/AGENTS.md index 179e01300c..58f4512c35 100644 --- a/hugegraph-commons/AGENTS.md +++ b/hugegraph-commons/AGENTS.md @@ -6,8 +6,8 @@ Shared rules: [root AGENTS.md](../AGENTS.md). Paths here are relative to this mo - Config, locks, events, iterators and REST: `hugegraph-common/src/main/java/org/apache/hugegraph/`. - RPC framework: `hugegraph-rpc/`, which depends on `hugegraph-common`. -- Commons targets Java 8 bytecode in [pom.xml](pom.xml), although CI uses JDK 11. - Do not infer its language/API baseline from Server's JDK requirement. +- Commons and RPC use Java 11 bytecode while building and testing on JDK 17; + see [pom.xml](pom.xml) and the [root POM](../pom.xml). ## Tests diff --git a/hugegraph-commons/README.md b/hugegraph-commons/README.md index d8cbcbc24a..71f6dea421 100644 --- a/hugegraph-commons/README.md +++ b/hugegraph-commons/README.md @@ -21,6 +21,10 @@ numeric or collection util classes to simplify the development of HugeGraph and - Util: performance analyzer, version checker, numeric and Collection utils, log and exception utils, etc. - Rpc: rpc component for inner module communication, currently it's based on [Sofa-RPC](https://github.com/sofastack/sofa-rpc) +The REST client serializes object bodies as JSON for `application/json`, including media types +with parameters such as `application/json; charset=UTF-16`. Request bodies use the declared +charset, or UTF-8 when none is specified; gzip preserves these encoded bytes. + You could use import the dependencies in `maven` like this: ```xml diff --git a/hugegraph-commons/hugegraph-common/README.md b/hugegraph-commons/hugegraph-common/README.md index bfeef03cd2..df04380dba 100644 --- a/hugegraph-commons/hugegraph-common/README.md +++ b/hugegraph-commons/hugegraph-common/README.md @@ -5,11 +5,26 @@ [![codecov](https://codecov.io/gh/hugegraph/hugegraph-common/branch/master/graph/badge.svg)](https://codecov.io/gh/hugegraph/hugegraph-common) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.apache.hugegraph/hugegraph-common/badge.svg)](https://mvnrepository.com/artifact/org.apache.hugegraph/hugegraph-common) -hugegraph-common is a common module for [HugeGraph](https://github.com/hugegraph/hugegraph) and its peripheral components. +hugegraph-common is a common module for [HugeGraph](https://github.com/apache/hugegraph) and its peripheral components. hugegraph-common encapsulates locks, configurations, events, iterators, rest and some numeric or collection util classes to simplify the development of HugeGraph and its components. +## REST request encoding + +The shared REST client encodes request bodies using the charset declared in +`Content-Type`, or UTF-8 when no charset is declared. JSON and text requests do +not depend on the JVM's default charset. Gzip compresses those same encoded bytes. + +## HTTPS certificate verification + +A custom trust store configures which certificate authorities or certificates the +REST client trusts. It does not disable server identity verification: the +certificate must also contain a Subject Alternative Name matching the requested +DNS name or IP address. Certificates with a different identity or no matching +Subject Alternative Name are rejected, including certificates explicitly trusted +by the configured store. This uses OkHttp's default hostname verifier. + ## Components - Lock: atomic lock, key lock, lock group and lock manager diff --git a/hugegraph-commons/hugegraph-common/build.sh b/hugegraph-commons/hugegraph-common/build.sh index b2cb6211b9..2e4b0bbc20 100644 --- a/hugegraph-commons/hugegraph-common/build.sh +++ b/hugegraph-commons/hugegraph-common/build.sh @@ -16,8 +16,5 @@ # limitations under the License. # -export MAVEN_HOME=/home/scmtools/buildkit/maven/apache-maven-3.3.9/ -export JAVA_HOME=/home/scmtools/buildkit/java/jdk1.8.0_25/ -export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH - -mvn clean test -Dtest=UnitTestSuite +mvn clean test -Dtest=UnitTestSuite \ + -Dsurefire.failIfNoSpecifiedTests=true -DskipCommonsTests=false diff --git a/hugegraph-commons/hugegraph-common/pom.xml b/hugegraph-commons/hugegraph-common/pom.xml index 14f7cc217c..25c10e139a 100644 --- a/hugegraph-commons/hugegraph-common/pom.xml +++ b/hugegraph-commons/hugegraph-common/pom.xml @@ -39,6 +39,7 @@ 4.10.0 + 0.11.5 @@ -72,6 +73,28 @@ ${log4j2.version} + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + true + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + true + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + true + + org.glassfish @@ -164,22 +187,18 @@ com.fasterxml.jackson.core jackson-annotations - ${jackson.version} com.fasterxml.jackson.core jackson-core - ${jackson.version} com.fasterxml.jackson.core jackson-databind - ${jackson.version} com.fasterxml.jackson.module jackson-module-jaxb-annotations - ${jackson.version} jakarta.xml.bind-api @@ -190,12 +209,10 @@ com.fasterxml.jackson.jaxrs jackson-jaxrs-base - ${jackson.version} com.fasterxml.jackson.jaxrs jackson-jaxrs-json-provider - ${jackson.version} @@ -257,7 +274,6 @@ org.jacoco jacoco-maven-plugin - 0.8.2 pre-unit-test diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthConstant.java b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/auth/AuthConstant.java similarity index 88% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthConstant.java rename to hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/auth/AuthConstant.java index 3c7c539363..089fb8aa59 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthConstant.java +++ b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/auth/AuthConstant.java @@ -24,4 +24,6 @@ public interface AuthConstant { */ String TOKEN_USER_NAME = "user_name"; String TOKEN_USER_ID = "user_id"; + // Retained for callers using the former struct constants; not emitted by login. + String TOKEN_USER_PASSWORD = "user_password"; } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/TokenGenerator.java b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/auth/TokenGenerator.java similarity index 64% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/TokenGenerator.java rename to hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/auth/TokenGenerator.java index 7c75c095b9..83fecb47e7 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/TokenGenerator.java +++ b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/auth/TokenGenerator.java @@ -23,27 +23,16 @@ import javax.crypto.SecretKey; -import org.apache.hugegraph.config.AuthOptions; -import org.apache.hugegraph.config.HugeConfig; - import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; -import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; -import jakarta.ws.rs.NotAuthorizedException; public class TokenGenerator { private final SecretKey key; - public TokenGenerator(HugeConfig config) { - String secretKey = config.get(AuthOptions.AUTH_TOKEN_SECRET); - this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); - } - public TokenGenerator(String secretKey) { this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); } @@ -56,17 +45,15 @@ public String create(Map payload, long expire) { .compact(); } + /** + * Verify a signed token. Callers translate JJWT failures into their own + * authentication or transport errors. + */ public Claims verify(String token) { - try { - Jws claimsJws = Jwts.parserBuilder() - .setSigningKey(key) - .build() - .parseClaimsJws(token); - return claimsJws.getBody(); - } catch (ExpiredJwtException e) { - throw new NotAuthorizedException("The token is expired", e); - } catch (JwtException e) { - throw new NotAuthorizedException("Invalid token", e); - } + Jws claimsJws = Jwts.parserBuilder() + .setSigningKey(this.key) + .build() + .parseClaimsJws(token); + return claimsJws.getBody(); } } diff --git a/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/license/MachineInfo.java b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/license/MachineInfo.java index cd106921ea..1b2f4a3271 100755 --- a/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/license/MachineInfo.java +++ b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/license/MachineInfo.java @@ -57,20 +57,19 @@ public List getMacAddress() { if (this.macAddressList != null) { return this.macAddressList; } - this.macAddressList = new ArrayList<>(); + List list = new ArrayList<>(); List inetAddresses = this.getLocalAllInetAddress(); if (inetAddresses != null && !inetAddresses.isEmpty()) { // Get the Mac address of all network interfaces - List list = new ArrayList<>(); Set uniqueValues = new HashSet<>(); for (InetAddress inetAddress : inetAddresses) { - String macByInetAddress = this.getMacByInetAddress(inetAddress); - if (uniqueValues.add(macByInetAddress)) { + String macByInetAddress = this.getMacByInetAddress(inetAddress, false); + if (macByInetAddress != null && uniqueValues.add(macByInetAddress)) { list.add(macByInetAddress); } } - this.macAddressList = list; } + this.macAddressList = list; return this.macAddressList; } @@ -99,6 +98,10 @@ public List getLocalAllInetAddress() { } public String getMacByInetAddress(InetAddress inetAddr) { + return this.getMacByInetAddress(inetAddr, true); + } + + private String getMacByInetAddress(InetAddress inetAddr, boolean required) { byte[] mac; try { mac = NetworkInterface.getByInetAddress(inetAddr) @@ -108,6 +111,12 @@ public String getMacByInetAddress(InetAddress inetAddr) { "Failed to get mac address for inet address '%s'", inetAddr)); } + if (mac == null) { + if (required) { + throw new IllegalStateException("Network interface has no hardware address"); + } + return null; + } StringBuilder sb = new StringBuilder(); for (int i = 0; i < mac.length; i++) { diff --git a/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/rest/AbstractRestClient.java b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/rest/AbstractRestClient.java index c13a686b71..a38dcb69a6 100644 --- a/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/rest/AbstractRestClient.java +++ b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/rest/AbstractRestClient.java @@ -20,7 +20,6 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.KeyStore; @@ -31,7 +30,6 @@ import java.util.concurrent.TimeUnit; import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; @@ -125,9 +123,10 @@ public AbstractRestClient(String url, RestClientConfig config) { } private static RequestBody buildRequestBody(Object body, RestHeaders headers) { - String contentType = parseContentType(headers); + MediaType contentType = MediaType.parse(parseContentType(headers)); String bodyContent; - if (RestHeaders.APPLICATION_JSON.equals(contentType)) { + if (contentType != null && "application".equals(contentType.type()) && + "json".equals(contentType.subtype())) { if (body == null) { bodyContent = "{}"; } else if (body instanceof String) { @@ -138,8 +137,7 @@ private static RequestBody buildRequestBody(Object body, RestHeaders headers) { } else { bodyContent = String.valueOf(body); } - RequestBody requestBody = RequestBody.create(bodyContent.getBytes(), - MediaType.parse(contentType)); + RequestBody requestBody = RequestBody.create(bodyContent, contentType); if (headers != null && "gzip".equals(headers.get(RestHeaders.CONTENT_ENCODING))) { @@ -211,7 +209,7 @@ private OkHttpClient buildOkHttpClient(RestClientConfig config) { } // ssl - configSsl(builder, this.baseUrl, config.getTrustStoreFile(), + configSsl(builder, config.getTrustStoreFile(), config.getTrustStorePassword()); // Execute builder callback before builder.build() for user configs @@ -233,7 +231,7 @@ private OkHttpClient buildOkHttpClient(RestClientConfig config) { } @SneakyThrows - private void configSsl(OkHttpClient.Builder builder, String url, String trustStoreFile, + private void configSsl(OkHttpClient.Builder builder, String trustStoreFile, String trustStorePass) { if (StringUtils.isBlank(trustStoreFile) || StringUtils.isBlank(trustStorePass)) { return; @@ -244,8 +242,7 @@ private void configSsl(OkHttpClient.Builder builder, String url, String trustSto sslContext.init(null, new TrustManager[]{trustManager}, null); SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); - builder.sslSocketFactory(sslSocketFactory, trustManager) - .hostnameVerifier(new HostNameVerifier(url)); + builder.sslSocketFactory(sslSocketFactory, trustManager); } @Override @@ -465,26 +462,20 @@ private X509TrustManager trustManagerForCertificates(String trustStoreFile, return (X509TrustManager) trustManagers[0]; } + /** + * Compatibility wrapper using the same certificate identity verification as OkHttp. + */ public static class HostNameVerifier implements HostnameVerifier { - private final String url; + private static final HostnameVerifier DEFAULT_VERIFIER = new OkHttpClient().hostnameVerifier(); public HostNameVerifier(String url) { - if (!url.startsWith("http://") && !url.startsWith("https://")) { - url = "http://" + url; - } - url = URI.create(url).getHost(); - this.url = url; + // Keep the public constructor; a URL is not evidence of the peer's identity. } @Override public boolean verify(String hostname, SSLSession session) { - if (!this.url.isEmpty() && this.url.endsWith(hostname)) { - return true; - } else { - HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier(); - return verifier.verify(hostname, session); - } + return DEFAULT_VERIFIER.verify(hostname, session); } } } diff --git a/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Client.java b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Client.java similarity index 86% rename from hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Client.java rename to hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Client.java index b08d6327d1..e0557ebc21 100644 --- a/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Client.java +++ b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Client.java @@ -32,4 +32,8 @@ default T serviceProxy(String graph, Class clazz) { } void removeAllServiceProxy(); + + default boolean removeServiceProxy(String graph, String interfaceId, Object proxy) { + throw new UnsupportedOperationException("Removing an individual RPC proxy is not supported"); + } } diff --git a/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Server.java b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Server.java similarity index 85% rename from hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Server.java rename to hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Server.java index cd0290900e..0c9eab15a8 100644 --- a/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Server.java +++ b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Server.java @@ -25,5 +25,9 @@ public interface RpcServiceConfig4Server { void removeService(String serviceId); + default boolean removeService(String serviceId, Object serviceImpl) { + throw new UnsupportedOperationException("Removing an RPC service by implementation is not supported"); + } + void removeAllService(); } diff --git a/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/util/RocksDBRuntime.java b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/util/RocksDBRuntime.java new file mode 100644 index 0000000000..d76f1aa84e --- /dev/null +++ b/hugegraph-commons/hugegraph-common/src/main/java/org/apache/hugegraph/util/RocksDBRuntime.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.hugegraph.util; + +import java.net.URL; +import java.security.CodeSource; + +/** Match configuration to the JNI Java classes selected by the launcher. */ +public final class RocksDBRuntime { + + private RocksDBRuntime() { + } + + private static URL origin(Class runtime) { + CodeSource source = runtime.getProtectionDomain().getCodeSource(); + if (source == null || source.getLocation() == null) { + throw new IllegalStateException("Cannot verify RocksDB runtime origin for " + runtime.getName() + + "; use a class loader with a CodeSource location"); + } + return source.getLocation(); + } + + public static void verify(String configuredProvider) { + if (!"rocksdb".equals(configuredProvider) && !"topling".equals(configuredProvider)) { + throw new IllegalArgumentException("Invalid rocksdb.provider: " + configuredProvider); + } + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + try { + Class rocks; + try { + rocks = Class.forName("org.rocksdb.RocksDB", false, loader); + } catch (ClassNotFoundException contextLoaderMiss) { + loader = RocksDBRuntime.class.getClassLoader(); + rocks = Class.forName("org.rocksdb.RocksDB", false, loader); + } + URL origin = origin(rocks); + boolean topling = false; + try { + Class marker = Class.forName("org.rocksdb.SidePluginRepo", false, loader); + topling = origin.equals(origin(marker)); + } catch (ClassNotFoundException standardRuntime) { + // Standard RocksDB has no Topling marker class. + } + if (topling != "topling".equals(configuredProvider)) { + throw new IllegalStateException("Configured rocksdb.provider=" + configuredProvider + + " conflicts with selected RocksDB Java runtime: " + origin + + "; match the launcher opt-in and business configuration"); + } + } catch (ClassNotFoundException e) { + throw new IllegalStateException("RocksDB Java runtime is missing", e); + } + } +} diff --git a/hugegraph-commons/hugegraph-common/src/main/resources/version.properties b/hugegraph-commons/hugegraph-common/src/main/resources/version.properties index 8d48ef39ca..c62e18ef71 100644 --- a/hugegraph-commons/hugegraph-common/src/main/resources/version.properties +++ b/hugegraph-commons/hugegraph-common/src/main/resources/version.properties @@ -20,4 +20,4 @@ Version=${revision} ApiVersion=0.72 ApiCheckBeginVersion=1.0 ApiCheckEndVersion=2.0 -VersionInBash=1.7.0 +VersionInBash=1.8.0 diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/testutil/AssertTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/testutil/AssertTest.java index 53f60247e9..21f935aa0a 100644 --- a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/testutil/AssertTest.java +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/testutil/AssertTest.java @@ -422,8 +422,6 @@ public void testAssertContains() { Assert.assertThrows(NullPointerException.class, () -> { Assert.assertContains(null, "null"); - }, e -> { - Assert.assertNull(e.getMessage()); }); } diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java index 4f5fd0728f..f876e9e0c7 100644 --- a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -19,11 +19,15 @@ import org.apache.hugegraph.testutil.AssertTest; import org.apache.hugegraph.testutil.WhiteboxTest; +import org.apache.hugegraph.unit.auth.TokenGeneratorTest; import org.apache.hugegraph.unit.config.HugeConfigTest; +import org.apache.hugegraph.unit.config.HugeConfigValidationTest; import org.apache.hugegraph.unit.config.OptionSpaceTest; +import org.apache.hugegraph.unit.config.TypedOptionTest; import org.apache.hugegraph.unit.event.EventHubTest; import org.apache.hugegraph.unit.rest.AbstractRestClientTest; import org.apache.hugegraph.unit.version.VersionTest; +import org.apache.hugegraph.unit.util.RocksDBRuntimeTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -50,6 +54,7 @@ import org.apache.hugegraph.unit.license.MachineInfoTest; import org.apache.hugegraph.unit.perf.PerfUtilTest; import org.apache.hugegraph.unit.perf.StopwatchTest; +import org.apache.hugegraph.unit.rest.HostNameVerifierTest; import org.apache.hugegraph.unit.rest.RestClientTest; import org.apache.hugegraph.unit.rest.RestResultTest; import org.apache.hugegraph.unit.util.BytesTest; @@ -71,6 +76,7 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ + RocksDBRuntimeTest.class, LockManagerTest.class, LockGroupTest.class, AtomicLockTest.class, @@ -78,8 +84,11 @@ RowLockTest.class, PausableScheduledThreadPoolTest.class, + TokenGeneratorTest.class, HugeConfigTest.class, + HugeConfigValidationTest.class, OptionSpaceTest.class, + TypedOptionTest.class, SafeDateFormatTest.class, BarrierEventTest.class, EventHubTest.class, @@ -87,6 +96,7 @@ StopwatchTest.class, AbstractRestClientTest.class, RestClientTest.class, + HostNameVerifierTest.class, RestResultTest.class, VersionTest.class, diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/auth/TokenGeneratorTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/auth/TokenGeneratorTest.java new file mode 100644 index 0000000000..a047e00e68 --- /dev/null +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/auth/TokenGeneratorTest.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.hugegraph.unit.auth; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.hugegraph.auth.AuthConstant; +import org.apache.hugegraph.auth.TokenGenerator; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Test; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.security.WeakKeyException; + +public class TokenGeneratorTest { + + private static final String SECRET = "01234567890123456789012345678901"; + + @Test + public void testCreateAndVerifyClaims() { + TokenGenerator generator = new TokenGenerator(SECRET); + Map payload = new HashMap<>(); + payload.put(AuthConstant.TOKEN_USER_NAME, "alice"); + payload.put(AuthConstant.TOKEN_USER_ID, "1234"); + long before = System.currentTimeMillis(); + Claims claims = generator.verify(generator.create(payload, 60000L)); + long after = System.currentTimeMillis(); + + Assert.assertEquals("alice", claims.get(AuthConstant.TOKEN_USER_NAME)); + Assert.assertEquals("1234", claims.get(AuthConstant.TOKEN_USER_ID)); + Assert.assertFalse(claims.containsKey(AuthConstant.TOKEN_USER_PASSWORD)); + // JWT timestamps have second precision. + Assert.assertTrue(claims.getExpiration().getTime() >= before + 59000L); + Assert.assertTrue(claims.getExpiration().getTime() <= after + 60000L); + } + + @Test + public void testReadLegacyHs256Token() { + // Fixed claims and independent HMAC-SHA256 signature, not this class's writer. + String token = "eyJhbGciOiJIUzI1NiJ9." + + "eyJ1c2VyX25hbWUiOiJsZWdhY3ktdXNlciIsInVzZXJfaWQiOiIxMjM0IiwiZXhwIjo0MTAyNDQ0ODAwfQ." + + "t2b5xSruyIrZ8c45HOe6OhJt390rm_fdbvft1JLyRTs"; + Claims claims = new TokenGenerator(SECRET).verify(token); + Assert.assertEquals("legacy-user", claims.get(AuthConstant.TOKEN_USER_NAME)); + Assert.assertEquals("1234", claims.get(AuthConstant.TOKEN_USER_ID)); + Assert.assertEquals(4102444800000L, claims.getExpiration().getTime()); + } + + @Test + public void testRejectExpiredToken() { + TokenGenerator generator = new TokenGenerator(SECRET); + String token = generator.create(Collections.singletonMap("user_name", "alice"), + -60000L); + Assert.assertThrows(ExpiredJwtException.class, () -> generator.verify(token)); + } + + @Test + public void testRejectWrongKeyAndMalformedTokens() { + TokenGenerator generator = new TokenGenerator(SECRET); + String token = generator.create(Collections.singletonMap("user_name", "alice"), + 60000L); + TokenGenerator other = new TokenGenerator("abcdefghijklmnopqrstuvwxyz012345"); + Assert.assertThrows(JwtException.class, () -> other.verify(token)); + Assert.assertThrows(JwtException.class, () -> generator.verify("not-a-token")); + Assert.assertThrows(IllegalArgumentException.class, () -> generator.verify("")); + } + + @Test + public void testRejectUnsignedToken() { + TokenGenerator generator = new TokenGenerator(SECRET); + String token = generator.create(Collections.singletonMap("user_name", "alice"), + 60000L); + String unsigned = "eyJhbGciOiJub25lIn0." + token.split("\\.")[1] + "."; + Assert.assertThrows(JwtException.class, () -> generator.verify(unsigned)); + } + + @Test + public void testRejectWeakKey() { + Assert.assertThrows(WeakKeyException.class, () -> new TokenGenerator("short-key")); + } +} diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/config/HugeConfigValidationTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/config/HugeConfigValidationTest.java new file mode 100644 index 0000000000..a9e4044f84 --- /dev/null +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/config/HugeConfigValidationTest.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.hugegraph.unit.config; + +import static org.apache.hugegraph.config.OptionChecker.disallowEmpty; +import static org.apache.hugegraph.config.OptionChecker.rangeInt; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.configuration2.Configuration; +import org.apache.hugegraph.config.ConfigException; +import org.apache.hugegraph.config.ConfigListOption; +import org.apache.hugegraph.config.ConfigOption; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.OptionHolder; +import org.apache.hugegraph.config.OptionSpace; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.unit.BaseUnitTest; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +public class HugeConfigValidationTest extends BaseUnitTest { + + @BeforeClass + public static void init() { + OptionSpace.register("validation-test", ValidationOptions.class.getName()); + } + + @Test + public void testUrlOptionAddsMissingScheme() { + HugeConfig config = configOf(ValidationOptions.httpUrl.name(), "127.0.0.1:8080"); + Assert.assertEquals("http://127.0.0.1:8080", config.get(ValidationOptions.httpUrl)); + + // Surrounding whitespace is trimmed before the scheme is prefixed + config = configOf(ValidationOptions.httpUrl.name(), " graph.local:8080/api "); + Assert.assertEquals("http://graph.local:8080/api", config.get(ValidationOptions.httpUrl)); + + // A configured scheme which already ends with "://" isn't doubled + config = configOf(ValidationOptions.httpsUrl.name(), "graph.local"); + Assert.assertEquals("https://graph.local", config.get(ValidationOptions.httpsUrl)); + } + + @Test + public void testUrlOptionKeepsExistingScheme() { + HugeConfig config = configOf(ValidationOptions.httpUrl.name(), "https://graph.local:8443"); + Assert.assertEquals("https://graph.local:8443", config.get(ValidationOptions.httpUrl)); + + // Only the scheme is lower-cased, host and path keep their case + config = configOf(ValidationOptions.httpUrl.name(), "HTTPS://Graph.Local/Path"); + Assert.assertEquals("https://Graph.Local/Path", config.get(ValidationOptions.httpUrl)); + } + + @Test + public void testUrlNormalizationSkipsDefaultAndPlainOptions() { + HugeConfig config = configOf(ValidationOptions.plainUrl.name(), "127.0.0.1:8080"); + Assert.assertEquals("127.0.0.1:8080", config.get(ValidationOptions.plainUrl)); + // Default values are returned as declared and never rewritten + Assert.assertEquals("localhost:8080", config.get(ValidationOptions.httpUrl)); + Assert.assertTrue(ValidationOptions.httpUrl.needsUrlNormalization()); + Assert.assertEquals("http", ValidationOptions.httpUrl.getDefaultScheme()); + Assert.assertFalse(ValidationOptions.plainUrl.needsUrlNormalization()); + Assert.assertNull(ValidationOptions.plainUrl.getDefaultScheme()); + } + + @Test + public void testConstructWithInvalidSource() { + Assert.assertThrows(ConfigException.class, () -> { + new HugeConfig((Map) null); + }, e -> { + Assert.assertContains("The property map is null", e.getMessage()); + }); + Assert.assertThrows(ConfigException.class, () -> { + new HugeConfig((Configuration) null); + }, e -> { + Assert.assertContains("The config object is null", e.getMessage()); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + new HugeConfig(""); + }, e -> { + Assert.assertContains("The config path can't be empty", e.getMessage()); + }); + Assert.assertThrows(IllegalArgumentException.class, () -> { + new HugeConfig("target/not-exist-" + System.nanoTime() + ".properties"); + }, e -> { + Assert.assertContains("Please specify a proper config file", e.getMessage()); + }); + // A directory is not a config file either + Assert.assertThrows(IllegalArgumentException.class, () -> { + new HugeConfig("src/test"); + }); + } + + @Test + public void testStringValuesAreParsedAndChecked() { + HugeConfig config = configOf(ValidationOptions.port.name(), "8182"); + Assert.assertEquals(8182, config.get(ValidationOptions.port)); + + Assert.assertThrows(ConfigException.class, () -> { + configOf(ValidationOptions.port.name(), "70000"); + }, e -> { + Assert.assertContains("Invalid option value for 'validation.port': 70000", + e.getMessage()); + }); + Assert.assertThrows(ConfigException.class, () -> { + configOf(ValidationOptions.port.name(), "http"); + }, e -> { + Assert.assertContains("expect 'Integer' type", e.getMessage()); + }); + Assert.assertThrows(ConfigException.class, () -> { + configOf(ValidationOptions.name.name(), " "); + }); + } + + @Test + public void testNonStringValueWithWrongType() { + Map options = new HashMap<>(); + options.put(ValidationOptions.port.name(), 8182L); + Assert.assertThrows(IllegalArgumentException.class, () -> { + new HugeConfig(options); + }, e -> { + Assert.assertContains("Invalid value for key 'validation.port': '8182'", + e.getMessage()); + }); + + options.put(ValidationOptions.port.name(), 8182); + Assert.assertEquals(8182, new HugeConfig(options).get(ValidationOptions.port)); + } + + @Test + public void testUnregisteredOptionIsKeptAsRawValue() { + HugeConfig config = configOf("validation.unregistered", "raw-value"); + Assert.assertEquals("raw-value", config.getProperty("validation.unregistered")); + Assert.assertEquals("default-name", config.get(ValidationOptions.name)); + } + + @Test + public void testGetMapWithInvalidEntry() { + HugeConfig config = configOf(ValidationOptions.pairs.name(), "[k1: v1, k2:a:b]"); + Assert.assertEquals(ImmutableMap.of("k1", "v1", "k2", "a:b"), + config.getMap(ValidationOptions.pairs)); + + HugeConfig invalid = configOf(ValidationOptions.pairs.name(), "[k1:v1, k2]"); + Assert.assertThrows(IllegalStateException.class, () -> { + invalid.getMap(ValidationOptions.pairs); + }, e -> { + Assert.assertContains("Invalid option format for 'validation.pairs': k2", + e.getMessage()); + Assert.assertContains("expect KEY:VALUE", e.getMessage()); + }); + } + + private static HugeConfig configOf(String key, Object value) { + Map options = new HashMap<>(); + options.put(key, value); + return new HugeConfig(options); + } + + public static class ValidationOptions extends OptionHolder { + + private static volatile ValidationOptions instance; + + public static synchronized ValidationOptions instance() { + if (instance == null) { + instance = new ValidationOptions(); + instance.registerOptions(); + } + return instance; + } + + public static final ConfigOption httpUrl = + new ConfigOption<>( + "validation.http_url", + "url option normalized with http scheme", + disallowEmpty(), + "localhost:8080" + ).withUrlNormalization("http"); + + public static final ConfigOption httpsUrl = + new ConfigOption<>( + "validation.https_url", + "url option normalized with https scheme", + disallowEmpty(), + "localhost:8443" + ).withUrlNormalization("https://"); + + public static final ConfigOption plainUrl = + new ConfigOption<>( + "validation.plain_url", + "url option without normalization", + disallowEmpty(), + "localhost:8080" + ); + + public static final ConfigOption port = + new ConfigOption<>( + "validation.port", + "port option", + rangeInt(1, 65535), + 8080 + ); + + public static final ConfigOption name = + new ConfigOption<>( + "validation.name", + "non-empty name option", + disallowEmpty(), + "default-name" + ); + + public static final ConfigListOption pairs = + new ConfigListOption<>( + "validation.pairs", + false, + "key value pairs", + null, + String.class, + ImmutableList.of() + ); + } +} diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/config/TypedOptionTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/config/TypedOptionTest.java new file mode 100644 index 0000000000..c962329b1b --- /dev/null +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/config/TypedOptionTest.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.hugegraph.unit.config; + +import static org.apache.hugegraph.config.OptionChecker.allowValues; +import static org.apache.hugegraph.config.OptionChecker.disallowEmpty; +import static org.apache.hugegraph.config.OptionChecker.inValues; +import static org.apache.hugegraph.config.OptionChecker.nonNegativeInt; +import static org.apache.hugegraph.config.OptionChecker.positiveInt; +import static org.apache.hugegraph.config.OptionChecker.rangeDouble; +import static org.apache.hugegraph.config.OptionChecker.rangeInt; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.hugegraph.config.ConfigConvOption; +import org.apache.hugegraph.config.ConfigException; +import org.apache.hugegraph.config.ConfigListConvOption; +import org.apache.hugegraph.config.ConfigListOption; +import org.apache.hugegraph.config.ConfigOption; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Test; + +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; + +public class TypedOptionTest { + + @Test + public void testParseConvertNumbersAndBooleans() { + ConfigOption intOption = new ConfigOption<>( + "typed.int", "int option", rangeInt(1, 100), 10); + Assert.assertEquals(42, intOption.parseConvert("42")); + Assert.assertEquals(1, intOption.parseConvert("1")); + Assert.assertEquals(100, intOption.parseConvert("100")); + + Assert.assertThrows(ConfigException.class, () -> { + intOption.parseConvert("forty-two"); + }, e -> { + Assert.assertContains("Invalid type of value 'forty-two' for option 'typed.int'", + e.getMessage()); + Assert.assertContains("expect 'Integer' type", e.getMessage()); + }); + Assert.assertThrows(ConfigException.class, () -> { + intOption.parseConvert("101"); + }, e -> { + Assert.assertContains("Invalid option value for 'typed.int': 101", e.getMessage()); + }); + Assert.assertThrows(ConfigException.class, () -> intOption.parseConvert("0")); + + ConfigOption longOption = new ConfigOption<>( + "typed.long", "long option", null, 1L); + Assert.assertEquals(Long.MAX_VALUE, longOption.parseConvert("9223372036854775807")); + Assert.assertThrows(ConfigException.class, () -> { + longOption.parseConvert("9223372036854775808"); + }); + + ConfigOption doubleOption = new ConfigOption<>( + "typed.double", "double option", rangeDouble(0D, 1D), 0.5D); + Assert.assertEquals(0.25D, doubleOption.parseConvert("0.25")); + Assert.assertThrows(ConfigException.class, () -> doubleOption.parseConvert("1.01")); + Assert.assertThrows(ConfigException.class, () -> doubleOption.parseConvert("half")); + + ConfigOption boolOption = new ConfigOption<>( + "typed.bool", "bool option", disallowEmpty(), false); + Assert.assertEquals(true, boolOption.parseConvert("true")); + Assert.assertEquals(false, boolOption.parseConvert("false")); + Assert.assertThrows(ConfigException.class, () -> { + boolOption.parseConvert("maybe"); + }, e -> { + Assert.assertContains("expect 'Boolean' type", e.getMessage()); + }); + } + + @Test + public void testParseConvertStringWithChecker() { + ConfigOption option = new ConfigOption<>( + "typed.choice", "choice option", + allowValues("rocksdb", "hstore"), "rocksdb"); + Assert.assertEquals("hstore", option.parseConvert("hstore")); + // Values are matched exactly, no case folding or trimming + Assert.assertThrows(ConfigException.class, () -> { + option.parseConvert("RocksDB"); + }, e -> { + Assert.assertContains("Invalid option value for 'typed.choice': RocksDB", + e.getMessage()); + }); + Assert.assertThrows(ConfigException.class, () -> option.parseConvert(" hstore")); + + ConfigOption nonEmpty = new ConfigOption<>( + "typed.non_empty", "non-empty option", disallowEmpty(), "value"); + Assert.assertEquals(" value ", nonEmpty.parseConvert(" value ")); + Assert.assertThrows(ConfigException.class, () -> nonEmpty.parseConvert("")); + Assert.assertThrows(ConfigException.class, () -> nonEmpty.parseConvert(" ")); + } + + @Test + public void testParseConvertClass() { + ConfigOption option = new ConfigOption<>( + "typed.class", "class option", null, Object.class); + Assert.assertEquals(String.class, option.parseConvert("java.lang.String")); + // Accepts the format produced by Class.toString() + Assert.assertEquals(String.class, option.parseConvert(String.class.toString())); + + Assert.assertThrows(ConfigException.class, () -> { + option.parseConvert("org.apache.hugegraph.NoSuchClass"); + }, e -> { + Assert.assertContains("Failed to parse Class from String " + + "'org.apache.hugegraph.NoSuchClass'", e.getMessage()); + Assert.assertInstanceOf(ClassNotFoundException.class, e.getCause()); + }); + } + + @Test + public void testConstructWithInvalidTypeOrDefault() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + new ConfigOption<>("typed.map", "map option", new HashMap()); + }, e -> { + Assert.assertContains("doesn't belong to acceptable type set", e.getMessage()); + }); + + Assert.assertThrows(NullPointerException.class, () -> { + new ConfigOption<>("typed.null", false, "null default", null, String.class, null); + }, e -> { + Assert.assertContains("'value' of 'typed.null' can't be null", e.getMessage()); + }); + + Assert.assertThrows(NullPointerException.class, () -> { + new ConfigOption<>(null, false, "no name", null, String.class, "v"); + }); + } + + @Test + public void testListOptionParse() { + ConfigListOption option = new ConfigListOption<>( + "typed.list", "int list", disallowEmpty(), 1, 2); + Assert.assertEquals(ImmutableList.of(1, 2), option.defaultValue()); + Assert.assertEquals(ImmutableList.of(3, 4, 5), option.parseConvert("[3, 4 ,5]")); + Assert.assertEquals(ImmutableList.of(7), option.parseConvert("7")); + + Assert.assertThrows(ConfigException.class, () -> { + option.parseConvert("[1, two]"); + }, e -> { + Assert.assertContains("Invalid type of value 'two'", e.getMessage()); + }); + // An empty list literal yields one blank element, which isn't a valid integer + Assert.assertThrows(ConfigException.class, () -> option.parseConvert("[]")); + } + + @Test + public void testListOptionWithInValuesChecker() { + ConfigListOption option = new ConfigListOption<>( + "typed.list_in", "string list", inValues("a", "b", "c"), "a"); + Assert.assertEquals(ImmutableList.of("c", "a"), option.parseConvert("[c, a]")); + Assert.assertThrows(ConfigException.class, () -> { + option.parseConvert("[a, d]"); + }, e -> { + Assert.assertContains("Invalid option value for 'typed.list_in'", e.getMessage()); + }); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + new ConfigListOption("typed.list_empty", false, "empty list", + null, null, Collections.emptyList()); + }, e -> { + Assert.assertContains("Element class can't be null", e.getMessage()); + }); + } + + @Test + public void testConvOptionChecksRawValueBeforeConvert() { + ConfigConvOption option = new ConfigConvOption<>( + "typed.unit", "time unit", allowValues("SECONDS", "MINUTES"), + TimeUnit::valueOf, "SECONDS"); + Assert.assertEquals(TimeUnit.SECONDS, option.defaultValue()); + Assert.assertEquals(TimeUnit.MINUTES, option.parseConvert("MINUTES")); + // HOURS is a valid TimeUnit but is rejected by the raw value checker + Assert.assertThrows(ConfigException.class, () -> option.parseConvert("HOURS")); + + ConfigConvOption unchecked = new ConfigConvOption<>( + "typed.unit_unchecked", "time unit", null, TimeUnit::valueOf, "SECONDS"); + Assert.assertThrows(IllegalArgumentException.class, () -> { + unchecked.parseConvert("WEEKS"); + }); + + Assert.assertThrows(NullPointerException.class, () -> { + new ConfigConvOption("typed.unit_null", "time unit", + null, null, "SECONDS"); + }, e -> { + Assert.assertContains("'convert' can't be null", e.getMessage()); + }); + } + + @Test + public void testListConvOptionParse() { + ConfigListConvOption option = new ConfigListConvOption<>( + "typed.units", "time units", disallowEmpty(), TimeUnit::valueOf, + "SECONDS", "DAYS"); + Assert.assertEquals(ImmutableList.of(TimeUnit.SECONDS, TimeUnit.DAYS), + option.defaultValue()); + Assert.assertEquals(ImmutableList.of(TimeUnit.HOURS, TimeUnit.MINUTES), + option.parseConvert("[HOURS, MINUTES]")); + Assert.assertThrows(IllegalArgumentException.class, () -> { + option.parseConvert("[HOURS, FORTNIGHTS]"); + }); + } + + @Test + public void testOptionCheckerBoundariesAndNulls() { + Predicate range = rangeInt(-1, 1); + Assert.assertTrue(range.apply(-1)); + Assert.assertTrue(range.apply(1)); + Assert.assertFalse(range.apply(-2)); + Assert.assertFalse(range.apply(2)); + Assert.assertFalse(range.apply(null)); + + Predicate doubles = rangeDouble(0.1D, 0.2D); + Assert.assertTrue(doubles.apply(0.1D)); + Assert.assertTrue(doubles.apply(0.2D)); + Assert.assertFalse(doubles.apply(0.20001D)); + Assert.assertFalse(doubles.apply(null)); + + Predicate positive = positiveInt(); + Assert.assertTrue(positive.apply(1L)); + Assert.assertFalse(positive.apply(0L)); + Assert.assertFalse(positive.apply(null)); + + Predicate nonNegative = nonNegativeInt(); + Assert.assertTrue(nonNegative.apply(0L)); + Assert.assertFalse(nonNegative.apply(-1L)); + Assert.assertFalse(nonNegative.apply(null)); + + Predicate allowed = allowValues("x", "y"); + Assert.assertTrue(allowed.apply("y")); + Assert.assertFalse(allowed.apply("z")); + Assert.assertFalse(allowed.apply(null)); + + Predicate> in = inValues("x", "y"); + Assert.assertTrue(in.apply(ImmutableList.of())); + Assert.assertTrue(in.apply(ImmutableList.of("x", "x", "y"))); + Assert.assertFalse(in.apply(ImmutableList.of("x", "z"))); + Assert.assertFalse(in.apply(null)); + } + + @Test + public void testDisallowEmptyChecker() { + Predicate nonEmpty = disallowEmpty(); + Assert.assertFalse(nonEmpty.apply(null)); + Assert.assertFalse(nonEmpty.apply("")); + Assert.assertFalse(nonEmpty.apply(" \t")); + Assert.assertFalse(nonEmpty.apply(new String[0])); + Assert.assertFalse(nonEmpty.apply(new int[0])); + Assert.assertFalse(nonEmpty.apply(Collections.emptyList())); + Assert.assertFalse(nonEmpty.apply(Collections.emptySet())); + + Assert.assertTrue(nonEmpty.apply("a")); + Assert.assertTrue(nonEmpty.apply(new String[]{""})); + Assert.assertTrue(nonEmpty.apply(ImmutableList.of(""))); + Assert.assertTrue(nonEmpty.apply(0)); + Assert.assertTrue(nonEmpty.apply(false)); + } +} diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/license/MachineInfoTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/license/MachineInfoTest.java index 9dc69cbdb9..3e5a0ba4ea 100644 --- a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/license/MachineInfoTest.java +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/license/MachineInfoTest.java @@ -18,10 +18,14 @@ package org.apache.hugegraph.unit.license; import java.net.InetAddress; -import java.net.UnknownHostException; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.regex.Pattern; +import org.junit.Assume; import org.junit.Test; import org.apache.hugegraph.license.MachineInfo; @@ -73,15 +77,89 @@ public void testGetLocalAllInetAddress() { } @Test - public void testGetMacByInetAddress() throws UnknownHostException { + public void testGetMacByInetAddress() throws Exception { List addressList = machineInfo.getLocalAllInetAddress(); for (InetAddress address : addressList) { - String mac = machineInfo.getMacByInetAddress(address); - Assert.assertTrue(MAC_PATTERN.matcher(mac).matches()); + byte[] hardwareAddress = NetworkInterface.getByInetAddress(address) + .getHardwareAddress(); + if (hardwareAddress == null) { + Throwable exception = Assert.assertThrows(RuntimeException.class, + () -> machineInfo.getMacByInetAddress(address)); + Assert.assertFalse(exception instanceof NullPointerException); + } else { + String mac = machineInfo.getMacByInetAddress(address); + Assert.assertTrue(MAC_PATTERN.matcher(mac).matches()); + } } InetAddress address = InetAddress.getByAddress(new byte[]{0, 0, 0, 0}); - Assert.assertThrows(RuntimeException.class, () -> { - machineInfo.getMacByInetAddress(address); - }); + Throwable exception = Assert.assertThrows(RuntimeException.class, + () -> machineInfo.getMacByInetAddress(address)); + Assert.assertFalse(exception instanceof NullPointerException); + } + + @Test + public void testGetMacAddressWithoutMac() throws SocketException { + InetAddress loopback = InetAddress.getLoopbackAddress(); + Assume.assumeTrue(NetworkInterface.getByInetAddress(loopback) + .getHardwareAddress() == null); + MachineInfo info = new MachineInfo() { + @Override + public List getLocalAllInetAddress() { + return Collections.singletonList(loopback); + } + }; + Throwable exception = Assert.assertThrows(RuntimeException.class, + () -> info.getMacByInetAddress(loopback)); + Assert.assertFalse(exception instanceof NullPointerException); + Assert.assertEquals("Network interface has no hardware address", exception.getMessage()); + List addresses = info.getMacAddress(); + Assert.assertTrue(addresses.isEmpty()); + Assert.assertSame(addresses, info.getMacAddress()); + } + + @Test + public void testGetMacAddressSkipsMissingAndDuplicateMac() throws SocketException { + InetAddress loopback = InetAddress.getLoopbackAddress(); + Assume.assumeTrue(NetworkInterface.getByInetAddress(loopback) + .getHardwareAddress() == null); + InetAddress addressWithMac = null; + for (InetAddress address : machineInfo.getLocalAllInetAddress()) { + if (NetworkInterface.getByInetAddress(address).getHardwareAddress() != null) { + addressWithMac = address; + break; + } + } + Assume.assumeNotNull(addressWithMac); + InetAddress address = addressWithMac; + MachineInfo info = new MachineInfo() { + @Override + public List getLocalAllInetAddress() { + return Arrays.asList(loopback, address, loopback, address); + } + }; + List addresses = info.getMacAddress(); + Assert.assertEquals(1, addresses.size()); + Assert.assertTrue(addresses.contains(machineInfo.getMacByInetAddress(address))); + Assert.assertSame(addresses, info.getMacAddress()); + } + + @Test + public void testGetMacAddressRetriesAfterLookupFailure() { + RuntimeException failure = new RuntimeException("Controlled interface enumeration failure"); + int[] attempts = {0}; + MachineInfo info = new MachineInfo() { + @Override + public List getLocalAllInetAddress() { + if (++attempts[0] == 1) { + throw failure; + } + return Collections.emptyList(); + } + }; + Assert.assertSame(failure, Assert.assertThrows(RuntimeException.class, info::getMacAddress)); + List addresses = info.getMacAddress(); + Assert.assertTrue(addresses.isEmpty()); + Assert.assertSame(addresses, info.getMacAddress()); + Assert.assertEquals(2, attempts[0]); } } diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/rest/AbstractRestClientTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/rest/AbstractRestClientTest.java index b2d2b1a3bc..ee2158b917 100644 --- a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/rest/AbstractRestClientTest.java +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/rest/AbstractRestClientTest.java @@ -17,12 +17,135 @@ package org.apache.hugegraph.unit.rest; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + import org.apache.hugegraph.rest.AbstractRestClient; +import org.apache.hugegraph.rest.RestHeaders; +import org.apache.hugegraph.testutil.Whitebox; import org.junit.Assert; import org.junit.Test; +import okhttp3.RequestBody; +import okio.Buffer; +import okio.GzipSource; + public class AbstractRestClientTest { + @Test + public void testRequestBodiesWithAsciiDefaultCharset() throws Exception { + String java = Paths.get(System.getProperty("java.home"), "bin", "java").toString(); + String classpath = System.getProperty("surefire.test.class.path", + System.getProperty("java.class.path")); + Path output = Files.createTempFile("rest-client-ascii-", ".log"); + Process process = null; + try { + process = new ProcessBuilder(java, "-Dfile.encoding=US-ASCII", "-cp", classpath, + AsciiRequestBodyProbe.class.getName()) + .redirectErrorStream(true) + .redirectOutput(output.toFile()) + .start(); + boolean finished = process.waitFor(60, TimeUnit.SECONDS); + String diagnostic = Files.readString(output, StandardCharsets.UTF_8); + Assert.assertTrue("US-ASCII probe timed out: " + diagnostic, finished); + Assert.assertEquals(diagnostic, 0, process.exitValue()); + } finally { + if (process != null && process.isAlive()) { + process.destroyForcibly(); + process.waitFor(5, TimeUnit.SECONDS); + } + Files.deleteIfExists(output); + } + } + + public static class AsciiRequestBodyProbe { + + public static void main(String[] args) throws IOException { + Assert.assertEquals(StandardCharsets.US_ASCII, Charset.defaultCharset()); + AbstractRestClientTest tests = new AbstractRestClientTest(); + tests.testJsonRequestBodyUsesUtf8(); + tests.testGzipRequestBodyUsesUtf8(); + tests.testRequestBodyRespectsExplicitCharset(); + tests.testJsonObjectRequestBodyRespectsExplicitCharset(); + } + } + + @Test + public void testJsonRequestBodyUsesUtf8() throws IOException { + String json = "{\"id\":\"\u4f60\u597d\ud83d\ude80\"}"; + assertUtf8Body(json, null, json, false); + assertUtf8Body(Collections.singletonMap("id", "\u4f60\u597d\ud83d\ude80"), + new RestHeaders(), json, false); + } + + @Test + public void testGzipRequestBodyUsesUtf8() throws IOException { + String json = "{\"id\":\"\u4f60\u597d\ud83d\ude80\"}"; + RestHeaders headers = new RestHeaders().add(RestHeaders.CONTENT_ENCODING, "gzip"); + assertUtf8Body(json, headers, json, true); + } + + @Test + public void testRequestBodyRespectsExplicitCharset() throws IOException { + RestHeaders headers = new RestHeaders().add(RestHeaders.CONTENT_TYPE, + "text/plain; charset=iso-8859-1"); + RequestBody body = requestBody("caf\u00e9", headers); + Buffer buffer = new Buffer(); + body.writeTo(buffer); + Assert.assertEquals(StandardCharsets.ISO_8859_1, body.contentType().charset()); + Assert.assertArrayEquals("caf\u00e9".getBytes(StandardCharsets.ISO_8859_1), + buffer.readByteArray()); + } + + @Test + public void testJsonObjectRequestBodyRespectsExplicitCharset() throws IOException { + String value = "\u4f60\u597d\ud83d\ude80"; + String json = "{\"id\":\"" + value + "\"}"; + for (Charset charset : new Charset[]{StandardCharsets.UTF_8, StandardCharsets.UTF_16}) { + RestHeaders headers = new RestHeaders().add(RestHeaders.CONTENT_TYPE, + "application/json; charset=" + charset.name()); + assertBody(Collections.singletonMap("id", value), headers, json, charset, false); + headers.add(RestHeaders.CONTENT_ENCODING, "gzip"); + assertBody(Collections.singletonMap("id", value), headers, json, charset, true); + } + } + + private static void assertUtf8Body(Object value, RestHeaders headers, + String expected, boolean gzip) throws IOException { + assertBody(value, headers, expected, StandardCharsets.UTF_8, gzip); + } + + private static void assertBody(Object value, RestHeaders headers, String expected, + Charset charset, boolean gzip) throws IOException { + RequestBody body = requestBody(value, headers); + Assert.assertEquals(charset, body.contentType().charset()); + Buffer buffer = new Buffer(); + body.writeTo(buffer); + if (gzip) { + try (GzipSource source = new GzipSource(buffer)) { + Buffer decoded = new Buffer(); + decoded.writeAll(source); + Assert.assertArrayEquals(expected.getBytes(charset), + decoded.readByteArray()); + } + } else { + Assert.assertArrayEquals(expected.getBytes(charset), + buffer.readByteArray()); + } + } + + private static RequestBody requestBody(Object value, RestHeaders headers) { + return Whitebox.invokeStatic(AbstractRestClient.class, + new Class[]{Object.class, RestHeaders.class}, + "buildRequestBody", value, headers); + } + @Test public void testEncodeWithSpaces() { String raw = "hello world"; diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/rest/HostNameVerifierTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/rest/HostNameVerifierTest.java new file mode 100644 index 0000000000..58af9f9325 --- /dev/null +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/rest/HostNameVerifierTest.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.hugegraph.unit.rest; + +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; + +import org.apache.hugegraph.rest.AbstractRestClient; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * Checks the certificate identity rules of {@link AbstractRestClient.HostNameVerifier} + * with in-memory certificates, no TLS handshake involved. + */ +public class HostNameVerifierTest { + + private static final int SAN_DNS = 2; + private static final int SAN_IP = 7; + + @Test + public void testConfiguredUrlDoesNotGrantTrust() throws Exception { + SSLSession session = sessionWithSans(san(SAN_DNS, "other.example")); + // The URL host equals the requested host, the certificate still has to match + Assert.assertFalse(verifier("https://graph.example:8080").verify("graph.example", session)); + Assert.assertFalse(verifier("graph.example").verify("graph.example", session)); + // A URL host ending with the requested host isn't a match either + Assert.assertFalse(verifier("https://evil-graph.example").verify("graph.example", session)); + Assert.assertTrue(verifier("https://graph.example").verify("other.example", session)); + } + + @Test + public void testDnsSubjectAlternativeNames() throws Exception { + SSLSession session = sessionWithSans(san(SAN_DNS, "graph.example"), + san(SAN_DNS, "*.cluster.example")); + AbstractRestClient.HostNameVerifier verifier = verifier("https://graph.example"); + Assert.assertTrue(verifier.verify("graph.example", session)); + Assert.assertTrue(verifier.verify("GRAPH.Example", session)); + Assert.assertTrue(verifier.verify("node1.cluster.example", session)); + + Assert.assertFalse(verifier.verify("sub.graph.example", session)); + Assert.assertFalse(verifier.verify("raph.example", session)); + // A wildcard only covers exactly one left-most label + Assert.assertFalse(verifier.verify("cluster.example", session)); + Assert.assertFalse(verifier.verify("a.node1.cluster.example", session)); + } + + @Test + public void testIpSubjectAlternativeNames() throws Exception { + SSLSession session = sessionWithSans(san(SAN_IP, "127.0.0.1"), + san(SAN_DNS, "localhost")); + AbstractRestClient.HostNameVerifier verifier = verifier("https://127.0.0.1"); + Assert.assertTrue(verifier.verify("127.0.0.1", session)); + Assert.assertTrue(verifier.verify("localhost", session)); + Assert.assertFalse(verifier.verify("127.0.0.2", session)); + + // An IP host never matches a DNS entry with the same text + SSLSession dnsOnly = sessionWithSans(san(SAN_DNS, "127.0.0.1")); + Assert.assertFalse(verifier.verify("127.0.0.1", dnsOnly)); + // A DNS host never matches an IP entry + SSLSession ipOnly = sessionWithSans(san(SAN_IP, "127.0.0.1")); + Assert.assertFalse(verifier.verify("localhost", ipOnly)); + } + + @Test + public void testCertificateWithoutSubjectAlternativeNames() throws Exception { + X509Certificate cert = Mockito.mock(X509Certificate.class); + Mockito.when(cert.getSubjectAlternativeNames()).thenReturn(null); + SSLSession session = sessionWith(cert); + // The common name is not used as an identity fallback + Assert.assertFalse(verifier("https://localhost").verify("localhost", session)); + } + + @Test + public void testUnverifiedPeerOrInvalidHost() throws Exception { + SSLSession unverified = Mockito.mock(SSLSession.class); + Mockito.when(unverified.getPeerCertificates()) + .thenThrow(new SSLPeerUnverifiedException("no peer certificate")); + Assert.assertFalse(verifier("https://localhost").verify("localhost", unverified)); + + SSLSession session = sessionWithSans(san(SAN_DNS, "localhost")); + Assert.assertFalse(verifier("https://localhost").verify("", session)); + Assert.assertFalse(verifier("https://localhost").verify("l\u00f3calhost", session)); + } + + private static AbstractRestClient.HostNameVerifier verifier(String url) { + return new AbstractRestClient.HostNameVerifier(url); + } + + private static List san(int type, String value) { + return Arrays.asList(type, value); + } + + private static SSLSession sessionWithSans(List... sans) throws Exception { + X509Certificate cert = Mockito.mock(X509Certificate.class); + List> names = new ArrayList<>(Arrays.asList(sans)); + Mockito.when(cert.getSubjectAlternativeNames()).thenReturn(names); + return sessionWith(cert); + } + + private static SSLSession sessionWith(X509Certificate cert) throws Exception { + SSLSession session = Mockito.mock(SSLSession.class); + Mockito.when(session.getPeerCertificates()).thenReturn(new Certificate[]{cert}); + return session; + } +} diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/rest/RestClientTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/rest/RestClientTest.java index 93a69dd8ec..8f61a8460c 100644 --- a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/rest/RestClientTest.java +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/rest/RestClientTest.java @@ -17,16 +17,29 @@ package org.apache.hugegraph.unit.rest; -import java.security.NoSuchAlgorithmException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +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.UUID; import java.util.concurrent.TimeUnit; -import java.util.function.BiFunction; +import java.util.stream.Stream; +import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSessionContext; import org.apache.hugegraph.rest.AbstractRestClient; import org.apache.hugegraph.rest.ClientException; @@ -43,6 +56,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.net.HttpHeaders; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsServer; import lombok.SneakyThrows; import okhttp3.HttpUrl; @@ -145,30 +160,112 @@ public void testPostHttpsWithTokenAndAllParams() { } @Test - public void testHostNameVerifier() { - BiFunction verifer = (url, hostname) -> { - AbstractRestClient.HostNameVerifier verifier; - SSLSession session; - try { - SSLSessionContext sc = SSLContext.getDefault() - .getClientSessionContext(); - session = sc.getSession(new byte[]{11}); - verifier = new AbstractRestClient.HostNameVerifier(url); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); + public void testHttpsCertificateIdentity() throws Exception { + Path directory = Files.createTempDirectory("rest-client-tls-"); + try { + Path validStore = createServerStore(directory, "valid", "dns:localhost,ip:127.0.0.1"); + Path wrongStore = createServerStore(directory, "wrong", "dns:other.example"); + Path noSanStore = createServerStore(directory, "no-san", null); + assertHttpsRequest(validStore, validStore, "localhost", null); + assertHttpsRequest(validStore, validStore, "127.0.0.1", null); + assertHttpsRequest(wrongStore, wrongStore, "localhost", SSLPeerUnverifiedException.class); + assertHttpsRequest(noSanStore, noSanStore, "localhost", SSLPeerUnverifiedException.class); + assertHttpsRequest(validStore, wrongStore, "localhost", SSLHandshakeException.class); + } finally { + try (Stream paths = Files.walk(directory)) { + for (Path path : (Iterable) paths.sorted(Comparator.reverseOrder())::iterator) { + Files.deleteIfExists(path); + } } - return verifier.verify(hostname, session); - }; + } + } - Assert.assertTrue(verifer.apply("http://baidu.com", "baidu.com")); - Assert.assertTrue(verifer.apply("http://test1.baidu.com", "baidu.com")); - Assert.assertTrue(verifer.apply("http://test2.baidu.com", "baidu.com")); - Assert.assertFalse(verifer.apply("http://baidu2.com", "baidu.com")); - Assert.assertTrue(verifer.apply("http://baidu.com", "")); - Assert.assertTrue(verifer.apply("baidu.com", "baidu.com")); - Assert.assertTrue(verifer.apply("http://baidu.com/test", "baidu.com")); - Assert.assertTrue(verifer.apply("baidu.com/test/abc", "baidu.com")); - Assert.assertFalse(verifer.apply("baidu.com.sina.com", "baidu.com")); + private static Path createServerStore(Path directory, String name, String san) throws Exception { + Path store = directory.resolve(name + ".p12"); + List command = new ArrayList<>(Arrays.asList( + Paths.get(System.getProperty("java.home"), "bin", "keytool").toString(), + "-genkeypair", "-alias", "server", "-keyalg", "RSA", "-keysize", "2048", + "-validity", "2", "-dname", "CN=localhost", "-storetype", "PKCS12", + "-keystore", store.toString(), "-storepass", "test-password", + "-keypass", "test-password", "-noprompt")); + if (san != null) { + command.addAll(Arrays.asList("-ext", "SAN=" + san)); + } + Path log = directory.resolve(name + ".log"); + Process process = new ProcessBuilder(command).redirectErrorStream(true) + .redirectOutput(log.toFile()).start(); + try { + boolean finished = process.waitFor(30, TimeUnit.SECONDS); + Assert.assertTrue("keytool timed out", finished); + Assert.assertEquals(Files.readString(log, StandardCharsets.UTF_8), 0, process.exitValue()); + } finally { + if (process.isAlive()) { + process.destroyForcibly(); + process.waitFor(5, TimeUnit.SECONDS); + } + } + return store; + } + + private static KeyStore readStore(Path path) throws Exception { + KeyStore store = KeyStore.getInstance("PKCS12"); + try (InputStream input = Files.newInputStream(path)) { + store.load(input, "test-password".toCharArray()); + } + return store; + } + + private static void assertHttpsRequest(Path serverStore, Path trustedStore, String hostname, + Class failure) throws Exception { + KeyStore keys = readStore(serverStore); + KeyManagerFactory keyManagers = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagers.init(keys, "test-password".toCharArray()); + SSLContext context = SSLContext.getInstance("TLS"); + context.init(keyManagers.getKeyManagers(), null, null); + HttpsServer server = HttpsServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.setHttpsConfigurator(new HttpsConfigurator(context)); + server.createContext("/probe", exchange -> { + byte[] response = "{}".getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, response.length); + try (OutputStream output = exchange.getResponseBody()) { + output.write(response); + } + }); + Path trustStore = Files.createTempFile(serverStore.getParent(), "trust-", ".jks"); + server.start(); + try { + KeyStore trusted = KeyStore.getInstance("JKS"); + trusted.load(null, null); + trusted.setCertificateEntry("server", readStore(trustedStore).getCertificate("server")); + try (OutputStream output = Files.newOutputStream(trustStore)) { + trusted.store(output, "test-password".toCharArray()); + } + RestClientConfig config = RestClientConfig.builder().timeout(5000) + .trustStoreFile(trustStore.toString()).trustStorePassword("test-password").build(); + String url = "https://" + hostname + ":" + server.getAddress().getPort(); + RestClient client = new AbstractRestClient(url, config) { + @Override + protected void checkStatus(Response response, int... statuses) { + Assert.assertEquals(200, response.code()); + } + }; + try { + if (failure == null) { + Assert.assertEquals(200, client.get("probe").status()); + } else { + org.junit.Assert.assertThrows(failure, () -> client.get("probe")); + } + } finally { + client.close(); + } + SSLSession session = Mockito.mock(SSLSession.class); + Mockito.when(session.getPeerCertificates()).thenReturn(keys.getCertificateChain("server")); + Assert.assertEquals(failure != SSLPeerUnverifiedException.class, + new AbstractRestClient.HostNameVerifier(url).verify(hostname, session)); + } finally { + server.stop(0); + Files.deleteIfExists(trustStore); + } } @Test diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/DateUtilTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/DateUtilTest.java index 7c51406b36..6c8214e3fe 100644 --- a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/DateUtilTest.java +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/DateUtilTest.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; @@ -81,9 +82,11 @@ public void testNow() { @Test public void testParseCornerDateValue() throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); + final Date expected = DateUtil.parse("0", "yyyy"); int threadCount = 10; List threads = new ArrayList<>(threadCount); AtomicInteger errorCount = new AtomicInteger(0); + AtomicReference firstError = new AtomicReference<>(); for (int t = 0; t < threadCount; t++) { Thread thread = new Thread(() -> { try { @@ -92,9 +95,9 @@ public void testParseCornerDateValue() throws InterruptedException { throw new RuntimeException(e); } try { - Assert.assertEquals(new Date(-62167248343000L), - DateUtil.parse("0", "yyyy")); - } catch (Exception e) { + Assert.assertEquals(expected, DateUtil.parse("0", "yyyy")); + } catch (Throwable e) { + firstError.compareAndSet(null, e); errorCount.incrementAndGet(); } }); @@ -109,6 +112,15 @@ public void testParseCornerDateValue() throws InterruptedException { thread.join(); } + Throwable error = firstError.get(); + if (error != null) { + AssertionError assertion = new AssertionError(String.format( + "Expected concurrent parses to match " + + "baseline result, but got %s failures", + errorCount.get())); + assertion.initCause(error); + throw assertion; + } Assert.assertEquals(0, errorCount.get()); } diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/ReflectionUtilTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/ReflectionUtilTest.java index f511ddfc7d..6f47aadf4d 100644 --- a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/ReflectionUtilTest.java +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/ReflectionUtilTest.java @@ -94,7 +94,7 @@ public void testClasses() throws IOException { @SuppressWarnings("unchecked") List classes = IteratorUtils.toList(ReflectionUtil.classes( "org.apache.hugegraph.util")); - Assert.assertEquals(19, classes.size()); + Assert.assertEquals(20, classes.size()); classes.sort(Comparator.comparing(ClassInfo::getName)); Assert.assertEquals("org.apache.hugegraph.util.Bytes", classes.get(0).getName()); diff --git a/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/RocksDBRuntimeTest.java b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/RocksDBRuntimeTest.java new file mode 100644 index 0000000000..6eb9fd1fc8 --- /dev/null +++ b/hugegraph-commons/hugegraph-common/src/test/java/org/apache/hugegraph/unit/util/RocksDBRuntimeTest.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.hugegraph.unit.util; + +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.ProtectionDomain; + +import javax.tools.ToolProvider; + +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.util.RocksDBRuntime; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class RocksDBRuntimeTest { + + @Rule + public TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void testStandardAndUnrelatedMarkerOrigin() throws Exception { + Path rocks = this.temporary.newFolder("standard").toPath(); + Path marker = this.temporary.newFolder("unrelated").toPath(); + compile(rocks, "RocksDB"); + compile(marker, "SidePluginRepo"); + verifyWithLoader("rocksdb", rocks); + verifyWithLoader("rocksdb", rocks, marker); + Assert.assertThrows(IllegalStateException.class, () -> verifyWithLoader("topling", rocks, marker)); + } + + @Test + public void testToplingRequiresSameOriginAndMatchingConfiguration() throws Exception { + Path runtime = this.temporary.newFolder("topling").toPath(); + compile(runtime, "RocksDB"); + compile(runtime, "SidePluginRepo"); + verifyWithLoader("topling", runtime); + Assert.assertThrows(IllegalStateException.class, () -> verifyWithLoader("rocksdb", runtime)); + Assert.assertThrows(IllegalArgumentException.class, () -> verifyWithLoader("invalid", runtime)); + } + + @Test + public void testMissingContextRuntimeFallsBackToDefiningLoader() throws Exception { + for (String provider : new String[]{"rocksdb", "topling"}) { + Path runtime = this.temporary.newFolder("fallback-" + provider).toPath(); + compile(runtime, "RocksDB"); + if ("topling".equals(provider)) { + compile(runtime, "SidePluginRepo"); + } + verifyWithDefiningLoader(provider, null, runtime); + try (URLClassLoader isolated = new URLClassLoader(new URL[0], null)) { + verifyWithDefiningLoader(provider, isolated, runtime); + String mismatch = "topling".equals(provider) ? "rocksdb" : "topling"; + Assert.assertThrows(IllegalStateException.class, + () -> verifyWithDefiningLoader(mismatch, isolated, runtime)); + } + } + } + + @Test + public void testContextLinkageFailureIsNotHiddenByFallback() throws Exception { + Path runtime = this.temporary.newFolder("fallback-linkage").toPath(); + compile(runtime, "RocksDB"); + LinkageError failure = new NoClassDefFoundError("broken context runtime dependency"); + ClassLoader broken = new ClassLoader(null) { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if ("org.rocksdb.RocksDB".equals(name)) { + throw failure; + } + return super.loadClass(name, resolve); + } + }; + Assert.assertSame(failure, Assert.assertThrows(LinkageError.class, + () -> verifyWithDefiningLoader("rocksdb", broken, runtime))); + } + + @Test + public void testContextProviderMismatchDoesNotSelectDefiningRuntime() throws Exception { + Path standard = this.temporary.newFolder("preferred-context").toPath(); + Path topling = this.temporary.newFolder("unused-defining").toPath(); + compile(standard, "RocksDB"); + compile(topling, "RocksDB"); + compile(topling, "SidePluginRepo"); + try (URLClassLoader context = new URLClassLoader(new URL[]{standard.toUri().toURL()}, null)) { + verifyWithDefiningLoader("rocksdb", context, topling); + Assert.assertThrows(IllegalStateException.class, + () -> verifyWithDefiningLoader("topling", context, topling)); + } + } + + @Test + public void testMissingCodeSourceFailsWithControlledError() throws Exception { + Path runtime = this.temporary.newFolder("no-origin").toPath(); + compile(runtime, "RocksDB"); + byte[] bytes = Files.readAllBytes(runtime.resolve("org/rocksdb/RocksDB.class")); + ClassLoader loader = new ClassLoader(null) { + @Override + protected Class findClass(String name) throws ClassNotFoundException { + if ("org.rocksdb.RocksDB".equals(name)) { + return this.defineClass(name, bytes, 0, bytes.length, new ProtectionDomain(null, null)); + } + throw new ClassNotFoundException(name); + } + }; + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(loader); + for (String provider : new String[]{"rocksdb", "topling"}) { + IllegalStateException failure = Assert.assertThrows(IllegalStateException.class, + () -> RocksDBRuntime.verify(provider)); + Assert.assertTrue(failure.getMessage().contains("Cannot verify RocksDB runtime origin")); + } + } finally { + Thread.currentThread().setContextClassLoader(previous); + } + } + + private static void verifyWithDefiningLoader(String provider, ClassLoader context, Path runtime) throws Exception { + String resource = "org/apache/hugegraph/util/RocksDBRuntime.class"; + Path target = runtime.resolve(resource); + Files.createDirectories(target.getParent()); + try (InputStream bytes = RocksDBRuntime.class.getClassLoader().getResourceAsStream(resource)) { + Files.copy(bytes, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + try (URLClassLoader defining = new URLClassLoader(new URL[]{runtime.toUri().toURL()}, null)) { + Thread.currentThread().setContextClassLoader(context); + Class verifier = Class.forName(RocksDBRuntime.class.getName(), true, defining); + try { + verifier.getMethod("verify", String.class).invoke(null, provider); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Error) { + throw (Error) cause; + } + throw (Exception) cause; + } + } finally { + Thread.currentThread().setContextClassLoader(previous); + } + } + + private static void compile(Path root, String name) throws Exception { + Path source = root.resolve("org/rocksdb/" + name + ".java"); + Files.createDirectories(source.getParent()); + // Loading these classes must not initialize them or native libraries. + String code = "package org.rocksdb; public class " + name + + " { static { if (true) throw new AssertionError(\"initialized\"); } }"; + Files.write(source, code.getBytes(StandardCharsets.UTF_8)); + Assert.assertEquals(0, ToolProvider.getSystemJavaCompiler().run(null, null, null, source.toString())); + } + + private static void verifyWithLoader(String provider, Path... roots) throws Exception { + URL[] urls = new URL[roots.length]; + for (int i = 0; i < roots.length; i++) { + urls[i] = roots[i].toUri().toURL(); + } + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + try (URLClassLoader loader = new URLClassLoader(urls, null)) { + Thread.currentThread().setContextClassLoader(loader); + RocksDBRuntime.verify(provider); + } finally { + Thread.currentThread().setContextClassLoader(previous); + } + } +} diff --git a/hugegraph-commons/hugegraph-rpc/pom.xml b/hugegraph-commons/hugegraph-rpc/pom.xml index 23f0a32077..142092634e 100644 --- a/hugegraph-commons/hugegraph-rpc/pom.xml +++ b/hugegraph-commons/hugegraph-rpc/pom.xml @@ -106,7 +106,7 @@ io.grpc grpc-core - 1.28.1 + ${grpc.version} @@ -124,7 +124,6 @@ org.jacoco jacoco-maven-plugin - 0.8.2 pre-unit-test diff --git a/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcConsumerConfig.java b/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcConsumerConfig.java index 9459ac516a..96819072e9 100644 --- a/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcConsumerConfig.java +++ b/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcConsumerConfig.java @@ -20,7 +20,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReference; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.config.RpcOptions; @@ -39,9 +41,6 @@ import com.alipay.sofa.rpc.core.response.SofaResponse; import com.alipay.sofa.rpc.ext.Extension; import com.alipay.sofa.rpc.ext.ExtensionLoaderFactory; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; public class RpcConsumerConfig implements RpcServiceConfig4Client { @@ -59,8 +58,8 @@ public RpcConsumerConfig(HugeConfig config, String remoteUrls) { RpcCommonConfig.initRpcConfigs(config); this.conf = config; this.remoteUrls = remoteUrls; - this.configs = Maps.newHashMap(); - this.bootstraps = Lists.newArrayList(); + this.configs = new ConcurrentHashMap<>(); + this.bootstraps = new CopyOnWriteArrayList<>(); } @Override @@ -70,28 +69,119 @@ public T serviceProxy(String interfaceId) { @Override public T serviceProxy(String graph, String interfaceId) { - ConsumerConfig config = this.consumerConfig(graph, interfaceId); - ConsumerBootstrap bootstrap = Bootstraps.from(config); - this.bootstraps.add(bootstrap); - return bootstrap.refer(); + String serviceId = graph == null ? interfaceId : interfaceId + ":" + graph; + AtomicReference proxy = new AtomicReference<>(); + this.configs.compute(serviceId, (key, existing) -> { + @SuppressWarnings("unchecked") + ConsumerConfig config = existing == null ? this.consumerConfig(graph, interfaceId) : + (ConsumerConfig) existing; + ConsumerBootstrap bootstrap = Bootstraps.from(config); + try { + proxy.set(bootstrap.refer()); + this.bootstraps.add(bootstrap); + } catch (RuntimeException | Error failure) { + // Retain the cleanup owner even when refer never returned a proxy. + this.bootstraps.add(bootstrap); + throwFailure(this.cleanup(bootstrap, failure)); + } + return config; + }); + return proxy.get(); + } + + @Override + public boolean removeServiceProxy(String graph, String interfaceId, Object proxy) { + String serviceId = graph == null ? interfaceId : interfaceId + ":" + graph; + boolean[] removed = {false}; + this.configs.computeIfPresent(serviceId, (key, config) -> { + for (ConsumerBootstrap bootstrap : this.bootstraps) { + if (bootstrap.getConsumerConfig() == config && bootstrap.getProxyIns() == proxy) { + Throwable failure = this.cleanup(bootstrap, null); + if (failure != null) { + throwFailure(failure); + } + removed[0] = true; + break; + } + } + for (ConsumerBootstrap bootstrap : this.bootstraps) { + if (bootstrap.getConsumerConfig() == config) { + return config; + } + } + return null; + }); + return removed[0]; } @Override public void removeAllServiceProxy() { + Throwable failure = null; for (ConsumerBootstrap bootstrap : this.bootstraps) { - bootstrap.unRefer(); + try { + bootstrap.unRefer(); + } catch (RuntimeException | Error cleanup) { + failure = addFailure(failure, cleanup); + } + } + if (failure != null) { + throwFailure(failure); } } public void destroy() { - Set clusters = Sets.newHashSet(); + Throwable failure = null; for (ConsumerBootstrap bootstrap : this.bootstraps) { + failure = this.cleanup(bootstrap, failure); + } + if (failure != null) { + throwFailure(failure); + } + } + + private Throwable cleanup(ConsumerBootstrap bootstrap, Throwable failure) { + boolean complete = true; + try { bootstrap.unRefer(); - clusters.add(bootstrap.getCluster()); + } catch (RuntimeException | Error cleanup) { + complete = false; + failure = addFailure(failure, cleanup); + } + try { + Cluster cluster = bootstrap.getCluster(); + if (cluster != null) { + cluster.destroy(); + } + } catch (RuntimeException | Error cleanup) { + complete = false; + failure = addFailure(failure, cleanup); + } + if (complete) { + this.bootstraps.remove(bootstrap); + } + return failure; + } + + private static Throwable addFailure(Throwable failure, Throwable cleanup) { + if (failure == null) { + return cleanup; + } + if (failure == cleanup) { + return failure; } - for (Cluster cluster : clusters) { - cluster.destroy(); + if (cleanup instanceof Error && !(failure instanceof Error)) { + cleanup.addSuppressed(failure); + return cleanup; } + failure.addSuppressed(cleanup); + return failure; + } + + private static void throwFailure(Throwable failure) { + if (failure instanceof Error) { + throw (Error) failure; + } + throw (RuntimeException) failure; } private ConsumerConfig consumerConfig(String graph, @@ -103,15 +193,7 @@ private ConsumerConfig consumerConfig(String graph, serviceId = interfaceId; } - @SuppressWarnings("unchecked") - ConsumerConfig consumerConfig = (ConsumerConfig) - this.configs.get(serviceId); - if (consumerConfig != null) { - return consumerConfig; - } - - assert consumerConfig == null; - consumerConfig = new ConsumerConfig<>(); + ConsumerConfig consumerConfig = new ConsumerConfig<>(); HugeConfig conf = this.conf; String protocol = conf.get(RpcOptions.RPC_PROTOCOL); @@ -138,7 +220,6 @@ private ConsumerConfig consumerConfig(String graph, .setSerialization(conf.get(RpcOptions.RPC_SERIALIZATION)) .setLoadBalancer(loadBalancer); - this.configs.put(serviceId, consumerConfig); return consumerConfig; } diff --git a/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcProviderConfig.java b/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcProviderConfig.java index dafdfcf5a4..3ad955a868 100644 --- a/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcProviderConfig.java +++ b/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcProviderConfig.java @@ -18,14 +18,15 @@ package org.apache.hugegraph.rpc; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import com.alipay.sofa.rpc.config.ProviderConfig; +import com.alipay.sofa.rpc.config.ServerConfig; import org.apache.hugegraph.util.E; -import com.google.common.collect.Maps; public class RpcProviderConfig implements RpcServiceConfig4Server { - private final Map> configs = Maps.newHashMap(); + private final Map> configs = new ConcurrentHashMap<>(); @Override public String addService(Class clazz, S serviceImpl) { @@ -54,27 +55,58 @@ private String addService(String graph, providerConfig.setInterfaceId(interfaceId) .setRef(serviceImpl); - E.checkArgument(!this.configs.containsKey(serviceId), + E.checkArgument(this.configs.putIfAbsent(serviceId, providerConfig) == null, "Not allowed to add service already exist: '%s'", serviceId); - this.configs.put(serviceId, providerConfig); return serviceId; } @Override public void removeService(String serviceId) { - ProviderConfig config = this.configs.remove(serviceId); - E.checkArgument(config != null, - "The service '%s' doesn't exist", serviceId); - config.unExport(); + boolean[] removed = {false}; + this.configs.computeIfPresent(serviceId, (key, config) -> { + config.unExport(); + removed[0] = true; + return null; + }); + E.checkArgument(removed[0], "The service '%s' doesn't exist", serviceId); } @Override - public void removeAllService() { - for (ProviderConfig config : this.configs.values()) { + public boolean removeService(String serviceId, Object serviceImpl) { + boolean[] removed = {false}; + this.configs.computeIfPresent(serviceId, (key, config) -> { + if (config.getRef() != serviceImpl) { + return config; + } config.unExport(); + removed[0] = true; + return null; + }); + return removed[0]; + } + + @Override + public void removeAllService() { + for (String serviceId : this.configs.keySet()) { + this.configs.computeIfPresent(serviceId, (key, config) -> { + config.unExport(); + return null; + }); + } + } + + void exportAll(ServerConfig server, int timeout) { + for (String serviceId : this.configs.keySet()) { + this.configs.computeIfPresent(serviceId, (key, config) -> { + // Sofa appends a single server; retain other bindings without adding duplicates. + if (config.getServer() == null || config.getServer().stream().noneMatch(bound -> bound == server)) { + config.setServer(server); + } + config.setTimeout(timeout).export(); + return config; + }); } - this.configs.clear(); } public Map> configs() { diff --git a/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcServer.java b/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcServer.java index 7791ac8cd5..1af841c1c7 100644 --- a/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcServer.java +++ b/hugegraph-commons/hugegraph-rpc/src/main/java/org/apache/hugegraph/rpc/RpcServer.java @@ -104,11 +104,7 @@ public void exportAll() { return; } int timeout = this.conf.get(RpcOptions.RPC_SERVER_TIMEOUT) * 1000; - for (ProviderConfig providerConfig : configs.values()) { - providerConfig.setServer(this.serverConfig) - .setTimeout(timeout) - .export(); - } + this.configs.exportAll(this.serverConfig, timeout); LOG.info("RpcServer started success on port {}", this.port()); } diff --git a/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/BaseUnitTest.java b/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/BaseUnitTest.java index 919ba151f0..5098f3f2ac 100644 --- a/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/BaseUnitTest.java +++ b/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/BaseUnitTest.java @@ -17,13 +17,22 @@ package org.apache.hugegraph.unit; +import java.net.URI; import java.net.URL; +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; import org.junit.BeforeClass; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.config.OptionSpace; import org.apache.hugegraph.rpc.RpcServer; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; + +import com.alipay.sofa.rpc.config.ServerConfig; +import com.alipay.sofa.rpc.server.Server; +import io.netty.channel.ChannelFuture; public class BaseUnitTest { @@ -39,7 +48,7 @@ protected static HugeConfig config(boolean server) { protected static HugeConfig config(String type) { String name = String.format("rpc-%s.properties", type); URL conf = BaseUnitTest.class.getClassLoader().getResource(name); - return new HugeConfig(conf.getPath()); + return new HugeConfig(Paths.get(URI.create(conf.toExternalForm())).toString()); } protected static void startServer(RpcServer rpcServer) { @@ -51,6 +60,34 @@ protected static void startServer(RpcServer rpcServer) { } protected static void stopServer(RpcServer rpcServer) { - rpcServer.destroy(); + closeServer(rpcServer, rpcServer::destroy); + } + + protected static void unexportServer(RpcServer rpcServer) { + closeServer(rpcServer, rpcServer::unexportAll); + } + + protected static ChannelFuture listener(RpcServer rpcServer) { + if (!rpcServer.enabled()) { + return null; + } + ServerConfig config = Whitebox.getInternalState(rpcServer, "serverConfig"); + Server server = config.getServer(); + if (server == null || !server.isStarted()) { + return null; + } + Object remoting = Whitebox.getInternalState(server, "remotingServer"); + return Whitebox.getInternalState(remoting, "channelFuture"); + } + + private static void closeServer(RpcServer rpcServer, Runnable close) { + ChannelFuture binding = listener(rpcServer); + close.run(); + if (binding != null) { + // Bolt stops asynchronously; the next fixture may reuse this port. + ChannelFuture closed = binding.channel().closeFuture(); + Assert.assertTrue(closed.awaitUninterruptibly(5, TimeUnit.SECONDS)); + Assert.assertTrue(closed.isSuccess()); + } } } diff --git a/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/RpcConfigTest.java b/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/RpcConfigTest.java new file mode 100644 index 0000000000..cf2ce94dfe --- /dev/null +++ b/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/RpcConfigTest.java @@ -0,0 +1,536 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hugegraph.unit; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hugegraph.config.ConfigException; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.RpcOptions; +import org.apache.hugegraph.rpc.RpcClientProvider; +import org.apache.hugegraph.rpc.RpcConsumerConfig; +import org.apache.hugegraph.rpc.RpcProviderConfig; +import org.apache.hugegraph.rpc.RpcServer; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.unit.ServerClientTest.HelloService; +import org.apache.hugegraph.unit.ServerClientTest.HelloServiceImpl; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.alipay.sofa.rpc.bootstrap.ConsumerBootstrap; +import com.alipay.sofa.rpc.bootstrap.DefaultConsumerBootstrap; +import com.alipay.sofa.rpc.client.Cluster; +import com.alipay.sofa.rpc.client.FailoverCluster; +import com.alipay.sofa.rpc.config.ConsumerConfig; +import com.alipay.sofa.rpc.config.ProviderConfig; +import com.alipay.sofa.rpc.ext.Extension; +import com.alipay.sofa.rpc.ext.ExtensionLoaderFactory; + +public class RpcConfigTest extends BaseUnitTest { + + private static final String HELLO = HelloService.class.getName(); + + private static ConsumerFailure consumerFailure; + + @BeforeClass + public static void registerCleanupBootstrap() { + ExtensionLoaderFactory.getExtensionLoader(ConsumerBootstrap.class) + .loadExtension(CleanupBootstrap.class); + ExtensionLoaderFactory.getExtensionLoader(Cluster.class) + .loadExtension(CleanupCluster.class); + } + + @Test + public void testReferFailureRetainsCleanupOwner() { + this.assertReferCleanup(new IllegalStateException("refer failure"), false, false); + } + + @Test + public void testReferErrorPreservedOnSelfSuppression() { + this.assertReferCleanup(new AssertionError("refer error"), true, false); + } + + @Test + public void testCleanupErrorPromotedAboveReferException() { + this.assertReferCleanup(new IllegalStateException("refer failure"), false, true); + } + + @Test + public void testReferFailureWithSuccessfulCleanupReleasesOwner() { + RpcConsumerConfig owner = new RpcConsumerConfig(new HugeConfig(Collections.emptyMap()), + "127.0.0.1:1"); + ConsumerFailure control = new ConsumerFailure(owner, new IllegalStateException("refer failure")); + control.disarm(); + try { + Assert.assertSame(control.referFailure, captureFailure(() -> owner.serviceProxy(HELLO))); + Assert.assertGt(0, control.destroyCalls.get()); + Assert.assertFalse(control.retained()); + Assert.assertNull(control.bootstrap.getProxyIns()); + } finally { + control.close(); + } + } + + private void assertReferCleanup(Throwable primary, boolean sameCleanup, boolean fatalCleanup) { + RpcConsumerConfig owner = new RpcConsumerConfig(new HugeConfig(Collections.emptyMap()), + "127.0.0.1:1"); + ConsumerFailure control = new ConsumerFailure(owner, primary); + control.unreferFailure = sameCleanup ? primary : new IllegalStateException("unrefer failure"); + if (fatalCleanup) { + control.clusterFailure = new AssertionError("cluster error"); + } else if (sameCleanup) { + control.clusterFailure = new AssertionError("secondary cluster error"); + } + try { + Throwable thrown = captureFailure(() -> owner.serviceProxy(HELLO)); + Assert.assertNotNull(control.bootstrap.getProxyIns()); + Assert.assertSame(fatalCleanup ? control.clusterFailure : primary, thrown); + if (fatalCleanup) { + Assert.assertSame(primary, thrown.getSuppressed()[0]); + } + Assert.assertGt(0, control.destroyCalls.get()); + Assert.assertTrue(control.retained()); + control.disarm(); + owner.destroy(); + Assert.assertFalse(control.retained()); + Assert.assertNull(control.bootstrap.getProxyIns()); + } finally { + control.close(); + } + } + + @Test + public void testClusterCleanupFailureRetainsOwnerForRetry() { + RpcConsumerConfig owner = new RpcConsumerConfig(new HugeConfig(Collections.emptyMap()), + "127.0.0.1:1"); + ConsumerFailure control = new ConsumerFailure(owner, new IllegalStateException("refer failure")); + control.failUnrefer = false; + control.clusterFailure = new IllegalStateException("cluster failure"); + try { + Assert.assertSame(control.referFailure, captureFailure(() -> owner.serviceProxy(HELLO))); + // Native unRefer swallows a cluster Exception, so the explicit destroy must also run. + Assert.assertGte(2, control.destroyCalls.get()); + Assert.assertTrue(control.retained()); + control.disarm(); + owner.destroy(); + Assert.assertFalse(control.retained()); + } finally { + control.close(); + } + } + + @Test + public void testDestroyContinuesRemainingConsumers() { + RpcConsumerConfig owner = new RpcConsumerConfig(new HugeConfig(Collections.emptyMap()), + "127.0.0.1:1"); + ConsumerFailure first = new ConsumerFailure(owner, new IllegalStateException("first refer")); + ConsumerFailure second = null; + try { + captureFailure(() -> owner.serviceProxy(HELLO)); + second = new ConsumerFailure(owner, new IllegalStateException("second refer")); + captureFailure(() -> owner.serviceProxy(HELLO)); + second.disarm(); + Assert.assertSame(first.unreferFailure, captureFailure(owner::removeAllServiceProxy)); + Assert.assertNull(second.bootstrap.getProxyIns()); + // Unrefer-only cleanup retains owners for the subsequent explicit destroy. + Assert.assertTrue(second.retained()); + Assert.assertSame(first.unreferFailure, captureFailure(owner::destroy)); + Assert.assertTrue(first.retained()); + Assert.assertFalse(second.retained()); + Assert.assertNull(second.bootstrap.getProxyIns()); + first.disarm(); + owner.destroy(); + Assert.assertFalse(first.retained()); + } finally { + first.close(); + if (second != null) { + second.close(); + } + } + } + + @Test + public void testRemoveConsumerRetainsFailedCleanupOwner() { + RpcConsumerConfig owner = new RpcConsumerConfig(new HugeConfig(Collections.emptyMap()), + "127.0.0.1:1"); + ConsumerFailure control = new ConsumerFailure(owner, null); + try { + HelloService proxy = owner.serviceProxy(HELLO); + Assert.assertSame(control.unreferFailure, captureFailure(() -> { + owner.removeServiceProxy(null, HELLO, proxy); + })); + Assert.assertGt(0, control.destroyCalls.get()); + Assert.assertTrue(control.retained()); + Map> configs = Whitebox.getInternalState(owner, "configs"); + Assert.assertSame(control.bootstrap.getConsumerConfig(), configs.get(HELLO)); + control.disarm(); + owner.destroy(); + Assert.assertFalse(control.retained()); + } finally { + control.close(); + } + } + + private static Throwable captureFailure(Runnable action) { + try { + action.run(); + } catch (RuntimeException | Error failure) { + return failure; + } + throw new AssertionError("Expected consumer failure"); + } + + private static final class ConsumerFailure implements AutoCloseable { + + private static final AtomicInteger IDS = new AtomicInteger(); + private final RpcConsumerConfig owner; + private final Throwable referFailure; + private final AtomicInteger destroyCalls = new AtomicInteger(); + private Throwable unreferFailure = new IllegalStateException("unrefer failure"); + private Throwable clusterFailure; + private boolean failUnrefer = true; + private CleanupBootstrap bootstrap; + + private ConsumerFailure(RpcConsumerConfig owner, Throwable referFailure) { + this.owner = owner; + this.referFailure = referFailure; + ConsumerConfig config = new ConsumerConfig<>(); + config.setInterfaceId(HELLO).setUniqueId("cleanup-" + IDS.incrementAndGet()) + .setProtocol("bolt").setDirectUrl("127.0.0.1:1") + .setLazy(true).setCheck(false).setInJVM(false) + .setBootstrap("hugegraph_cleanup_test").setCluster("hugegraph_cleanup_test"); + Map> configs = Whitebox.getInternalState(owner, "configs"); + configs.put(HELLO, config); + consumerFailure = this; + } + + private boolean retained() { + List bootstraps = Whitebox.getInternalState(this.owner, "bootstraps"); + return bootstraps.contains(this.bootstrap); + } + + private void disarm() { + this.failUnrefer = false; + this.clusterFailure = null; + } + + @Override + public void close() { + this.disarm(); + if (this.bootstrap != null) { + this.bootstrap.unRefer(); + this.bootstrap.getCluster().destroy(); + } + consumerFailure = null; + } + } + + @Extension("hugegraph_cleanup_test") + public static final class CleanupBootstrap extends DefaultConsumerBootstrap { + + private final ConsumerFailure control; + + public CleanupBootstrap(ConsumerConfig config) { + super(config); + this.control = consumerFailure; + this.control.bootstrap = this; + } + + @Override + public HelloService refer() { + HelloService proxy = super.refer(); + if (this.control.referFailure != null) { + throwFailure(this.control.referFailure); + } + return proxy; + } + + @Override + public void unRefer() { + if (this.control.failUnrefer) { + throwFailure(this.control.unreferFailure); + } + super.unRefer(); + } + + private static void throwFailure(Throwable failure) { + if (failure instanceof Error) { + throw (Error) failure; + } + throw (RuntimeException) failure; + } + } + + @Extension("hugegraph_cleanup_test") + public static final class CleanupCluster extends FailoverCluster { + + private final ConsumerFailure control; + + public CleanupCluster(ConsumerBootstrap bootstrap) { + super(bootstrap); + this.control = ((CleanupBootstrap) bootstrap).control; + } + + @Override + public void destroy() { + // Use the native cluster lifecycle; control only the cleanup failure boundary. + this.control.destroyCalls.incrementAndGet(); + if (this.control.clusterFailure != null) { + CleanupBootstrap.throwFailure(this.control.clusterFailure); + } + super.destroy(); + } + } + + @Test + public void testExcludeSelfUrl() { + Assert.assertEquals("127.0.0.1:8092,127.0.0.1:8093", + excludeSelfUrl("127.0.0.1:8091,127.0.0.1:8092;127.0.0.1:8093", + "127.0.0.1:8091")); + // Order is kept and duplicates are collapsed + Assert.assertEquals("b:2,a:1", + excludeSelfUrl("b:2,a:1,b:2,self:0", "self:0")); + Assert.assertEquals("a:1", excludeSelfUrl("a:1", "self:0")); + Assert.assertEquals("", excludeSelfUrl("self:0,self:0", "self:0")); + Assert.assertEquals("", excludeSelfUrl("", "self:0")); + } + + @Test + public void testClientDisabledWhenOnlySelfIsRemote() { + Map options = new HashMap<>(); + options.put(RpcOptions.RPC_SERVER_HOST.name(), "127.0.0.1"); + options.put(RpcOptions.RPC_SERVER_PORT.name(), "8091"); + options.put(RpcOptions.RPC_REMOTE_URL.name(), "127.0.0.1:8091"); + RpcClientProvider selfOnly = new RpcClientProvider(new HugeConfig(options)); + Assert.assertFalse(selfOnly.enabled()); + Assert.assertThrows(IllegalArgumentException.class, selfOnly::config, e -> { + Assert.assertContains("add an address other than self service", e.getMessage()); + }); + // Both are no-ops when the client is disabled + selfOnly.unreferAll(); + selfOnly.destroy(); + + options.put(RpcOptions.RPC_REMOTE_URL.name(), "127.0.0.1:8091,127.0.0.1:8092"); + RpcClientProvider withPeer = new RpcClientProvider(new HugeConfig(options)); + try { + Assert.assertTrue(withPeer.enabled()); + // Self is dropped from the direct URL the SOFA consumer is built with + Assert.assertEquals("127.0.0.1:8092", + consumerConfig(withPeer.config(), null, HELLO).getDirectUrl()); + } finally { + withPeer.destroy(); + } + } + + @Test + public void testRpcOptionsRejectInvalidValues() { + assertInvalidOption(RpcOptions.RPC_PROTOCOL.name(), "grpc"); + assertInvalidOption(RpcOptions.RPC_SERIALIZATION.name(), "json"); + assertInvalidOption(RpcOptions.RPC_CLIENT_LOAD_BALANCER.name(), "leastActive"); + assertInvalidOption(RpcOptions.RPC_SERVER_PORT.name(), "-1"); + assertInvalidOption(RpcOptions.RPC_SERVER_TIMEOUT.name(), "0"); + assertInvalidOption(RpcOptions.RPC_CLIENT_RETRIES.name(), "-1"); + assertInvalidOption(RpcOptions.RPC_CLIENT_READ_TIMEOUT.name(), "ten"); + assertInvalidOption(RpcOptions.RPC_LOGGER_IMPL.name(), " "); + + Map options = new HashMap<>(); + options.put(RpcOptions.RPC_SERVER_PORT.name(), "0"); + options.put(RpcOptions.RPC_CLIENT_RETRIES.name(), "0"); + options.put(RpcOptions.RPC_CLIENT_LOAD_BALANCER.name(), "roundRobin"); + HugeConfig config = new HugeConfig(options); + Assert.assertEquals(0, config.get(RpcOptions.RPC_SERVER_PORT)); + Assert.assertEquals(0, config.get(RpcOptions.RPC_CLIENT_RETRIES)); + Assert.assertEquals("roundRobin", config.get(RpcOptions.RPC_CLIENT_LOAD_BALANCER)); + } + + @Test + public void testProviderServiceIds() { + RpcProviderConfig provider = new RpcProviderConfig(); + String plain = provider.addService(HelloService.class, new HelloServiceImpl()); + String graph1 = provider.addService("g1", HelloService.class, new HelloServiceImpl()); + String graph2 = provider.addService("g2", HelloService.class, new HelloServiceImpl()); + Assert.assertEquals(HELLO, plain); + Assert.assertEquals(HELLO + ":g1", graph1); + Assert.assertEquals(HELLO + ":g2", graph2); + Assert.assertEquals(3, provider.configs().size()); + + ProviderConfig config = provider.configs().get(graph1); + Assert.assertEquals(graph1, config.getId()); + Assert.assertEquals("g1", config.getUniqueId()); + Assert.assertEquals(HELLO, config.getInterfaceId()); + Assert.assertEquals("", provider.configs().get(plain).getUniqueId()); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + provider.addService("g1", HelloService.class, new HelloServiceImpl()); + }, e -> { + Assert.assertContains("Not allowed to add service already exist: '" + + graph1 + "'", e.getMessage()); + }); + + provider.removeService(graph1); + Assert.assertFalse(provider.configs().containsKey(graph1)); + Assert.assertThrows(IllegalArgumentException.class, () -> { + provider.removeService(graph1); + }, e -> { + Assert.assertContains("doesn't exist", e.getMessage()); + }); + // A removed id can be registered again + Assert.assertEquals(graph1, provider.addService("g1", HelloService.class, + new HelloServiceImpl())); + + provider.removeAllService(); + Assert.assertTrue(provider.configs().isEmpty()); + } + + @Test + public void testConsumerConfigFromOptions() { + Map options = new HashMap<>(); + options.put(RpcOptions.RPC_CLIENT_READ_TIMEOUT.name(), "7"); + options.put(RpcOptions.RPC_CLIENT_CONNECT_TIMEOUT.name(), "3"); + options.put(RpcOptions.RPC_CLIENT_RECONNECT_PERIOD.name(), "5"); + options.put(RpcOptions.RPC_CLIENT_RETRIES.name(), "2"); + options.put(RpcOptions.RPC_CLIENT_LOAD_BALANCER.name(), "random"); + String remote = "127.0.0.1:8092,127.0.0.1:8093"; + RpcConsumerConfig consumer = new RpcConsumerConfig(new HugeConfig(options), remote); + + ConsumerConfig plain = consumerConfig(consumer, null, HELLO); + Assert.assertEquals(HELLO, plain.getInterfaceId()); + Assert.assertEquals("bolt", plain.getProtocol()); + Assert.assertEquals("hessian2", plain.getSerialization()); + Assert.assertEquals(remote, plain.getDirectUrl()); + Assert.assertEquals(7000, plain.getTimeout()); + Assert.assertEquals(3000, plain.getConnectTimeout()); + Assert.assertEquals(5000, plain.getReconnectPeriod()); + Assert.assertEquals(2, plain.getRetries()); + Assert.assertEquals("random", plain.getLoadBalancer()); + Assert.assertEquals("", plain.getUniqueId()); + Assert.assertNotEquals("fanout", plain.getCluster()); + + // Graph scoped consumers broadcast to every provider of that graph + ConsumerConfig graph = consumerConfig(consumer, "g1", HELLO); + Assert.assertEquals(HELLO + ":g1", graph.getId()); + Assert.assertEquals("g1", graph.getUniqueId()); + Assert.assertEquals("fanout", graph.getCluster()); + + consumer.destroy(); + } + + @Test + public void testConsumerConfigCachedThroughServiceProxy() { + RpcServer server = new RpcServer(config("server-random")); + RpcConsumerConfig consumer = null; + try { + server.config().addService(HelloService.class, new HelloServiceImpl()); + server.config().addService("g1", HelloService.class, new HelloServiceImpl()); + server.config().addService("g2", HelloService.class, new HelloServiceImpl()); + server.exportAll(); + consumer = new RpcConsumerConfig(config("client"), server.host() + ":" + server.port()); + ConsumerConfig plain = referredConfig(consumer, null); + ConsumerConfig graph = referredConfig(consumer, "g1"); + Assert.assertSame(plain, referredConfig(consumer, null)); + Assert.assertSame(graph, referredConfig(consumer, "g1")); + Assert.assertNotSame(graph, referredConfig(consumer, "g2")); + } finally { + if (consumer != null) { + consumer.destroy(); + } + stopServer(server); + } + } + + private static ConsumerConfig referredConfig(RpcConsumerConfig consumer, String graph) { + HelloService proxy = consumer.serviceProxy(graph, HELLO); + Assert.assertEquals("hello tom!", proxy.hello("tom")); + Map> configs = Whitebox.getInternalState(consumer, "configs"); + String id = graph == null ? HELLO : HELLO + ":" + graph; + Assert.assertNotNull(configs.get(id)); + return configs.get(id); + } + + @Test + public void testServerDestroyClosesServices() { + RpcServer server = new RpcServer(config("server-random")); + Assert.assertTrue(server.enabled()); + // Host and port are only known once the server is bound + Assert.assertNull(server.host()); + Assert.assertEquals(0, server.port()); + + CloseableHelloService closeable = new CloseableHelloService(false); + CloseableHelloService failing = new CloseableHelloService(true); + server.config().addService(HelloService.class, closeable); + server.config().addService("g1", HelloService.class, failing); + try { + server.exportAll(); + Assert.assertEquals("127.0.0.1", server.host()); + Assert.assertGt(0, server.port()); + } finally { + server.destroy(); + } + + Assert.assertEquals(1, closeable.closed.get()); + // A failure while closing one service doesn't stop the shutdown + Assert.assertEquals(1, failing.closed.get()); + Assert.assertTrue(server.config().configs().isEmpty()); + } + + private static String excludeSelfUrl(String rpcUrl, String selfUrl) { + return Whitebox.invokeStatic(RpcClientProvider.class, "excludeSelfUrl", + rpcUrl, selfUrl); + } + + private static ConsumerConfig consumerConfig(RpcConsumerConfig consumer, + String graph, String interfaceId) { + return Whitebox.invoke(RpcConsumerConfig.class, + new Class[]{String.class, String.class}, + "consumerConfig", consumer, graph, interfaceId); + } + + private static void assertInvalidOption(String key, String value) { + Map options = new HashMap<>(); + options.put(key, value); + Assert.assertThrows(ConfigException.class, () -> { + new HugeConfig(options); + }, e -> { + Assert.assertContains(key, e.getMessage()); + }); + } + + public static class CloseableHelloService extends HelloServiceImpl + implements AutoCloseable { + + private final boolean failOnClose; + private final AtomicInteger closed = new AtomicInteger(); + + public CloseableHelloService(boolean failOnClose) { + this.failOnClose = failOnClose; + } + + @Override + public void close() { + this.closed.incrementAndGet(); + if (this.failOnClose) { + throw new IllegalStateException("close failure for test"); + } + } + } +} diff --git a/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/ServerClientTest.java b/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/ServerClientTest.java index a1c64c0585..a4c14b07e3 100644 --- a/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/ServerClientTest.java +++ b/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/ServerClientTest.java @@ -20,6 +20,15 @@ import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.rpc.RpcClientProvider; @@ -36,9 +45,12 @@ import org.junit.Test; import com.alipay.sofa.rpc.common.RpcOptions; +import com.alipay.sofa.rpc.config.ProviderConfig; +import com.alipay.sofa.rpc.config.ServerConfig; import com.alipay.sofa.rpc.core.exception.SofaRpcException; import com.alipay.sofa.rpc.core.exception.SofaRpcRuntimeException; import com.google.common.collect.ImmutableMap; +import io.netty.channel.ChannelFuture; public class ServerClientTest extends BaseUnitTest { @@ -57,7 +69,7 @@ public static void clear() { rpcClient.destroy(); } if (rpcServer != null) { - rpcServer.destroy(); + stopServer(rpcServer); } } @@ -67,7 +79,45 @@ public void teardown() { rpcClient.unreferAll(); } if (rpcServer != null) { - rpcServer.unexportAll(); + unexportServer(rpcServer); + } + } + + @Test + public void testUnexportWaitsBeforeReusingListenerPort() throws Exception { + rpcServer.config().addService(HelloService.class, new HelloServiceImpl()); + startServer(rpcServer); + ChannelFuture binding = listener(rpcServer); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + ExecutorService worker = Executors.newSingleThreadExecutor(); + binding.channel().eventLoop().execute(() -> { + entered.countDown(); + try { + E.checkState(release.await(5, TimeUnit.SECONDS), "RPC close barrier timed out"); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(failure); + } + }); + try { + Assert.assertTrue(entered.await(5, TimeUnit.SECONDS)); + Future closing = worker.submit(() -> unexportServer(rpcServer)); + Assert.assertThrows(TimeoutException.class, () -> closing.get(1, TimeUnit.SECONDS)); + Assert.assertTrue(rpcServer.config().configs().isEmpty()); + Assert.assertTrue(binding.channel().isOpen()); + Assert.assertFalse(binding.channel().closeFuture().isDone()); + release.countDown(); + closing.get(5, TimeUnit.SECONDS); + Assert.assertTrue(binding.channel().closeFuture().isSuccess()); + rpcServer.config().addService(HelloService.class, new HelloServiceImpl()); + startServer(rpcServer); + HelloService client = rpcClient.config().serviceProxy(HelloService.class); + Assert.assertEquals("hello tom!", client.hello("tom")); + } finally { + release.countDown(); + worker.shutdownNow(); + Assert.assertTrue(worker.awaitTermination(5, TimeUnit.SECONDS)); } } @@ -406,118 +456,140 @@ public void testFanoutCallServiceWithError() { @Test public void testLoadBalancer() { - // Init 3 servers - HugeConfig server3 = config("server3"); - RpcServer rpcServer3 = new RpcServer(server3); - - HugeConfig server4 = config("server4"); - RpcServer rpcServer4 = new RpcServer(server4); - - HugeConfig server5 = config("server5"); - RpcServer rpcServer5 = new RpcServer(server5); - - GraphHelloServiceImpl s3g1 = new GraphHelloServiceImpl("g1"); - GraphHelloServiceImpl s4g1 = new GraphHelloServiceImpl("g1"); - GraphHelloServiceImpl s5g1 = new GraphHelloServiceImpl("g1"); - - rpcServer3.config().addService(HelloService.class, s3g1); - rpcServer4.config().addService(HelloService.class, s4g1); - rpcServer5.config().addService(HelloService.class, s5g1); - - startServer(rpcServer3); - startServer(rpcServer4); - startServer(rpcServer5); - - // Test LB "consistentHash" - HugeConfig clientLB = config("client-lb"); - RpcClientProvider rpcClientCHash = new RpcClientProvider(clientLB); - HelloService cHash = rpcClientCHash.config() - .serviceProxy(HelloService.class); - - Assert.assertEquals("g1: load", cHash.echo("load")); - Assert.assertEquals(16.8, cHash.sum(10, 6.8), 0.00000001d); - Assert.assertEquals(16.8, s3g1.result() + s4g1.result() + s5g1.result(), - 0.00000001d); - - Assert.assertEquals("g1: load", cHash.echo("load")); - Assert.assertEquals(16.8, cHash.sum(10, 6.8), 0.00000001d); - Assert.assertEquals(16.8, s3g1.result() + s4g1.result() + s5g1.result(), - 0.00000001d); - - Assert.assertEquals("g1: load", cHash.echo("load")); - Assert.assertEquals(16.8, cHash.sum(10, 6.8), 0.00000001d); - Assert.assertEquals(16.8, s3g1.result() + s4g1.result() + s5g1.result(), - 0.00000001d); - - s3g1.resetResult(); - s4g1.resetResult(); - s5g1.resetResult(); - - // Test LB "roundRobin" - String lbKey = org.apache.hugegraph.config.RpcOptions.RPC_CLIENT_LOAD_BALANCER.name(); - clientLB.setProperty(lbKey, "roundRobin"); - RpcClientProvider rpcClientRound = new RpcClientProvider(clientLB); - HelloService round = rpcClientRound.config() - .serviceProxy(HelloService.class); - - Assert.assertEquals("g1: load", round.echo("load")); - Assert.assertEquals(1.1, round.sum(1, 0.1), 0.00000001d); - Assert.assertEquals(1.1, s3g1.result() + s4g1.result() + s5g1.result(), - 0.00000001d); - - Assert.assertEquals("g1: load", round.echo("load")); - Assert.assertEquals(1.1, round.sum(1, 0.1), 0.00000001d); - Assert.assertEquals(2.2, s3g1.result() + s4g1.result() + s5g1.result(), - 0.00000001d); - - Assert.assertEquals("g1: load", round.echo("load")); - Assert.assertEquals(1.1, round.sum(1, 0.1), 0.00000001d); - Assert.assertEquals(3.3, s3g1.result() + s4g1.result() + s5g1.result(), - 0.00000001d); - - s3g1.resetResult(); - s4g1.resetResult(); - s5g1.resetResult(); - - // Test LB "random" - clientLB.setProperty(lbKey, "random"); - RpcClientProvider rpcClientRandom = new RpcClientProvider(clientLB); - HelloService random = rpcClientRandom.config() - .serviceProxy(HelloService.class); - - Assert.assertEquals("g1: load", random.echo("load")); - Assert.assertEquals(1.1, random.sum(1, 0.1), 0.00000001d); - Assert.assertEquals(1.1, s3g1.result() + s4g1.result() + s5g1.result(), - 0.00000001d); - - Assert.assertEquals("g1: load", random.echo("load")); - Assert.assertEquals(1.1, random.sum(1, 0.1), 0.00000001d); - double sum = s3g1.result() + s4g1.result() + s5g1.result(); - Assert.assertTrue(2.2 == sum || 1.1 == sum); - - Assert.assertEquals("g1: load", random.echo("load")); - Assert.assertEquals(1.1, random.sum(1, 0.1), 0.00000001d); - double sum2 = s3g1.result() + s4g1.result() + s5g1.result(); - Assert.assertTrue(sum == sum2 || sum + 1.1 == sum2); - - for (int i = 0; i < 9; i++) { - Assert.assertEquals(1.1, random.sum(1, 0.1), 0.00000001d); + List cleanup = new ArrayList<>(); + Throwable failure = null; + try { + // Init 3 servers + HugeConfig server3 = config("server3"); + RpcServer rpcServer3 = new RpcServer(server3); + cleanup.add(() -> stopServer(rpcServer3)); + + HugeConfig server4 = config("server4"); + RpcServer rpcServer4 = new RpcServer(server4); + cleanup.add(() -> stopServer(rpcServer4)); + + HugeConfig server5 = config("server5"); + RpcServer rpcServer5 = new RpcServer(server5); + cleanup.add(() -> stopServer(rpcServer5)); + + GraphHelloServiceImpl s3g1 = new GraphHelloServiceImpl("g1"); + GraphHelloServiceImpl s4g1 = new GraphHelloServiceImpl("g1"); + GraphHelloServiceImpl s5g1 = new GraphHelloServiceImpl("g1"); + + rpcServer3.config().addService(HelloService.class, s3g1); + rpcServer4.config().addService(HelloService.class, s4g1); + rpcServer5.config().addService(HelloService.class, s5g1); + + startServer(rpcServer3); + startServer(rpcServer4); + startServer(rpcServer5); + + // Test LB "consistentHash" + HugeConfig clientLB = config("client-lb"); + RpcClientProvider rpcClientCHash = new RpcClientProvider(clientLB); + cleanup.add(rpcClientCHash::destroy); + HelloService cHash = rpcClientCHash.config() + .serviceProxy(HelloService.class); + + Assert.assertEquals("g1: load", cHash.echo("load")); + Assert.assertEquals(16.8, cHash.sum(10, 6.8), 0.00000001d); + Assert.assertEquals(16.8, s3g1.result() + s4g1.result() + s5g1.result(), + 0.00000001d); + + Assert.assertEquals("g1: load", cHash.echo("load")); + Assert.assertEquals(16.8, cHash.sum(10, 6.8), 0.00000001d); + Assert.assertEquals(16.8, s3g1.result() + s4g1.result() + s5g1.result(), + 0.00000001d); + + Assert.assertEquals("g1: load", cHash.echo("load")); + Assert.assertEquals(16.8, cHash.sum(10, 6.8), 0.00000001d); + Assert.assertEquals(16.8, s3g1.result() + s4g1.result() + s5g1.result(), + 0.00000001d); + + s3g1.resetResult(); + s4g1.resetResult(); + s5g1.resetResult(); + + // Test LB "roundRobin" + String lbKey = org.apache.hugegraph.config.RpcOptions.RPC_CLIENT_LOAD_BALANCER.name(); + clientLB.setProperty(lbKey, "roundRobin"); + RpcClientProvider rpcClientRound = new RpcClientProvider(clientLB); + cleanup.add(rpcClientRound::destroy); + HelloService round = rpcClientRound.config() + .serviceProxy(HelloService.class); + + Assert.assertEquals("g1: load", round.echo("load")); + Assert.assertEquals(1.1, round.sum(1, 0.1), 0.00000001d); + Assert.assertEquals(1.1, s3g1.result() + s4g1.result() + s5g1.result(), + 0.00000001d); + + Assert.assertEquals("g1: load", round.echo("load")); + Assert.assertEquals(1.1, round.sum(1, 0.1), 0.00000001d); + Assert.assertEquals(2.2, s3g1.result() + s4g1.result() + s5g1.result(), + 0.00000001d); + + Assert.assertEquals("g1: load", round.echo("load")); + Assert.assertEquals(1.1, round.sum(1, 0.1), 0.00000001d); + Assert.assertEquals(3.3, s3g1.result() + s4g1.result() + s5g1.result(), + 0.00000001d); + + s3g1.resetResult(); + s4g1.resetResult(); + s5g1.resetResult(); + + // Test LB "random" + clientLB.setProperty(lbKey, "random"); + RpcClientProvider rpcClientRandom = new RpcClientProvider(clientLB); + cleanup.add(rpcClientRandom::destroy); + HelloService random = rpcClientRandom.config() + .serviceProxy(HelloService.class); + + // Random routing can legitimately leave some providers unvisited. + GraphHelloServiceImpl[] services = {s3g1, s4g1, s5g1}; + int visited = 0; + for (int i = 0; i < 12; i++) { + if (i < 3) { + Assert.assertEquals("g1: load", random.echo("load")); + } + Assert.assertEquals(1.1, random.sum(1, 0.1), 0.00000001d); + int currentVisited = 0; + for (GraphHelloServiceImpl service : services) { + double result = service.result(); + Assert.assertTrue(result == 0.0 || result == 1.1); + if (result != 0.0) { + currentVisited++; + } + } + Assert.assertTrue(currentVisited > 0); + Assert.assertTrue(currentVisited >= visited && currentVisited <= visited + 1); + visited = currentVisited; + } + + s3g1.resetResult(); + s4g1.resetResult(); + s5g1.resetResult(); + } catch (RuntimeException | Error e) { + failure = e; + throw e; + } finally { + RuntimeException cleanupFailure = null; + for (int i = cleanup.size() - 1; i >= 0; i--) { + try { + cleanup.get(i).run(); + } catch (RuntimeException e) { + if (failure != null) { + failure.addSuppressed(e); + } else if (cleanupFailure == null) { + cleanupFailure = e; + } else { + cleanupFailure.addSuppressed(e); + } + } + } + if (cleanupFailure != null) { + throw cleanupFailure; + } } - Assert.assertEquals(3.3, s3g1.result() + s4g1.result() + s5g1.result(), - 0.00000001d); - - s3g1.resetResult(); - s4g1.resetResult(); - s5g1.resetResult(); - - // Destroy all - rpcClientCHash.destroy(); - rpcClientRound.destroy(); - rpcClientRandom.destroy(); - - stopServer(rpcServer3); - stopServer(rpcServer4); - stopServer(rpcServer5); } @Test @@ -558,7 +630,7 @@ public void testServiceProxy() { @Test public void testAddServiceMultiTimesOfSameService() { - RpcServer rpcServerExport = new RpcServer(config(true)); + RpcServer rpcServerExport = new RpcServer(config("server-random")); rpcServerExport.config().addService(HelloService.class, new HelloServiceImpl()); @@ -578,32 +650,70 @@ public void testAddServiceMultiTimesOfSameService() { @Test public void testExportMultiTimesOfSameServer() { - RpcServer rpcServerExport = new RpcServer(config(true)); - rpcServerExport.config().addService(HelloService.class, - new HelloServiceImpl()); - rpcServerExport.exportAll(); - rpcServerExport.exportAll(); - - stopServer(rpcServerExport); + RpcServer rpcServerExport = new RpcServer(config("server-random")); + try { + rpcServerExport.config().addService(HelloService.class, new HelloServiceImpl()); + rpcServerExport.exportAll(); + rpcServerExport.exportAll(); + Assert.assertEquals(1, rpcServerExport.config().configs().get( + HelloService.class.getName()).getServer().size()); + } finally { + stopServer(rpcServerExport); + } } @Test public void testExportMultiTimesOfSameService() { - RpcServer rpcServerExport = new RpcServer(config(true)); - rpcServerExport.config().addService(HelloService.class, - new HelloServiceImpl()); - rpcServerExport.exportAll(); - - rpcServerExport.config().addService("graph", HelloService.class, - new HelloServiceImpl()); - rpcServerExport.exportAll(); + RpcServer rpcServerExport = new RpcServer(config("server-random")); + try { + rpcServerExport.config().addService(HelloService.class, new HelloServiceImpl()); + rpcServerExport.exportAll(); + rpcServerExport.config().addService("graph", HelloService.class, new HelloServiceImpl()); + rpcServerExport.exportAll(); + rpcServerExport.exportAll(); + for (ProviderConfig provider : rpcServerExport.config().configs().values()) { + Assert.assertEquals(1, provider.getServer().size()); + } + } finally { + stopServer(rpcServerExport); + } + } - stopServer(rpcServerExport); + @Test + public void testExportRetriesWithoutDuplicatingConfiguredServers() { + RpcServer server = new RpcServer(config("server-random")); + AtomicInteger exports = new AtomicInteger(); + IllegalStateException failure = new IllegalStateException("export failed before binding"); + ProviderConfig provider = new ProviderConfig<>() { + @Override + public void export() { + if (exports.incrementAndGet() == 1) { + throw failure; + } + } + }; + // Sofa equality ignores settings such as serialization; preserve distinct config objects. + ServerConfig existing = new ServerConfig().setProtocol("bolt").setHost("127.0.0.1") + .setPort(0).setSerialization("java"); + provider.setServer(existing); + server.config().configs().put(HelloService.class.getName(), provider); + try { + Assert.assertThrows(IllegalStateException.class, server::exportAll, + caught -> Assert.assertSame(failure, caught)); + server.exportAll(); + server.exportAll(); + Assert.assertEquals(3, exports.get()); + Assert.assertEquals(2, provider.getServer().size()); + Assert.assertSame(existing, provider.getServer().get(0)); + Assert.assertSame(Whitebox.getInternalState(server, "serverConfig"), provider.getServer().get(1)); + } finally { + stopServer(server); + } } @Test public void testExportNoneService() { - RpcServer rpcServerNoneService = new RpcServer(config(true)); + RpcServer rpcServerNoneService = new RpcServer(config("server-random")); // Will be ignored if none service added rpcServerNoneService.exportAll(); @@ -611,57 +721,81 @@ public void testExportNoneService() { stopServer(rpcServerNoneService); } + private static RpcClientProvider clientFor(RpcServer server) { + HugeConfig config = config(false); + String remoteUrlKey = org.apache.hugegraph.config.RpcOptions.RPC_REMOTE_URL.name(); + config.setProperty(remoteUrlKey, server.host() + ":" + server.port()); + return new RpcClientProvider(config); + } + @Test - public void testUnexportService() { - RpcServer rpcServerUnexport = new RpcServer(config(true)); + public void testTemporaryServerDoesNotShareMainListener() { + rpcServer.config().addService(HelloService.class, new HelloServiceImpl()); + startServer(rpcServer); + RpcServer temporary = new RpcServer(config("server-random")); + try { + temporary.config().addService(HelloService.class, new HelloServiceImpl()); + startServer(temporary); + Assert.assertNotEquals(rpcServer.port(), temporary.port()); + } finally { + stopServer(temporary); + } + HelloService client = rpcClient.config().serviceProxy(HelloService.class); + Assert.assertEquals("hello tom!", client.hello("tom")); + } + @Test + public void testUnexportService() { + RpcServer rpcServerUnexport = new RpcServer(config("server-random")); RpcProviderConfig serverConfig = rpcServerUnexport.config(); String service = serverConfig.addService(HelloService.class, new HelloServiceImpl()); + String graphService = serverConfig.addService("graph", HelloService.class, + new GraphHelloServiceImpl("graph")); rpcServerUnexport.exportAll(); - - RpcConsumerConfig clientConfig = rpcClient.config(); - HelloService client = clientConfig.serviceProxy(HelloService.class); - - Assert.assertEquals("hello tom!", client.hello("tom")); - - rpcServerUnexport.unexport(service); - - Assert.assertThrows(SofaRpcException.class, () -> { - client.hello("tom"); - }); - - stopServer(rpcServerUnexport); + RpcClientProvider isolatedClient = clientFor(rpcServerUnexport); + try { + HelloService client = isolatedClient.config().serviceProxy(HelloService.class); + HelloService graphClient = isolatedClient.config().serviceProxy("graph", HelloService.class); + ChannelFuture binding = listener(rpcServerUnexport); + Assert.assertEquals("hello tom!", client.hello("tom")); + rpcServerUnexport.unexport(service); + Assert.assertThrows(SofaRpcException.class, () -> client.hello("tom")); + Assert.assertEquals("graph: hello tom!", graphClient.hello("tom")); + Assert.assertTrue(binding.channel().isOpen()); + rpcServerUnexport.unexport(graphService); + Assert.assertTrue(binding.channel().closeFuture().awaitUninterruptibly(5, TimeUnit.SECONDS)); + Assert.assertTrue(binding.channel().closeFuture().isSuccess()); + Assert.assertTrue(serverConfig.configs().isEmpty()); + Assert.assertThrows(SofaRpcException.class, () -> graphClient.hello("tom")); + } finally { + isolatedClient.destroy(); + stopServer(rpcServerUnexport); + } } @Test public void testUnexportAllService() { - RpcServer rpcServerUnexport = new RpcServer(config(true)); - + RpcServer rpcServerUnexport = new RpcServer(config("server-random")); RpcProviderConfig serverConfig = rpcServerUnexport.config(); serverConfig.addService(HelloService.class, new HelloServiceImpl()); serverConfig.addService("graph", HelloService.class, new GraphHelloServiceImpl("graph")); rpcServerUnexport.exportAll(); - - RpcConsumerConfig clientConfig = rpcClient.config(); - HelloService client = clientConfig.serviceProxy(HelloService.class); - HelloService clientG = clientConfig.serviceProxy("graph", - HelloService.class); - - Assert.assertEquals("hello tom!", client.hello("tom")); - Assert.assertEquals("graph: hello tom!", clientG.hello("tom")); - - rpcServerUnexport.unexportAll(); - - Assert.assertThrows(SofaRpcException.class, () -> { - client.hello("tom"); - }); - Assert.assertThrows(SofaRpcException.class, () -> { - clientG.hello("tom"); - }); - - stopServer(rpcServerUnexport); + RpcClientProvider isolatedClient = clientFor(rpcServerUnexport); + try { + RpcConsumerConfig clientConfig = isolatedClient.config(); + HelloService client = clientConfig.serviceProxy(HelloService.class); + HelloService clientG = clientConfig.serviceProxy("graph", HelloService.class); + Assert.assertEquals("hello tom!", client.hello("tom")); + Assert.assertEquals("graph: hello tom!", clientG.hello("tom")); + rpcServerUnexport.unexportAll(); + Assert.assertThrows(SofaRpcException.class, () -> client.hello("tom")); + Assert.assertThrows(SofaRpcException.class, () -> clientG.hello("tom")); + } finally { + isolatedClient.destroy(); + stopServer(rpcServerUnexport); + } } @Test @@ -674,6 +808,108 @@ public void testUnexportNotExistService() { }); } + @Test + public void testRemoveServiceUsesImplementationIdentity() throws Exception { + RpcServer server = new RpcServer(config("server-random")); + GraphHelloServiceImpl original = new GraphHelloServiceImpl("graph"); + RpcProviderConfig providers = server.config(); + String serviceId = providers.addService("graph", HelloService.class, original); + PausedProvider provider = new PausedProvider(); + provider.setId(serviceId).setUniqueId("graph"); + provider.setInterfaceId(HelloService.class.getName()).setRef(original); + providers.configs().put(serviceId, provider); + server.exportAll(); + RpcClientProvider client = clientFor(server); + ExecutorService worker = Executors.newFixedThreadPool(2); + try { + HelloService proxy = client.config().serviceProxy("graph", HelloService.class); + Assert.assertFalse(providers.removeService(serviceId, new GraphHelloServiceImpl("graph"))); + Assert.assertEquals("graph: hello tom!", proxy.hello("tom")); + provider.armed = true; + Future exporting = worker.submit(server::exportAll); + Assert.assertTrue(provider.entered.await(5, TimeUnit.SECONDS)); + CountDownLatch removing = new CountDownLatch(1); + Future removal = worker.submit(() -> { + removing.countDown(); + return providers.removeService(serviceId, original); + }); + Assert.assertTrue(removing.await(5, TimeUnit.SECONDS)); + Assert.assertThrows(TimeoutException.class, () -> removal.get(1, TimeUnit.SECONDS)); + Assert.assertSame(provider, providers.configs().get(serviceId)); + provider.release.countDown(); + exporting.get(5, TimeUnit.SECONDS); + Assert.assertTrue(removal.get(5, TimeUnit.SECONDS)); + Assert.assertFalse(providers.configs().containsKey(serviceId)); + GraphHelloServiceImpl replacement = new GraphHelloServiceImpl("replacement"); + providers.addService("graph", HelloService.class, replacement); + server.exportAll(); + Assert.assertFalse(providers.removeService(serviceId, original)); + Assert.assertSame(replacement, providers.configs().get(serviceId).getRef()); + Assert.assertEquals("replacement: hello tom!", proxy.hello("tom")); + } finally { + provider.release.countDown(); + worker.shutdownNow(); + try { + Assert.assertTrue(worker.awaitTermination(5, TimeUnit.SECONDS)); + } finally { + client.destroy(); + stopServer(server); + } + } + } + + private static class PausedProvider extends ProviderConfig { + + private final CountDownLatch entered = new CountDownLatch(1); + private final CountDownLatch release = new CountDownLatch(1); + private volatile boolean armed; + + @Override + public ProviderConfig setTimeout(int timeout) { + if (this.armed) { + this.entered.countDown(); + try { + E.checkState(this.release.await(5, TimeUnit.SECONDS), "RPC export barrier timed out"); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(failure); + } + this.armed = false; + } + return super.setTimeout(timeout); + } + } + + @Test + public void testRemoveServiceProxyUsesProxyIdentity() { + RpcServer server = new RpcServer(config("server-random")); + server.config().addService("graph", HelloService.class, new GraphHelloServiceImpl("graph")); + server.config().addService("other", HelloService.class, new GraphHelloServiceImpl("other")); + server.exportAll(); + RpcClientProvider client = clientFor(server); + try { + RpcConsumerConfig consumers = client.config(); + HelloService original = consumers.serviceProxy("graph", HelloService.class); + HelloService second = consumers.serviceProxy("graph", HelloService.class); + HelloService other = consumers.serviceProxy("other", HelloService.class); + Assert.assertNotSame(original, second); + Assert.assertTrue(consumers.removeServiceProxy("graph", HelloService.class.getName(), original)); + Assert.assertThrows(SofaRpcRuntimeException.class, () -> original.hello("tom")); + Assert.assertEquals("graph: hello tom!", second.hello("tom")); + Assert.assertFalse(consumers.removeServiceProxy("graph", HelloService.class.getName(), original)); + Assert.assertTrue(consumers.removeServiceProxy("graph", HelloService.class.getName(), second)); + List bootstraps = Whitebox.getInternalState(consumers, "bootstraps"); + Assert.assertEquals(1, bootstraps.size()); + HelloService replacement = consumers.serviceProxy("graph", HelloService.class); + Assert.assertFalse(consumers.removeServiceProxy("graph", HelloService.class.getName(), original)); + Assert.assertEquals("graph: hello tom!", replacement.hello("tom")); + Assert.assertEquals("other: hello tom!", other.hello("tom")); + } finally { + client.destroy(); + stopServer(server); + } + } + @Test public void testServerDisabled() { HugeConfig clientConf = config(false); diff --git a/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java index a19b5522c8..c006ab13bf 100644 --- a/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-commons/hugegraph-rpc/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -24,7 +24,8 @@ @Suite.SuiteClasses({ VersionTest.class, ExceptionTest.class, - ServerClientTest.class + ServerClientTest.class, + RpcConfigTest.class }) public class UnitTestSuite { } diff --git a/hugegraph-commons/pom.xml b/hugegraph-commons/pom.xml index b9e780bd32..a3ef4983df 100644 --- a/hugegraph-commons/pom.xml +++ b/hugegraph-commons/pom.xml @@ -89,19 +89,15 @@ - - - 1.7.0 UTF-8 ${project.basedir}/.. - 1.8 - 1.8 + 11 2.18.0 1.10 - 2.8.0 + 2.10.1 1.9.4 3.2.2 - 3.12.0 + 3.18.0 2.7 1.13 30.0-jre @@ -110,7 +106,6 @@ 3.28.0-GA 3.0.3 3.0.3 - 2.14.0-rc1 4.13.1 4.1.0 4.0.0-RC2 @@ -132,8 +127,6 @@ maven-compiler-plugin - ${compiler.source} - ${compiler.target} 500 @@ -193,7 +186,6 @@ org.jacoco jacoco-maven-plugin - 0.8.7 pre-unit-test @@ -288,9 +280,12 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 ${skipCommonsTests} + + @{argLine} + "@${project.parent.basedir}/../hugegraph-server/hugegraph-test/conf/jvm-test-module.options" + diff --git a/hugegraph-pd/Dockerfile b/hugegraph-pd/Dockerfile index 655860d407..c7050826ac 100644 --- a/hugegraph-pd/Dockerfile +++ b/hugegraph-pd/Dockerfile @@ -18,7 +18,7 @@ # Dockerfile for HugeGraph PD # 1st stage: build source code -FROM --platform=$BUILDPLATFORM maven:3.9.0-eclipse-temurin-11 AS build +FROM --platform=$BUILDPLATFORM maven:3.9.16-eclipse-temurin-17 AS build WORKDIR /pkg @@ -35,7 +35,7 @@ RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sh # 2nd stage: runtime env # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13 -FROM eclipse-temurin:11-jre-jammy +FROM eclipse-temurin:17-jre-jammy LABEL maintainer="HugeGraph Docker Maintainers " diff --git a/hugegraph-pd/README.md b/hugegraph-pd/README.md index 5050025565..a5dc5d6f13 100644 --- a/hugegraph-pd/README.md +++ b/hugegraph-pd/README.md @@ -3,6 +3,8 @@ [![License](https://img.shields.io/badge/license-Apache%202-0E78BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) [![Version](https://img.shields.io/badge/version-1.7.0-blue)](https://github.com/apache/hugegraph) + + ## Overview HugeGraph PD (Placement Driver) is a meta server that provides cluster management and coordination services for HugeGraph distributed deployments. It serves as the central control plane responsible for: @@ -36,8 +38,8 @@ For detailed architecture and design, see [Architecture Documentation](docs/arch ### Prerequisites -- **Java**: 11 or higher -- **Maven**: 3.5 or higher +- **Java**: 17 (currently the only supported release) +- **Maven**: 3.6.3 or higher - **Disk Space**: At least 1GB for PD data directory ### Build @@ -81,11 +83,18 @@ bin/stop-hugegraph-pd.sh bin/start-hugegraph-pd.sh [-g GC_TYPE] [-j "JVM_OPTIONS"] [-y ENABLE_OTEL] [-d DAEMON] ``` -- `-g`: GC type (`g1` or `ZGC`, default: `g1`) +- `-g`: Explicit GC selection (`g1`/`G1` or `zgc`/`ZGC`). Without `-g`, JVM defaults or the collector + set in `JAVA_OPTIONS` apply. Explicit `-g g1` selects G1 even when JVM ergonomics would choose Serial GC; + unknown values are rejected. When using explicit `-g`, do not select another collector in `JAVA_OPTIONS` + or `-j`; conflicting selections are rejected by the JVM. - `-j`: Custom JVM options (e.g., `-j "-Xmx4g -Xms4g"`) - `-y`: Enable OpenTelemetry tracing (`true` or `false`, default: `false`) - `-d`: Daemon mode (`true` = daemon, `false` = foreground; default: `true`). Set to `false` when running under Docker or a process supervisor so the container exits if Java dies. +The launcher always loads `bin/jvm-module.options`, including with custom `JAVA_OPTIONS` or `-j`. +Keep this file in the distribution: it grants the Java 17 access required by Hessian Raft serialization; +startup fails if the file is missing or unreadable. Direct Java launches must include `@bin/jvm-module.options`. + ### Configuration Key configuration file: `conf/application.yml` @@ -299,7 +308,7 @@ docker/docker-compose-3pd-3store-3server.yml PD exposes metrics via REST API at: - Health check: `http://:8620/actuator/health` -- Liveness: `http://:8620/v1/health` (REST listener is up) +- Liveness: `http://:8620/v1/health` (`503` for terminal Raft error or shutdown; elections and temporary quorum loss remain healthy) - Readiness: `http://:8620/v1/ready` (`200` only while the PD sees a raft leader) - Metrics: `http://:8620/actuator/metrics` diff --git a/hugegraph-pd/docs/api-reference.md b/hugegraph-pd/docs/api-reference.md index 8fbd28a651..35d70fadd4 100644 --- a/hugegraph-pd/docs/api-reference.md +++ b/hugegraph-pd/docs/api-reference.md @@ -809,9 +809,13 @@ Two unauthenticated endpoints are meant for probes and startup gates: | Endpoint | Meaning | Status | |----------|---------|--------| -| `GET /v1/health` | Liveness: the REST listener is up. Does not consult raft. | always `200` | +| `GET /v1/health` | Liveness: the REST listener is up and Raft has no terminal error. Empty text body. | `503` for `STATE_ERROR` or `STATE_SHUTDOWN`, `200` otherwise | | `GET /v1/ready` | Readiness: the raft node is active and sees a leader, so this PD is inside a quorum. | `200` when ready, `503` otherwise | +The health probe reads Raft state only; it does not verify metadata-store initialization. +Metadata open failures propagate through startup; bounded LOCK retry remains tracked in +[#3226](https://github.com/apache/hugegraph/issues/3226). + ```bash curl -i http://localhost:8620/v1/ready ``` @@ -829,6 +833,11 @@ A follower reports `"state": "STATE_FOLLOWER"` with `"isLeader": false`. When th The answer is served from state the raft callbacks maintain rather than from the raft node, so it stays prompt while an election is running and never waits on the node lock. `state` is therefore the last change raft announced. A PD reports `STATE_UNINITIALIZED` with `"ready": false` from process start until its first raft callback, which is the ordinary startup window before a quorum first forms, and jraft emits no callback for candidacy or leadership transfer, so a candidate reports `STATE_FOLLOWER` with `"ready": false`. +After a fatal snapshot error, `/v1/ready` retains `STATE_ERROR` even after the leader +steps down, and `/v1/health` returns `503`. Fix the underlying disk or storage +problem and restart the affected PD: freeing disk space alone does not restart +JRaft's terminal state machine. + Point Kubernetes readiness probes, `depends_on` healthchecks and any "wait for PD" script at `/v1/ready`; keep liveness probes on `/v1/health` so a PD that merely lost its leader is not restarted. Match on the body rather than on the status code alone. A PD that predates this endpoint does not reliably answer `404` for it: `RestAuthentication` refuses a request it does not exclude by writing an error envelope, and as of 1.7.0 it does so without setting a status, so an unknown path answers `200` with `{"status":-1,"error":"Unauthorized!"}`. A status-only probe therefore reads such a PD as ready. The body match holds whichever status a refusal carries: a shell gate should use `curl -fsS http://:8620/v1/ready | grep -q '"ready":true'`, and a Kubernetes `httpGet` probe should be paired with a PD image that carries the endpoint. @@ -871,6 +880,19 @@ A cluster has lost its quorum when `sum(hg_raft_leader) == 0` or when `hg_raft_h Do not aggregate `hg_raft_alive_peers` across instances: it is `NaN` on every node but the leader, and one `NaN` sample turns the result of `sum()` or `avg()` into `NaN` as well. Select the leader's series instead, for example `hg_raft_alive_peers and on(instance) (hg_raft_leader == 1)`. +#### Store state gauges + +Exported on `/actuator/prometheus` for each store registered in PD, from the store and shard group metadata PD keeps: + +| Gauge | Value | +|-------|-------| +| `hg_store_state{store="",state=""}` | `1` for the store's current state, `0` for the others. `state` is one of `Unknown`, `Pending`, `Up`, `Offline`, `Exiting`, `Tombstone` | +| `hg_store_leader_count{store=""}` | Number of shard groups whose leader is on the store | + +A new store's series can take one extra scrape to appear, and they disappear once the store is removed from PD. Every PD member reads the same replicated metadata, so select one instance, such as the leader, before aggregating: `hg_store_leader_count and on(instance) (hg_raft_leader == 1)`. + +`hg_store_leader_count` shows where leadership sits during and after a Store recovery: a rebuilt store reports `0` until leaders move back to it, either through `/v1/task/balanceLeaders` or by raft elections. + ### Partition API #### List Partitions diff --git a/hugegraph-pd/docs/architecture.md b/hugegraph-pd/docs/architecture.md index 080189be95..6fd56dd212 100644 --- a/hugegraph-pd/docs/architecture.md +++ b/hugegraph-pd/docs/architecture.md @@ -54,7 +54,7 @@ HugeGraph PD (Placement Driver) is the control plane for HugeGraph distributed d - **Storage**: RocksDB for persistent metadata - **Communication**: gRPC with Protocol Buffers - **Framework**: Spring Boot for REST APIs and dependency injection -- **Language**: Java 11+ +- **Language**: Java 17 (currently supported release) ## Module Architecture diff --git a/hugegraph-pd/docs/configuration.md b/hugegraph-pd/docs/configuration.md index 4e60d8e8be..c2d6eecc9f 100644 --- a/hugegraph-pd/docs/configuration.md +++ b/hugegraph-pd/docs/configuration.md @@ -119,6 +119,9 @@ raft: |-----------|------|---------|-------------| | `raft.address` | String | `127.0.0.1:8610` | Raft service address for this PD node. Format: `:`. Must be unique across all PD nodes. | | `raft.peers-list` | String | `127.0.0.1:8610` | Comma-separated list of all PD nodes' Raft addresses. Used for cluster formation and leader election. | +| `raft.rpc-timeout` | Integer | `10000` | Timeout of a Raft RPC between PD nodes, in milliseconds. It is also the deadline of the ReadIndex wait PD runs before it reads an id counter: `getId` and `getCId` fail when the leader cannot confirm and apply a ReadIndex in this time, for example while a new leader is still committing its first entry. | +| `raft.rpc-connect-timeout` | Integer | `1000` | Timeout for opening a connection to a peer, in milliseconds. A candidate opens its connections one peer at a time while it holds the Raft node lock, so an election waits this long for each peer that accepts the connection but does not answer (a stopped process, a lost host). It is the timeout of the Ping that opens a connection; requests on a connection that is already open use `raft.rpc-timeout`. | +| `raft.rpc-install-snapshot-timeout` | Integer | `0` | Timeout for sending a snapshot to a follower that fell behind the log, in milliseconds. It bounds the transfer of the whole metadata store. `0` uses `raft.rpc-timeout`, the value this timeout had before it became a separate option; raise it if large snapshots time out. | **Critical Rules**: 1. `raft.address` must be unique for each PD node diff --git a/hugegraph-pd/docs/development.md b/hugegraph-pd/docs/development.md index 514bd989a1..61ed03f3b5 100644 --- a/hugegraph-pd/docs/development.md +++ b/hugegraph-pd/docs/development.md @@ -18,10 +18,10 @@ This document provides comprehensive guidance for developing, testing, and contr Ensure you have the following tools installed: -| Tool | Minimum Version | Recommended | Purpose | +| Tool | Version Requirement | Recommended | Purpose | |------|----------------|-------------|---------| -| **JDK** | 11 | 11 or 17 | Java runtime and compilation | -| **Maven** | 3.5.0 | 3.8+ | Build tool and dependency management | +| **JDK** | 17 (only supported release) | 17 LTS | Java runtime and compilation | +| **Maven** | 3.6.3+ | 3.8+ | Build tool and dependency management | | **Git** | 2.0+ | Latest | Version control | | **IDE** | N/A | IntelliJ IDEA | Development environment | @@ -30,11 +30,11 @@ Ensure you have the following tools installed: ```bash # Check Java version java -version -# Expected: openjdk version "11.0.x" or later +# Expected: openjdk version "17.0.x" # Check Maven version mvn -version -# Expected: Apache Maven 3.5.0 or later +# Expected: Apache Maven 3.6.3 or later # Check Git version git --version @@ -77,8 +77,8 @@ Required for Lombok support: #### Configure JDK 1. **File → Project Structure → Project** -2. **Project SDK**: Select JDK 11 or 17 -3. **Project language level**: 11 +2. **Project SDK**: Select JDK 17 +3. **Project language level**: 17 4. **Apply** and **OK** ## Building from Source @@ -196,10 +196,10 @@ mvn test -pl hugegraph-pd/hg-pd-test -am -P pd-rest-test ```bash # Run specific test class -mvn -pl hugegraph-pd/hg-pd-test test -Dtest=PartitionServiceTest -DfailIfNoTests=false +mvn -pl hugegraph-pd/hg-pd-test test -Dtest=PartitionServiceTest # Run specific test method -mvn -pl hugegraph-pd/hg-pd-test test -Dtest=PartitionServiceTest#testSplitPartition -DfailIfNoTests=false +mvn -pl hugegraph-pd/hg-pd-test test -Dtest=PartitionServiceTest#testSplitPartition ``` #### Test from IDE @@ -382,7 +382,7 @@ if (store == null) { - **Main class**: `org.apache.hugegraph.pd.HgPdApplication` (in `hg-pd-service`) - **Program arguments**: `--spring.config.location=file:./conf/application.yml` - **Working directory**: `hugegraph-pd/hg-pd-dist/target/hugegraph-pd-/` - - **JRE**: 11 or 17 + - **JRE**: 17 2. Set breakpoints in code diff --git a/hugegraph-pd/hg-pd-cli/pom.xml b/hugegraph-pd/hg-pd-cli/pom.xml index 01524c7479..ec24ac969d 100644 --- a/hugegraph-pd/hg-pd-cli/pom.xml +++ b/hugegraph-pd/hg-pd-cli/pom.xml @@ -31,7 +31,6 @@ hg-pd-cli - 2.15.2 @@ -49,7 +48,7 @@ com.alipay.sofa jraft-core - ${pd-store-jraft.version} + ${jraft.version} org.rocksdb @@ -78,38 +77,32 @@ org.yaml snakeyaml - 1.33 + ${snakeyaml.version} test com.fasterxml.jackson.core jackson-annotations - ${jackson.version} com.fasterxml.jackson.core jackson-core - ${jackson.version} com.fasterxml.jackson.core jackson-databind - ${jackson.version} com.fasterxml.jackson.module jackson-module-jaxb-annotations - ${jackson.version} com.fasterxml.jackson.jaxrs jackson-jaxrs-base - ${jackson.version} com.fasterxml.jackson.jaxrs jackson-jaxrs-json-provider - ${jackson.version} org.apache.hugegraph diff --git a/hugegraph-pd/hg-pd-client/pom.xml b/hugegraph-pd/hg-pd-client/pom.xml index c2413d3564..569ee2284c 100644 --- a/hugegraph-pd/hg-pd-client/pom.xml +++ b/hugegraph-pd/hg-pd-client/pom.xml @@ -69,7 +69,7 @@ org.yaml snakeyaml - 1.28 + ${snakeyaml.version} test diff --git a/hugegraph-pd/hg-pd-core/pom.xml b/hugegraph-pd/hg-pd-core/pom.xml index 38837f1e7d..e020578f0d 100644 --- a/hugegraph-pd/hg-pd-core/pom.xml +++ b/hugegraph-pd/hg-pd-core/pom.xml @@ -38,7 +38,7 @@ com.alipay.sofa jraft-core - ${pd-store-jraft.version} + ${jraft.version} org.rocksdb @@ -49,7 +49,7 @@ org.rocksdb rocksdbjni - 6.29.5 + ${rocksdb.version} org.apache.hugegraph @@ -58,7 +58,7 @@ org.springframework spring-context - 5.3.20 + ${spring-framework.version} org.apache.hugegraph @@ -67,7 +67,7 @@ org.springframework.boot spring-boot - 2.5.14 + ${spring-boot.version} org.projectlombok diff --git a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/StoreNodeService.java b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/StoreNodeService.java index 3503d1ffc8..f8d8b81ed6 100644 --- a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/StoreNodeService.java +++ b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/StoreNodeService.java @@ -158,6 +158,15 @@ public Metapb.Store register(Metapb.Store store) throws PDException { } // offline or up, or in the initial activation list, go live automatically + // TODO: do not mark a re-registering Store Up before it has restored its partition engines + // (HgStoreEngine.restoreLocalPartitionEngine); report a restoring state, or expose + // restore-complete per shard group, so a rolling restart can wait on it. The Helm chart + // (helm/hugegraph) production preset (values-cluster.yaml) keeps Store rollouts on + // OnDelete with a manual /v1/shardGroups barrier between Pod deletions; the default + // values use RollingUpdate. Restore-complete alone does not retire the barrier: a stopped + // Store stays Up until its keep-alive entry expires, so keep the rollout checks until PD + // also exposes liveness that tells stale membership apart. + // https://github.com/apache/hugegraph/issues/3229 Metapb.StoreState storeState = lastStore.getState(); if (storeState == Metapb.StoreState.Offline || storeState == Metapb.StoreState.Up || inInitialStoreList(store)) { diff --git a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/TaskScheduleService.java b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/TaskScheduleService.java index 003a4278dc..c111a6f85c 100644 --- a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/TaskScheduleService.java +++ b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/TaskScheduleService.java @@ -190,6 +190,22 @@ public void onStoreRaftChanged(Metapb.Store store) { public void shutDown() { executor.shutdownNow(); + boolean interrupted = Thread.interrupted(); + try { + while (!executor.isTerminated()) { + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("Waiting for PD scheduled tasks to stop"); + } + } catch (InterruptedException e) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } } private boolean isLeader() { diff --git a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/config/PDConfig.java b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/config/PDConfig.java index 56bd58b34d..3049915602 100644 --- a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/config/PDConfig.java +++ b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/config/PDConfig.java @@ -186,11 +186,24 @@ public class Raft { private int snapshotInterval; @Value("${raft.rpc-timeout:10000}") private int rpcTimeout; + // Bounds the ping that opens a connection to a peer; kept apart from rpc-timeout + // because a candidate opens its connections while it holds the raft node lock + @Value("${raft.rpc-connect-timeout:1000}") + private int rpcConnectTimeout = 1000; + // A follower that fell behind the log receives the whole store within this time; + // 0 keeps the timeout it always had, raft.rpc-timeout + @Value("${raft.rpc-install-snapshot-timeout:0}") + private int rpcInstallSnapshotTimeout; @Value("${grpc.host}") private String host; @Value("${server.port}") private int port; + public int getRpcInstallSnapshotTimeout() { + return this.rpcInstallSnapshotTimeout > 0 ? this.rpcInstallSnapshotTimeout : + this.rpcTimeout; + } + @Value("${pd.cluster_id:1}") private long clusterId; @Value("${grpc.port}") diff --git a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/meta/IdMetaStore.java b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/meta/IdMetaStore.java index 8e1fde67d6..c0f362d598 100644 --- a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/meta/IdMetaStore.java +++ b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/meta/IdMetaStore.java @@ -27,6 +27,7 @@ import org.apache.hugegraph.pd.common.PDException; import org.apache.hugegraph.pd.config.PDConfig; +import org.apache.hugegraph.pd.grpc.Pdpb; import org.apache.hugegraph.pd.store.KV; import com.caucho.hessian.io.Hessian2Input; @@ -47,6 +48,8 @@ public class IdMetaStore extends MetadataRocksDBStore { private static final String SEPARATOR = "@"; private static final ConcurrentHashMap SEQUENCES = new ConcurrentHashMap<>(); public static long CID_DEL_TIMEOUT = 24 * 3600 * 1000; + // An allocation whose write was rejected for its term starts again this often at most + private static final int ALLOCATION_ATTEMPTS = 3; private final long clusterId; public IdMetaStore(PDConfig pdConfig) { @@ -75,17 +78,50 @@ public static byte[] longToBytes(long l) { * @throws PDException */ public long getId(String key, int delta) throws PDException { + return inLeaderTerm(term -> allocateId(key, delta, term)); + } + + private long allocateId(String key, int delta, long term) throws PDException { Object probableLock = getLock(key); byte[] keyBs = (ID_PREFIX + key).getBytes(Charset.defaultCharset()); synchronized (probableLock) { byte[] bs = getOne(keyBs); long current = bs != null ? bytesToLong(bs) : 0L; long next = current + delta; - put(keyBs, longToBytes(next)); + getStore().put(keyBs, longToBytes(next), term); return current; } } + /** + * Runs an allocation that reads local state and then writes through raft, as two steps + * and not one raft entry. The reads follow a ReadIndex wait: a leader elected a moment + * ago may not have applied its predecessor's last write yet, and would hand out the same + * id again. The writes carry the leader term of that wait: a request that pauses between + * its read and its write could otherwise write its stale result after this node lost the + * leadership and won it back, over what the leader in between allocated. jraft rejects + * such a write, and the allocation starts again from a fresh read. + */ + private long inLeaderTerm(Allocation allocation) throws PDException { + for (int attempt = 1; ; attempt++) { + // Fails with NOT_LEADER when this node is not the leader, which ends the loop + long term = getStore().waitReadIndexInTerm(); + try { + return allocation.run(term); + } catch (PDException e) { + if (e.getErrorCode() != Pdpb.ErrorType.NOT_LEADER_VALUE || + attempt >= ALLOCATION_ATTEMPTS) { + throw e; + } + } + } + } + + private interface Allocation { + + long run(long term) throws PDException; + } + private Object getLock(String key) { Object probableLock = new Object(); Object currentLock = SEQUENCES.putIfAbsent(key, probableLock); @@ -119,6 +155,12 @@ public void resetId(String key) throws PDException { * @throws PDException */ public long getCId(String key, String name, long max) throws PDException { + // The delayed-deletion slots are read locally like the counter in getId() + return inLeaderTerm(term -> allocateCId(key, name, max, term)); + } + + private long allocateCId(String key, String name, long max, long term) + throws PDException { // Check for expired cids to delete. The frequency of deleting graphs is relatively low, // so this has little performance impact. byte[] delKeyPrefix = new StringBuffer() @@ -131,8 +173,8 @@ public long getCId(String key, String name, long max) throws PDException { if (value.length >= 2) { if (System.currentTimeMillis() - value[1] > CID_DEL_TIMEOUT) { try { - delCId(key, value[0]); - remove(kv.getKey()); + getStore().remove(genCIDSlotKey(key, value[0]), term); + getStore().remove(kv.getKey(), term); } catch (Exception e) { log.error("Exception ", e); } @@ -145,10 +187,11 @@ public long getCId(String key, String name, long max) throws PDException { byte[] value = getOne(cidDelayKey); if (value != null) { // Remove from delayed deletion queue - remove(cidDelayKey); + getStore().remove(cidDelayKey, term); return ((long[]) deserialize(value))[0]; } else { - return getCId(key, max); + // In the term of the wait above, which covers the counter and slots too + return allocateCId(key, max, term); } } } @@ -173,6 +216,11 @@ public long delCIdDelay(String key, String name, long cid) throws PDException { * @throws PDException */ public long getCId(String key, long max) throws PDException { + // Same two steps as getId(): read the counter and slots, then write through raft + return inLeaderTerm(term -> allocateCId(key, max, term)); + } + + private long allocateCId(String key, long max, long term) throws PDException { Object probableLock = getLock(key); byte[] keyBs = (CID_PREFIX + key).getBytes(Charset.defaultCharset()); synchronized (probableLock) { @@ -203,8 +251,8 @@ public long getCId(String key, long max) throws PDException { if (current == last) { return -1; } - put(genCIDSlotKey(key, current), longToBytes(current)); - put(keyBs, longToBytes(current + 1)); + getStore().put(genCIDSlotKey(key, current), longToBytes(current), term); + getStore().put(keyBs, longToBytes(current + 1), term); return current; } } diff --git a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/meta/MetadataFactory.java b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/meta/MetadataFactory.java index 673c313dfc..954a9f9e2e 100644 --- a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/meta/MetadataFactory.java +++ b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/meta/MetadataFactory.java @@ -28,30 +28,47 @@ */ public class MetadataFactory { - private static HgKVStore store = null; + private static HgKVStore store; + private static boolean shutdown; - public static HgKVStore getStore(PDConfig pdConfig) { + public static synchronized HgKVStore getStore(PDConfig pdConfig) { + if (shutdown) { + throw new IllegalStateException("PD metadata store has shut down"); + } if (store == null) { - synchronized (MetadataFactory.class) { - if (store == null) { - HgKVStore proto = new HgKVStoreImpl(); - //proto.init(pdConfig); - store = pdConfig.getRaft().isEnable() ? - new RaftKVStore(RaftEngine.getInstance(), proto) : - proto; - store.init(pdConfig); + HgKVStore proto = new HgKVStoreImpl(); + HgKVStore candidate = pdConfig.getRaft().isEnable() ? + new RaftKVStore(RaftEngine.getInstance(), proto) : proto; + try { + candidate.init(pdConfig); + store = candidate; + } catch (RuntimeException | Error failure) { + try { + candidate.close(); + } catch (RuntimeException | Error cleanup) { + if (cleanup != failure) { + failure.addSuppressed(cleanup); + } } + throw failure; } } return store; } - public static void closeStore() { + public static synchronized void closeStore() { if (store != null) { store.close(); } } + // Final service shutdown must not allow a late facade to create a new DB. + public static synchronized void shutdownStore() { + shutdown = true; + closeStore(); + store = null; + } + public static StoreInfoMeta newStoreInfoMeta(PDConfig pdConfig) { return new StoreInfoMeta(pdConfig); } diff --git a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftEngine.java b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftEngine.java index d39384909d..48fe074d76 100644 --- a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftEngine.java +++ b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftEngine.java @@ -46,6 +46,7 @@ import com.alipay.sofa.jraft.Node; import com.alipay.sofa.jraft.RaftGroupService; import com.alipay.sofa.jraft.Status; +import com.alipay.sofa.jraft.closure.ReadIndexClosure; import com.alipay.sofa.jraft.conf.Configuration; import com.alipay.sofa.jraft.core.Replicator; import com.alipay.sofa.jraft.core.State; @@ -58,7 +59,6 @@ import com.alipay.sofa.jraft.rpc.RpcServer; import com.alipay.sofa.jraft.rpc.impl.BoltRpcServer; import com.alipay.sofa.jraft.util.Endpoint; -import com.alipay.sofa.jraft.util.internal.ThrowUtil; import io.netty.channel.ChannelHandler; import lombok.extern.slf4j.Slf4j; @@ -74,6 +74,16 @@ public class RaftEngine { */ private static final long ALIVE_PEERS_REFRESH_MS = 1000L; + private static final long READ_INDEX_RETRY_DELAY_MS = 20L; + // An EBUSY retry waits 20 ms doubled per EBUSY answer in a row, up to 20 << 5 = 640 ms + private static final int READ_INDEX_BUSY_MAX_SHIFT = 5; + private static final ScheduledExecutorService READ_INDEX_RETRY = + Executors.newSingleThreadScheduledExecutor(runnable -> { + Thread thread = new Thread(runnable, "pd-raft-read-index-retry"); + thread.setDaemon(true); + return thread; + }); + private volatile static RaftEngine instance = new RaftEngine(); private RaftStateMachine stateMachine; private String groupId = "pd_raft"; @@ -85,7 +95,7 @@ public class RaftEngine { private volatile int alivePeerCount = -1; private ScheduledExecutorService alivePeersRefresher; - public RaftEngine() { + private RaftEngine() { this.stateMachine = new RaftStateMachine(); } @@ -133,9 +143,7 @@ public synchronized boolean init(PDConfig.Raft config) { // Snapshot interval nodeOptions.setSnapshotIntervalSecs(config.getSnapshotInterval()); - nodeOptions.setRpcConnectTimeoutMs(config.getRpcTimeout()); - nodeOptions.setRpcDefaultTimeout(config.getRpcTimeout()); - nodeOptions.setRpcInstallSnapshotTimeout(config.getRpcTimeout()); + setRpcTimeouts(nodeOptions, config); // TODO: tune RaftOptions for PD (see hugegraph-store PartitionEngine for reference) final PeerId serverId = JRaftUtils.getPeerId(config.getAddress()); @@ -145,6 +153,7 @@ public synchronized boolean init(PDConfig.Raft config) { // construct raft group and start raft this.raftGroupService = new RaftGroupService(groupId, serverId, nodeOptions, rpcServer, true); + this.stateMachine.resetForNewNode(); this.raftNode = raftGroupService.start(false); startAlivePeersRefresher(); log.info("RaftEngine start successfully: id = {}, peers list = {}", groupId, @@ -152,6 +161,20 @@ public synchronized boolean init(PDConfig.Raft config) { return this.raftNode != null; } + /** + * Wire the three raft rpc timeouts from their own options. jraft pings a peer to open a + * connection, and a candidate does so for every peer while it holds the node lock, so a + * peer that accepts the connection but never answers (a stopped process, a lost host) + * stalls the election, and the answers to the other peers' votes, for the whole connect + * timeout. Installing a snapshot sends the whole store and needs far longer than a + * normal request. + */ + static void setRpcTimeouts(NodeOptions nodeOptions, PDConfig.Raft config) { + nodeOptions.setRpcConnectTimeoutMs(config.getRpcConnectTimeout()); + nodeOptions.setRpcDefaultTimeout(config.getRpcTimeout()); + nodeOptions.setRpcInstallSnapshotTimeout(config.getRpcInstallSnapshotTimeout()); + } + /** * Create a Raft RPC Server for communication between PDs */ @@ -195,43 +218,68 @@ public List backChannelHandlers() { } } - public void shutDown() { - if (this.alivePeersRefresher != null) { - this.alivePeersRefresher.shutdownNow(); - try { - // Best effort: shutdownNow only interrupts, and a refresh parked in - // listAlivePeers waits on a lock acquire the interrupt does not break, - // for up to the raft rpc connect timeout per unreachable peer. A refresh - // that outlives this wait may publish one stale count over the reset - // below; acceptable while shutDown has no production caller. - if (!this.alivePeersRefresher.awaitTermination(1, TimeUnit.SECONDS)) { - log.warn("Raft alive-peers refresher still running after shutdown; " + - "hg_raft_alive_peers may briefly report a stale value"); + // Call while Raft can still complete metadata work already issued by a listener. + public void stopLeaderCallbacks() { + this.stateMachine.stopLeaderCallbacks(); + } + + public synchronized void shutDown() { + boolean interrupted = Thread.interrupted(); + try { + if (this.alivePeersRefresher != null) { + this.alivePeersRefresher.shutdownNow(); + while (!this.alivePeersRefresher.isTerminated()) { + try { + if (!this.alivePeersRefresher.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("Waiting for PD Raft alive-peers refresher to stop"); + } + } catch (InterruptedException e) { + interrupted = true; + } } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + this.alivePeersRefresher = null; } - this.alivePeersRefresher = null; - } - this.alivePeerCount = -1; - if (this.raftGroupService != null) { - this.raftGroupService.shutdown(); - try { - this.raftGroupService.join(); - } catch (final InterruptedException e) { + this.alivePeerCount = -1; + if (this.raftGroupService != null) { + this.raftGroupService.shutdown(); + boolean joined = false; + while (!joined) { + try { + this.raftGroupService.join(); + joined = true; + } catch (InterruptedException e) { + interrupted = true; + } + } + this.raftGroupService = null; + this.raftNode = null; + } else if (this.raftNode != null) { + this.raftNode.shutdown(); + boolean joined = false; + while (!joined) { + try { + this.raftNode.join(); + joined = true; + } catch (InterruptedException e) { + interrupted = true; + } + } this.raftNode = null; - ThrowUtil.throwException(e); } - this.raftGroupService = null; - } - if (this.rpcServer != null) { - this.rpcServer.shutdown(); - this.rpcServer = null; - } - if (this.raftNode != null) { - this.raftNode.shutdown(); + if (this.rpcServer != null) { + this.rpcServer.shutdown(); + this.rpcServer = null; + } + if (this.raftRpcClient != null) { + this.raftRpcClient.shutdown(); + this.raftRpcClient = null; + } + log.info("PD Raft shutdown joined"); + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } } - this.raftNode = null; } public boolean isLeader() { @@ -602,6 +650,134 @@ public Node getRaftNode() { return raftNode; } + /** + * Wait until this node has applied every entry committed before the call, so a local + * read that follows sees every write acknowledged before it. jraft's ReadIndex confirms + * the commit index with a quorum in the current term and runs the closure once the + * applied index reaches it; a leader elected a moment ago first waits for an entry of + * its own term to commit, so EAGAIN and EBUSY are retried until the deadline. Bounded by + * the raft rpc timeout. + *

+ * Never call it on the state machine thread: the closure waits for that thread to apply. + */ + public void waitReadIndex() throws PDException { + waitReadIndex(this.config.getRpcTimeout()); + } + + /** + * {@link #waitReadIndex()} for a read that a write depends on, such as an id counter. + * Returns the leader term, taken before the wait, for {@link Task#setExpectedTerm(long)}: + * jraft then rejects the write unless this node is still the leader of that term. A + * request that read in term 1 and paused could otherwise write its stale result after + * this node lost the leadership and won it back in term 3, over the writes of term 2. + */ + public long waitReadIndexInTerm() throws PDException { + long term = this.stateMachine.getLeaderTerm(); + waitReadIndex(); + if (term > 0L) { + return term; + } + // A leader elected a moment ago is announced when the first entry of its term is + // applied, which the wait above waited for; wait again inside the announced term + term = this.stateMachine.getLeaderTerm(); + if (term <= 0L) { + throw new PDException(Pdpb.ErrorType.NOT_LEADER_VALUE, "Not the raft leader"); + } + waitReadIndex(); + return term; + } + + void waitReadIndex(long timeoutMs) throws PDException { + CompletableFuture future = new CompletableFuture<>(); + long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs); + readIndex(future, deadline, 0); + try { + future.get(timeoutMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // Stops a pending retry + future.cancel(false); + Thread.currentThread().interrupt(); + throw new PDException(Pdpb.ErrorType.UNKNOWN_VALUE, + "Interrupted while waiting for the raft read index", e); + } catch (TimeoutException e) { + future.cancel(false); + throw new PDException(Pdpb.ErrorType.UNKNOWN_VALUE, + String.format("Raft read index timed out after %d ms", + timeoutMs)); + } catch (ExecutionException e) { + if (e.getCause() instanceof PDException) { + throw (PDException) e.getCause(); + } + throw new PDException(Pdpb.ErrorType.UNKNOWN_VALUE, e.getCause()); + } + } + + private void readIndex(CompletableFuture future, long deadline, int busyRetries) { + if (future.isDone()) { + return; + } + Node node = this.raftNode; + if (node == null) { + future.completeExceptionally(new PDException(Pdpb.ErrorType.NOT_LEADER_VALUE, + "Raft node is not started")); + return; + } + long remaining = TimeUnit.NANOSECONDS.toMillis(deadline - System.nanoTime()); + if (remaining <= 0L) { + // The waiter times out at the same deadline and reports how long it waited + return; + } + // Without a timeout the closure fails with ETIMEDOUT after jraft's own default + // (jraft.read-index.timeout, 2 s), before the caller's deadline + ReadIndexClosure closure = new ReadIndexClosure(remaining) { + @Override + public void run(Status status, long index, byte[] reqCtx) { + if (status.isOk()) { + future.complete(null); + return; + } + RaftError error = status.getRaftError(); + if (error == RaftError.ETIMEDOUT) { + // The closure's own timer, set to the time left: the waiter reports it + return; + } + // EAGAIN: no entry of the current term committed yet. It clears once the new + // leader commits, which can take longer than any fixed number of short + // retries, so retry every 20 ms until the caller's deadline. + // EBUSY: leadership is being transferred, or jraft's read index queue is + // full. Retries at a fixed 20 ms would keep a full queue full, so each one + // waits twice as long as the one before it. + boolean busy = error == RaftError.EBUSY; + long delayMs = busy ? READ_INDEX_RETRY_DELAY_MS << + Math.min(busyRetries, READ_INDEX_BUSY_MAX_SHIFT) : + READ_INDEX_RETRY_DELAY_MS; + long next = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(delayMs); + if ((error == RaftError.EAGAIN || busy) && next - deadline < 0) { + int retries = busy ? busyRetries + 1 : 0; + READ_INDEX_RETRY.schedule(() -> readIndex(future, deadline, retries), + delayMs, TimeUnit.MILLISECONDS); + return; + } + // ENODESHUTDOWN and EHOSTDOWN come with the IllegalStateException caught + // below and may complete the future first, so they map the same way + int type = error == RaftError.EPERM || error == RaftError.ENODESHUTDOWN || + error == RaftError.EHOSTDOWN ? Pdpb.ErrorType.NOT_LEADER_VALUE : + Pdpb.ErrorType.UNKNOWN_VALUE; + future.completeExceptionally( + new PDException(type, "Raft read index failed: " + status)); + } + }; + try { + node.readIndex(new byte[0], closure); + } catch (IllegalStateException e) { + // jraft throws once the node or its read service is shutting down, after + // queueing the closure with ENODESHUTDOWN or EHOSTDOWN; turn it into a + // PDException for getId's callers + future.completeExceptionally(new PDException(Pdpb.ErrorType.NOT_LEADER_VALUE, + "Raft node is shutting down", e)); + } + } + private boolean peerEquals(PeerId p1, PeerId p2) { if (p1 == null && p2 == null) { return true; diff --git a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftRpcClient.java b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftRpcClient.java index 8c7398a53a..18602101f0 100644 --- a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftRpcClient.java +++ b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftRpcClient.java @@ -45,6 +45,13 @@ public synchronized boolean init(final RpcOptions rpcOptions) { return this.rpcClient.init(null); } + public synchronized void shutdown() { + if (this.rpcClient != null) { + this.rpcClient.shutdown(); + this.rpcClient = null; + } + } + /** * Request a snapshot */ diff --git a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftStateMachine.java b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftStateMachine.java index d07ab75f8c..40830e3af4 100644 --- a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftStateMachine.java +++ b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/raft/RaftStateMachine.java @@ -80,6 +80,9 @@ static final class ProbeView { } private List taskHandlers; private List stateListeners; + private final Object callbackMonitor = new Object(); + private int callbacks; + private boolean stoppingCallbacks; public RaftStateMachine() { this.taskHandlers = new CopyOnWriteArrayList<>(); @@ -98,6 +101,13 @@ public boolean isLeader() { return this.leaderTerm.get() > 0; } + /** + * The term jraft last announced this node as the leader of, -1 while it is not the leader + */ + long getLeaderTerm() { + return this.leaderTerm.get(); + } + /** * The last state and leader visibility a raft callback announced, as one immutable * view. jraft emits no callback for candidacy or leadership transfer, so a candidate @@ -107,6 +117,15 @@ ProbeView getProbeView() { return this.probeView; } + // Called only before starting a new node, after the previous node has joined. + void resetForNewNode() { + synchronized (this.callbackMonitor) { + this.leaderTerm.set(-1); + this.probeView = new ProbeView(State.STATE_UNINITIALIZED, false); + this.stoppingCallbacks = false; + } + } + @Override public void onApply(Iterator iter) { while (iter.hasNext()) { @@ -150,37 +169,91 @@ public void onShutdown() { super.onShutdown(); } + private void notifyStateListeners() { + synchronized (this.callbackMonitor) { + if (this.stoppingCallbacks || CollectionUtils.isEmpty(this.stateListeners)) { + return; + } + this.callbacks++; + } + try { + Utils.runInThread(() -> { + try { + this.stateListeners.forEach(RaftStateListener::onRaftLeaderChanged); + } finally { + callbackFinished(); + } + }); + } catch (RuntimeException | Error failure) { + callbackFinished(); + throw failure; + } + } + + private void callbackFinished() { + synchronized (this.callbackMonitor) { + this.callbacks--; + this.callbackMonitor.notifyAll(); + } + } + + void stopLeaderCallbacks() { + boolean interrupted = false; + synchronized (this.callbackMonitor) { + this.stoppingCallbacks = true; + while (this.callbacks != 0) { + try { + this.callbackMonitor.wait(5000L); + } catch (InterruptedException e) { + interrupted = true; + } + if (this.callbacks != 0) { + log.warn("Waiting for PD leader callbacks to finish"); + } + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + @Override public void onLeaderStart(final long term) { + if (isTerminal()) { + return; + } this.leaderTerm.set(term); this.probeView = new ProbeView(State.STATE_LEADER, true); super.onLeaderStart(term); log.info("Raft becomes leader"); - Utils.runInThread(() -> { - if (!CollectionUtils.isEmpty(stateListeners)) { - stateListeners.forEach(RaftStateListener::onRaftLeaderChanged); - } - }); + notifyStateListeners(); } @Override public void onLeaderStop(final Status status) { this.leaderTerm.set(-1); - this.probeView = new ProbeView(State.STATE_FOLLOWER, false); + // JRaft steps down after reporting a fatal error. That callback does not recover the node. + if (!isTerminal()) { + this.probeView = new ProbeView(State.STATE_FOLLOWER, false); + } super.onLeaderStop(status); log.info("Raft lost leader "); } @Override public void onStartFollowing(final LeaderChangeContext ctx) { + if (isTerminal()) { + return; + } this.probeView = new ProbeView(State.STATE_FOLLOWER, true); super.onStartFollowing(ctx); - Utils.runInThread(() -> { - if (!CollectionUtils.isEmpty(stateListeners)) { - stateListeners.forEach(RaftStateListener::onRaftLeaderChanged); - } - }); + notifyStateListeners(); + } + + private boolean isTerminal() { + State state = this.probeView.state; + return state == State.STATE_ERROR || state == State.STATE_SHUTDOWN; } @Override @@ -195,45 +268,59 @@ public void onConfigurationCommitted(final Configuration conf) { log.info("Raft onConfigurationCommitted {}", conf); } + /** + * jraft calls this on the state machine thread, with the snapshot index set to the last + * applied index. The RocksDB checkpoint is taken here, before the thread applies the next + * entry: a checkpoint taken later holds entries past the snapshot index, and a node that + * installs the snapshot applies them a second time. Only the compression runs on the job + * thread. The state machine thread waits for the checkpoint, which is taken under the + * store's write lock: RocksDB flushes the memtable for it, copies the metadata files and + * hard-links the SST files, or copies them across filesystems. How long that pause is has + * not been measured. + */ @Override public void onSnapshotSave(final SnapshotWriter writer, final Closure done) { - MetadataService.getUninterruptibleJobs().submit(() -> { - lock.lock(); + String snapshotDir = writer.getPath() + File.separator + SNAPSHOT_DIR_NAME; + lock.lock(); + try { + log.info("start snapshot save"); try { - log.info("start snapshot save"); - String snapshotDir = writer.getPath() + File.separator + SNAPSHOT_DIR_NAME; + FileUtils.deleteDirectory(new File(snapshotDir)); + FileUtils.forceMkdir(new File(snapshotDir)); + } catch (IOException e) { + log.error("Failed to create snapshot directory {}", snapshotDir, e); + done.run(new Status(RaftError.EIO, e.toString())); + return; + } + for (RaftTaskHandler taskHandler : taskHandlers) { try { - FileUtils.deleteDirectory(new File(snapshotDir)); - FileUtils.forceMkdir(new File(snapshotDir)); - } catch (IOException e) { - log.error("Failed to create snapshot directory {}", snapshotDir); + KVOperation op = KVOperation.createSaveSnapshot(snapshotDir); + taskHandler.invoke(op, null); + log.info("Raft onSnapshotSave success"); + } catch (PDException e) { + log.error("Raft onSnapshotSave failed", e); done.run(new Status(RaftError.EIO, e.toString())); return; } - for (RaftTaskHandler taskHandler : taskHandlers) { - try { - KVOperation op = KVOperation.createSaveSnapshot(snapshotDir); - taskHandler.invoke(op, null); - log.info("Raft onSnapshotSave success"); - } catch (PDException e) { - log.error("Raft onSnapshotSave failed. {}", e.toString()); - done.run(new Status(RaftError.EIO, e.toString())); - } - } + } + } catch (Exception e) { + log.error("failed to save snapshot", e); + done.run(new Status(RaftError.EIO, e.toString())); + return; + } finally { + lock.unlock(); + } + + MetadataService.getUninterruptibleJobs().submit(() -> { + lock.lock(); + try { // compress - try { - compressSnapshot(writer); - FileUtils.deleteDirectory(new File(snapshotDir)); - } catch (Exception e) { - log.error("Failed to delete snapshot directory {}, {}", snapshotDir, - e.toString()); - done.run(new Status(RaftError.EIO, e.toString())); - return; - } + compressSnapshot(writer); + FileUtils.deleteDirectory(new File(snapshotDir)); done.run(Status.OK()); log.info("snapshot save done"); } catch (Exception e) { - log.error("failed to save snapshot", e); + log.error("Failed to compress snapshot directory {}", snapshotDir, e); done.run(new Status(RaftError.EIO, e.toString())); } finally { lock.unlock(); @@ -255,8 +342,10 @@ public boolean onSnapshotLoad(final SnapshotReader reader) { try { decompressSnapshot(reader); } catch (PDException e) { - log.error("Failed to decompress snapshot directory {}, {}", snapshotDir, e.toString()); - return true; + // false makes jraft fail the load. true would report the snapshot as + // installed and move the applied index to it, with none of its data loaded + log.error("Failed to decompress snapshot directory {}", snapshotDir, e); + return false; } CountDownLatch latch = new CountDownLatch(taskHandlers.size()); diff --git a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/store/HgKVStore.java b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/store/HgKVStore.java index 263cb70b28..f814df97cf 100644 --- a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/store/HgKVStore.java +++ b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/store/HgKVStore.java @@ -56,4 +56,35 @@ public interface HgKVStore { List scanRange(byte[] start, byte[] end); void close(); + + /** + * Wait until a local read sees every write committed before the call. A store that + * is not replicated has nothing to wait for. + */ + default void waitReadIndex() throws PDException { + } + + /** + * {@link #waitReadIndex()} for reads that a write depends on. Returns the leader term + * of the reads for the two methods below; -1 from a store that is not replicated. + */ + default long waitReadIndexInTerm() throws PDException { + waitReadIndex(); + return -1L; + } + + /** + * A put that is applied only while this node is still the leader of {@code term}, and + * fails with NOT_LEADER otherwise: what was read in that term may be stale by then. + */ + default void put(byte[] key, byte[] value, long term) throws PDException { + put(key, value); + } + + /** + * A remove under the same condition + */ + default long remove(byte[] key, long term) throws PDException { + return remove(key); + } } diff --git a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/store/HgKVStoreImpl.java b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/store/HgKVStoreImpl.java index bd2e7a9e22..f33648e0eb 100644 --- a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/store/HgKVStoreImpl.java +++ b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/store/HgKVStoreImpl.java @@ -62,12 +62,11 @@ public class HgKVStoreImpl implements HgKVStore { @Override public void init(PDConfig config) { - dbOptions = new Options().setCreateIfMissing(true); - final Lock writeLock = this.readWriteLock.writeLock(); writeLock.lock(); try { - this.dbPath = config.getDataPath() + "/rocksdb/"; + this.dbOptions = new Options().setCreateIfMissing(true); + this.dbPath = config.getDataPath() + "/rocksdb"; File file = new File(this.dbPath); if (!file.exists()) { try { @@ -77,8 +76,14 @@ public void init(PDConfig config) { } } openRocksDB(dbPath); - } catch (PDException e) { - log.error("Failed to open data file,{}", e); + } catch (PDException | RuntimeException | Error e) { + // TODO: retry opening a held RocksDB LOCK before failing PD startup. + // https://github.com/apache/hugegraph/issues/3226 + if (this.dbOptions != null) { + this.dbOptions.close(); + this.dbOptions = null; + } + throw new IllegalStateException("Failed to open PD metadata at " + this.dbPath, e); } finally { writeLock.unlock(); } @@ -114,10 +119,10 @@ public byte[] get(byte[] key) throws PDException { public List scanPrefix(byte[] prefix) { final Lock readLock = this.readWriteLock.readLock(); readLock.lock(); - try (ReadOptions options = new ReadOptions() - .setIterateLowerBound(new Slice(prefix))) { + try (Slice bound = new Slice(prefix); + ReadOptions options = new ReadOptions().setIterateLowerBound(bound); + RocksIterator iterator = db.newIterator(options)) { List kvs = new ArrayList<>(); - RocksIterator iterator = db.newIterator(options); iterator.seekToFirst(); while (iterator.isValid() && 0 == Bytes.indexOf(iterator.key(), prefix)) { kvs.add(new KV(iterator.key(), iterator.value())); @@ -147,9 +152,9 @@ public long remove(byte[] key) throws PDException { public long removeByPrefix(byte[] prefix) throws PDException { final Lock readLock = this.readWriteLock.readLock(); readLock.lock(); - try (ReadOptions options = new ReadOptions() - .setIterateLowerBound(new Slice(prefix))) { - RocksIterator iterator = db.newIterator(options); + try (Slice bound = new Slice(prefix); + ReadOptions options = new ReadOptions().setIterateLowerBound(bound); + RocksIterator iterator = db.newIterator(options)) { iterator.seekToFirst(); while (iterator.isValid()) { @@ -304,11 +309,11 @@ public void loadSnapshot(String snapshotPath) throws PDException { public List scanRange(byte[] start, byte[] end) { final Lock readLock = this.readWriteLock.readLock(); readLock.lock(); - try (ReadOptions options = new ReadOptions() - .setIterateLowerBound(new Slice(start)) - .setIterateUpperBound(new Slice(end))) { + try (Slice lower = new Slice(start); + Slice upper = new Slice(end); + ReadOptions options = new ReadOptions().setIterateLowerBound(lower).setIterateUpperBound(upper); + RocksIterator iterator = db.newIterator(options)) { List kvs = new ArrayList<>(); - RocksIterator iterator = db.newIterator(options); iterator.seekToFirst(); while (iterator.isValid()) { kvs.add(new KV(iterator.key(), iterator.value())); @@ -322,7 +327,17 @@ public List scanRange(byte[] start, byte[] end) { @Override public void close() { - closeRocksDB(); + Lock writeLock = this.readWriteLock.writeLock(); + writeLock.lock(); + try { + closeRocksDB(); + if (this.dbOptions != null) { + this.dbOptions.close(); + this.dbOptions = null; + } + } finally { + writeLock.unlock(); + } } private void closeRocksDB() { diff --git a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/store/RaftKVStore.java b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/store/RaftKVStore.java index b61f07ac1d..47ebad864f 100644 --- a/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/store/RaftKVStore.java +++ b/hugegraph-pd/hg-pd-core/src/main/java/org/apache/hugegraph/pd/store/RaftKVStore.java @@ -20,6 +20,7 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.apache.hugegraph.pd.common.PDException; @@ -179,6 +180,44 @@ public void close() { store.close(); } + /** + * Reads above are served from the local state, which on a new leader can trail the + * entries its predecessor committed; this waits for them to be applied + */ + @Override + public void waitReadIndex() throws PDException { + this.engine.waitReadIndex(); + } + + @Override + public long waitReadIndexInTerm() throws PDException { + return this.engine.waitReadIndexInTerm(); + } + + @Override + public void put(byte[] key, byte[] value, long term) throws PDException { + applyInTerm(KVOperation.createPut(key, value), term); + } + + @Override + public long remove(byte[] key, long term) throws PDException { + applyInTerm(KVOperation.createRemove(key), term); + return 0; + } + + private void applyInTerm(KVOperation op, long term) throws PDException { + try { + applyOperation(op, term).get(); + } catch (ExecutionException e) { + // Keeps NOT_LEADER, which is how jraft's EPERM for another term arrives here + throw e.getCause() instanceof PDException ? (PDException) e.getCause() : + new PDException(Pdpb.ErrorType.UNKNOWN_VALUE, e.getCause()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new PDException(Pdpb.ErrorType.UNKNOWN_VALUE, e); + } + } + /** * Need to walk the real operation of Raft */ @@ -221,9 +260,16 @@ public void doLoadSnapshot(String snapshotPath) throws PDException { } private CompletableFuture applyOperation(final KVOperation op) throws PDException { + // -1 is jraft's default: the entry is accepted in any term + return applyOperation(op, -1L); + } + + private CompletableFuture applyOperation(final KVOperation op, long expectedTerm) + throws PDException { CompletableFuture future = new CompletableFuture<>(); try { final Task task = new Task(); + task.setExpectedTerm(expectedTerm); task.setData(ByteBuffer.wrap(op.toByteArray())); task.setDone(new RaftStateMachine.RaftClosureAdapter(op, new KVStoreClosure() { Object data; diff --git a/hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh b/hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh index 77af2af89d..f3bf1437e1 100755 --- a/hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh +++ b/hugegraph-pd/hg-pd-dist/docker/docker-entrypoint.sh @@ -22,7 +22,17 @@ log() { echo "[hugegraph-pd-entrypoint] $*"; } require_env() { local name="$1" if [[ -z "${!name:-}" ]]; then - echo "ERROR: missing required env '${name}'" >&2; exit 2 + echo "ERROR: missing required env '${name}'" >&2 + if [[ "${name}" == "HG_PD_AUTH_SECRET_KEY" ]]; then + echo "This shared credential configures PD REST Basic authentication (auth.secret-key)." >&2 + echo "Helm normally creates a shared Secret through pd.auth.autoGenerate=true and injects it" >&2 + echo "into HG_PD_AUTH_SECRET_KEY via the PD StatefulSet env.valueFrom.secretKeyRef." >&2 + echo "Check pd.auth.value or pd.auth.existingSecret and pd.auth.key, then verify the" >&2 + echo "rendered StatefulSet env and the referenced Secret name/key in the release namespace." >&2 + echo "For standalone containers, set HG_PD_AUTH_SECRET_KEY to the same value on every PD" >&2 + echo "and use that value for Server PD_AUTH_PASSWORD. Do not generate a separate value per node." >&2 + fi + exit 2 fi } diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/descriptor/server-assembly.xml b/hugegraph-pd/hg-pd-dist/src/assembly/descriptor/server-assembly.xml index c43a868352..ca6439ae7b 100644 --- a/hugegraph-pd/hg-pd-dist/src/assembly/descriptor/server-assembly.xml +++ b/hugegraph-pd/hg-pd-dist/src/assembly/descriptor/server-assembly.xml @@ -41,6 +41,24 @@ + + + ${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/bin/prepare-topling.sh + bin + 755 + + + ${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/bin/preload-topling.sh + bin + 755 + + + ${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/conf/toplingdb.yaml + conf + 644 + + + diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/jvm-module.options b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/jvm-module.options new file mode 100644 index 0000000000..b41209f448 --- /dev/null +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/jvm-module.options @@ -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. +# + +# Hessian reflects into StackTraceElement when serializing Raft operations. +--add-opens=java.base/java.lang=ALL-UNNAMED diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh index 1329df2271..d687db42cb 100755 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh @@ -37,7 +37,7 @@ while getopts "d:g:j:y:" arg; do # Telemetry is used to collect metrics, traces and logs d) DAEMON="$OPTARG" ;; y) OPEN_TELEMETRY="$OPTARG" ;; - ?) echo "USAGE: $0 [-d true|false] [-g g1] [-j xxx] [-y true|false]" && exit 1 ;; + ?) echo "USAGE: $0 [-d true|false] [-g g1|ZGC] [-j xxx] [-y true|false]" && exit 1 ;; esac done @@ -60,6 +60,12 @@ LOGS="$TOP/logs" OUTPUT=${LOGS}/hugegraph-pd-stdout.log GITHUB="https://github.com" PID_FILE="$BIN/pid" +JVM_MODULE_OPTIONS="$BIN/jvm-module.options" + +if [[ ! -f "$JVM_MODULE_OPTIONS" || ! -r "$JVM_MODULE_OPTIONS" ]]; then + echo "Error: JVM module options file is missing or unreadable: $JVM_MODULE_OPTIONS" >&2 + exit 1 +fi . "$BIN"/util.sh @@ -69,7 +75,7 @@ ensure_path_writable "$PLUGINS" # The maximum and minimum heap memory that service can use MAX_MEM=$((32 * 1024)) MIN_MEM=$((1 * 512)) -EXPECT_JDK_VERSION=11 +EXPECT_JDK_VERSION=17 # Change to $BIN's parent cd "${TOP}" || exit @@ -82,8 +88,11 @@ else fi # check jdk version -JAVA_VERSION=$($JAVA -version 2>&1 | awk 'NR==1{gsub(/"/,""); print $3}' | awk -F'_' '{print $1}') -if [[ $? -ne 0 || $JAVA_VERSION < $EXPECT_JDK_VERSION ]]; then +JAVA_VERSION=$($JAVA -version 2>&1 | + awk -F'"' '/^(java|openjdk) version "/ {print $2; exit}' | + sed 's/^1\.//' | cut -d'.' -f1) +JAVA_VERSION="${JAVA_VERSION%%[!0-9]*}" +if [[ -z $JAVA_VERSION || $JAVA_VERSION -lt $EXPECT_JDK_VERSION ]]; then echo "Please make sure that the JDK is installed and the version >= $EXPECT_JDK_VERSION" >> ${OUTPUT} exit 1 fi @@ -103,22 +112,27 @@ if [ "$JAVA_OPTIONS" = "" ]; then # -Xloggc:./logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps" fi -# Using G1GC as the default garbage collector (Recommended for large memory machines) +# Keep JVM/caller GC selection by default; explicitly select G1 when requested. case "$GC_OPTION" in - "") - echo "Using G1GC as the default garbage collector" - JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+ParallelRefProcEnabled \ - -XX:InitiatingHeapOccupancyPercent=50 -XX:G1RSetUpdatingPauseTimePercent=5" + ""|g1|G1) + if [[ "$GC_OPTION" == g1 || "$GC_OPTION" == G1 ]]; then + echo "Using G1GC" + JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseG1GC" + else + echo "Using JVM garbage collector configuration" + fi + JAVA_OPTIONS="-XX:+ParallelRefProcEnabled -XX:InitiatingHeapOccupancyPercent=50 \ + -XX:G1RSetUpdatingPauseTimePercent=5 ${JAVA_OPTIONS}" ;; zgc|ZGC) - echo "Using ZGC as the default garbage collector (Only support Java 11+)" + echo "Using ZGC as the default garbage collector (requires Java 17 or later)" JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseZGC -XX:+UnlockExperimentalVMOptions \ -XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 \ -XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 \ -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive" ;; *) - echo "Unrecognized gc option: '$GC_OPTION', default use g1, options only support 'ZGC' now" >> ${OUTPUT} + echo "Unrecognized gc option: '$GC_OPTION', supported options: g1, ZGC" >> ${OUTPUT} exit 1 esac @@ -170,15 +184,26 @@ fi JVM_OPTIONS="-Dlog4j.configurationFile=${CONF}/log4j2.xml -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" +source "$BIN/preload-topling.sh" || exit 1 +BOOT_JARS=("${LIB}"/hg-pd-service-*.jar) +if [ "${#BOOT_JARS[@]}" -ne 1 ] || [ ! -f "${BOOT_JARS[0]}" ]; then + echo "Error: expected one component executable JAR in $LIB" >&2 + exit 1 +fi +JAVA_MAIN=(-jar "${BOOT_JARS[0]}") +if [ -n "${TOPLING_RUNTIME_CLASSPATH:-}" ]; then + JAVA_MAIN=(-cp "${TOPLING_RUNTIME_CLASSPATH}:${BOOT_JARS[0]}" org.springframework.boot.loader.JarLauncher) +fi + # Turn on security check if [[ $DAEMON == "true" ]]; then echo "Starting HugeGraphPDServer in daemon mode..." if [[ "${STDOUT_MODE:-false}" == "true" ]]; then - exec ${JAVA} -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ - -Dspring.config.location=${CONF}/application.yml ${LIB}/hg-pd-service-*.jar & + exec ${JAVA} "@$JVM_MODULE_OPTIONS" -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ + -Dspring.config.location=${CONF}/application.yml "${JAVA_MAIN[@]}" "$@" & else - exec ${JAVA} -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ - -Dspring.config.location=${CONF}/application.yml ${LIB}/hg-pd-service-*.jar >> ${OUTPUT} 2>&1 & + exec ${JAVA} "@$JVM_MODULE_OPTIONS" -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ + -Dspring.config.location=${CONF}/application.yml "${JAVA_MAIN[@]}" "$@" >> ${OUTPUT} 2>&1 & fi PID="$!" # Write pid to file @@ -190,10 +215,10 @@ else echo "$$" > "$PID_FILE" echo "[+pid] $$" if [[ "${STDOUT_MODE:-false}" == "true" ]]; then - exec ${JAVA} -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ - -Dspring.config.location=${CONF}/application.yml ${LIB}/hg-pd-service-*.jar + exec ${JAVA} "@$JVM_MODULE_OPTIONS" -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ + -Dspring.config.location=${CONF}/application.yml "${JAVA_MAIN[@]}" "$@" else - exec ${JAVA} -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ - -Dspring.config.location=${CONF}/application.yml ${LIB}/hg-pd-service-*.jar >> ${OUTPUT} 2>&1 + exec ${JAVA} "@$JVM_MODULE_OPTIONS" -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ + -Dspring.config.location=${CONF}/application.yml "${JAVA_MAIN[@]}" "$@" >> ${OUTPUT} 2>&1 fi fi diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/conf/application.yml b/hugegraph-pd/hg-pd-dist/src/assembly/static/conf/application.yml index ada9791922..73bcae9069 100644 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/conf/application.yml +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/conf/application.yml @@ -96,3 +96,7 @@ partition: # The default maximum number of replicas per machine # the initial number of partitions= store-max-shard-count * store-number / default-shard-count store-max-shard-count: 12 + +# Match the explicit launcher opt-in when enabling Topling. +rocksdb: + provider: rocksdb diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/conf/application.yml.template b/hugegraph-pd/hg-pd-dist/src/assembly/static/conf/application.yml.template index 43bdf99fc4..1fc2fc4fea 100644 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/conf/application.yml.template +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/conf/application.yml.template @@ -63,6 +63,12 @@ raft: address: $RAFT_ADDRESS$ # raft cluster peers-list: $RAFT_PEERS_LIST$ + # Timeout for opening a connection to a peer, in milliseconds; an election waits this + # long for each peer that accepts the connection but does not answer + rpc-connect-timeout: 1000 + # Timeout for sending a snapshot to a follower that fell behind the log, in milliseconds; + # 0 uses rpc-timeout + rpc-install-snapshot-timeout: 0 # The interval between snapshot generation, in seconds snapshotInterval: 300 metrics: true diff --git a/hugegraph-pd/hg-pd-grpc/pom.xml b/hugegraph-pd/hg-pd-grpc/pom.xml index be8ccc44fd..4c6b77abbd 100644 --- a/hugegraph-pd/hg-pd-grpc/pom.xml +++ b/hugegraph-pd/hg-pd-grpc/pom.xml @@ -33,7 +33,8 @@ 1.7.1 - 1.39.0 + + 1.39.0 3.17.2 0.6.1 @@ -90,7 +91,7 @@ grpc-java - io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:${grpc.codegen.version}:exe:${os.detected.classifier} ${project.basedir}/src/main/proto ${project.basedir}/src/main/java @@ -138,14 +139,13 @@ - 1.47.0 - 3.21.7 + 1.47.0 com.google.protobuf protobuf-java - ${riscv64.protobuf.java.version} + ${protobuf.runtime.version} diff --git a/hugegraph-pd/hg-pd-service/pom.xml b/hugegraph-pd/hg-pd-service/pom.xml index eef38b4073..7332c1210b 100644 --- a/hugegraph-pd/hg-pd-service/pom.xml +++ b/hugegraph-pd/hg-pd-service/pom.xml @@ -57,16 +57,29 @@ ${revision} + - org.apache.hugegraph - hg-store-common - ${revision} + org.apache.commons + commons-text + 1.10.0 - org.apache.hugegraph - hugegraph-core - ${revision} + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime @@ -83,7 +96,7 @@ org.springframework.boot spring-boot-starter-web - 2.5.14 + ${spring-boot.version} @@ -101,7 +114,7 @@ org.springframework.boot spring-boot-starter-actuator - 2.5.14 + ${spring-boot.version} io.micrometer @@ -111,12 +124,12 @@ org.springframework.boot spring-boot-starter-jetty - 2.5.14 + ${spring-boot.version} org.springframework.boot spring-boot-starter-log4j2 - 2.5.14 + ${spring-boot.version} log4j-slf4j-impl @@ -137,7 +150,7 @@ com.google.protobuf protobuf-java-util - 3.17.2 + ${protobuf.runtime.version} org.apache.hugegraph diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/boot/HugePDServer.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/boot/HugePDServer.java index 54d2761df8..2ca7f7540c 100644 --- a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/boot/HugePDServer.java +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/boot/HugePDServer.java @@ -17,6 +17,7 @@ package org.apache.hugegraph.pd.boot; +import org.apache.hugegraph.util.RocksDBRuntime; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; @@ -37,8 +38,10 @@ public static void main(String[] args) { System.setProperty("logging.path", "logs"); System.setProperty("com.alipay.remoting.client.log.level", "error"); } - Runtime.getRuntime().addShutdownHook(new ShutdownHook(Thread.currentThread())); - SpringApplication.run(HugePDServer.class); + SpringApplication application = new SpringApplication(HugePDServer.class); + application.addInitializers(context -> RocksDBRuntime.verify( + context.getEnvironment().getProperty("rocksdb.provider", "rocksdb"))); + application.run(args); System.out.println("Hugegraph-pd started."); } } diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/boot/ShutdownHook.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/boot/ShutdownHook.java index 371235f341..a8cf28abbf 100644 --- a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/boot/ShutdownHook.java +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/boot/ShutdownHook.java @@ -15,75 +15,120 @@ * limitations under the License. */ - package org.apache.hugegraph.pd.boot; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.apache.hugegraph.pd.TaskScheduleService; +import org.apache.hugegraph.pd.meta.MetadataFactory; +import org.apache.hugegraph.pd.raft.RaftEngine; +import org.apache.hugegraph.pd.pulse.PDPulseSubject; +import org.apache.hugegraph.pd.service.KvServiceGrpcImpl; import org.apache.hugegraph.pd.service.MetadataService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.hugegraph.pd.service.PDService; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationListener; +import org.lognet.springboot.grpc.context.GRpcServerInitializedEvent; -import java.util.concurrent.ThreadPoolExecutor; +import io.grpc.Server; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; -public class ShutdownHook extends Thread { +/** + * Spring stops its transports before destroying singleton beans. The PDService + * dependency keeps its metadata owners available until this cleanup completes. + */ +@Slf4j +@Component +public class ShutdownHook implements DisposableBean, ApplicationListener { - private static Logger log = LoggerFactory.getLogger(ShutdownHook.class); - private static String msg = "there are still uninterruptible jobs that have not been completed and" + - " will wait for them to complete"; - private Thread main; + private final PDService service; + private final KvServiceGrpcImpl kvService; + private Server grpcServer; - public ShutdownHook(Thread main) { - super(); - this.main = main; + public ShutdownHook(PDService service, KvServiceGrpcImpl kvService) { + this.service = service; + this.kvService = kvService; } @Override - public void run() { - log.info("shutdown signal received"); - main.interrupt(); - waitForShutdown(); - try { - main.join(); - } catch (InterruptedException e) { - } - log.info("shutdown completed"); + public void onApplicationEvent(GRpcServerInitializedEvent event) { + this.grpcServer = event.getServer(); } - private void waitForShutdown() { - checkUninterruptibleJobs(); + @Override + public void destroy() { + // The starter's default stop initiates shutdown without waiting. + awaitGrpcTermination(); + this.kvService.stopScheduling(); + closeOwners(this.service); } - private void checkUninterruptibleJobs() { - ThreadPoolExecutor jobs = MetadataService.getUninterruptibleJobs(); + public static void closeOwners(PDService service) { + log.info("Closing PD background work and native owners"); + TaskScheduleService tasks = service.getTaskService(); + if (tasks != null) { + tasks.shutDown(); + } + PDPulseSubject.stopScheduling(); + RaftEngine raft = RaftEngine.getInstance(); + raft.stopLeaderCallbacks(); + // Raft may still submit snapshot saves while its shutdown is joining. + raft.shutDown(); + awaitUninterruptibleJobs(); + // A failed join must not release the database out from under Raft. + MetadataFactory.shutdownStore(); + log.info("PD native owners closed"); + } + + private void awaitGrpcTermination() { + if (this.grpcServer == null) { + return; + } + // End long-lived watch/pulse streams as well as accepting no new calls. + this.grpcServer.shutdownNow(); + boolean interrupted = Thread.interrupted(); try { - if (jobs != null) { - long lastPrint = System.currentTimeMillis() - 5000; - log.info("check for ongoing background jobs that cannot be interrupted, active:{}, queue:{}.", - jobs.getActiveCount(), jobs.getQueue().size()); - while (jobs.getActiveCount() != 0 || jobs.getQueue().size() != 0) { - synchronized (ShutdownHook.class) { - if (System.currentTimeMillis() - lastPrint > 5000) { - log.warn(msg); - lastPrint = System.currentTimeMillis(); - } - try { - ShutdownHook.class.wait(200); - } catch (InterruptedException e) { - log.error("close jobs with error:", e); - } + while (!this.grpcServer.isTerminated()) { + try { + if (!this.grpcServer.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("Waiting for PD gRPC transport to terminate"); } + } catch (InterruptedException e) { + interrupted = true; } - log.info("all ongoing background jobs have been completed and the shutdown will continue"); } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } - } catch (Exception e) { - log.error("close jobs with error:", e); + private static void awaitUninterruptibleJobs() { + ThreadPoolExecutor jobs = MetadataService.getUninterruptibleJobs(); + if (jobs == null) { + return; } + jobs.shutdown(); + boolean interrupted = Thread.interrupted(); try { - if (jobs != null) { - jobs.shutdownNow(); + while (!jobs.isTerminated()) { + try { + if (!jobs.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("Waiting for PD metadata jobs, active: {}, queued: {}", + jobs.getActiveCount(), jobs.getQueue().size()); + } + } catch (InterruptedException e) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); } - } catch (Exception e) { - log.error("close jobs with error:", e); } } } diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/metrics/PDMetrics.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/metrics/PDMetrics.java index 793aa24b05..62957ee76d 100644 --- a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/metrics/PDMetrics.java +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/metrics/PDMetrics.java @@ -56,6 +56,7 @@ public final class PDMetrics { private MeterRegistry registry; private Map> lasts = new ConcurrentHashMap(); private int interval = 120 * 1000; + private StoreStateMetrics storeStateMetrics; public synchronized void init(MeterRegistry meterRegistry) { @@ -71,6 +72,11 @@ private void registerMeters() { Gauge.builder(PREFIX + ".graphs", () -> updateGraphs()) .description("Number of graphs registered in PD") .register(registry); + // PDService creates its StoreNodeService in @PostConstruct, so look it up per read + this.storeStateMetrics = new StoreStateMetrics( + registry, + () -> this.pdService.getStoreNodeService().getStores(), + () -> this.pdService.getStoreNodeService().getShardGroups()); Gauge.builder(PREFIX + ".stores", () -> updateStores()) .description("Number of stores registered in PD") .register(registry); @@ -111,23 +117,14 @@ private long updateGraphs() { } private long updateStores() { - return getStores(); + // Also registers the per-store gauges of stores that joined since the last scrape + return this.storeStateMetrics.storeCount(); } private long getGraphs() { return getGraphMetas().size(); } - private long getStores() { - try { - return this.pdService.getStoreNodeService().getStores(null).size(); - } catch (PDException e) { - log.error(e.getMessage(), e); - e.printStackTrace(); - } - return 0; - } - private long setTerms() { List groups = null; try { diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/metrics/StoreStateMetrics.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/metrics/StoreStateMetrics.java new file mode 100644 index 0000000000..4181a34de1 --- /dev/null +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/metrics/StoreStateMetrics.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.hugegraph.pd.metrics; + +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 org.apache.hugegraph.pd.common.PDException; +import org.apache.hugegraph.pd.grpc.Metapb; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.extern.slf4j.Slf4j; + +/** + * Per-store gauges built from the store and shard group metadata PD already keeps: + * {@code hg.store.state{store,state}} is 1 for the store's current state and 0 for the + * others, and {@code hg.store.leader.count{store}} counts the shard groups it leads. + * Stores are registered and removed at runtime, so the meters follow the store list on + * every refresh. The gauges, and {@code hg.stores} through {@link #storeCount()}, trigger + * the refresh themselves at most once per {@link #REFRESH_INTERVAL_MS}, so one scrape reads + * the metadata once. + */ +@Slf4j +final class StoreStateMetrics { + + static final long REFRESH_INTERVAL_MS = 1000; + + @FunctionalInterface + interface Source { + + T get() throws PDException; + } + + private final MeterRegistry registry; + private final Source> stores; + private final Source> shardGroups; + // Guarded by this + private final Map> meters = new HashMap<>(); + private long lastRefresh; + + private volatile Map states = Collections.emptyMap(); + private volatile Map leaders = Collections.emptyMap(); + + StoreStateMetrics(MeterRegistry registry, Source> stores, + Source> shardGroups) { + this.registry = registry; + this.stores = stores; + this.shardGroups = shardGroups; + } + + /** + * Reads the store list and shard groups, registers meters for new stores and removes + * those of stores PD no longer knows. On a read error the previous values are kept. + */ + synchronized void refresh() { + List storeList; + List groups; + try { + storeList = this.stores.get(); + groups = this.shardGroups.get(); + } catch (PDException | RuntimeException e) { + // RuntimeException: a scrape can arrive before PDService finishes init + log.warn("Failed to read stores for store state metrics", e); + return; + } + this.lastRefresh = System.currentTimeMillis(); + + Map newStates = new HashMap<>(); + Map newLeaders = new HashMap<>(); + for (Metapb.Store store : storeList) { + newStates.put(store.getId(), store.getState()); + newLeaders.put(store.getId(), 0); + } + for (Metapb.ShardGroup group : groups) { + for (Metapb.Shard shard : group.getShardsList()) { + if (shard.getRole() == Metapb.ShardRole.Leader && + newLeaders.containsKey(shard.getStoreId())) { + newLeaders.merge(shard.getStoreId(), 1, Integer::sum); + } + } + } + this.states = newStates; + this.leaders = newLeaders; + + for (Long storeId : newStates.keySet()) { + this.meters.computeIfAbsent(storeId, this::registerStore); + } + Iterator>> iter = this.meters.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry> entry = iter.next(); + if (!newStates.containsKey(entry.getKey())) { + entry.getValue().forEach(this.registry::remove); + iter.remove(); + } + } + } + + synchronized void refreshIfStale() { + if (System.currentTimeMillis() - this.lastRefresh >= REFRESH_INTERVAL_MS) { + this.refresh(); + } + } + + private double state(long storeId, Metapb.StoreState state) { + this.refreshIfStale(); + return this.states.get(storeId) == state ? 1 : 0; + } + + /** + * The number of stores PD had at the last refresh, which hg.stores reports so a scrape + * reads the store list once + */ + long storeCount() { + this.refreshIfStale(); + return this.states.size(); + } + + private double leaderCount(long storeId) { + this.refreshIfStale(); + Integer count = this.leaders.get(storeId); + return count == null ? Double.NaN : count; + } + + private List registerStore(long storeId) { + String store = String.valueOf(storeId); + List list = new ArrayList<>(); + for (Metapb.StoreState state : Metapb.StoreState.values()) { + if (state == Metapb.StoreState.UNRECOGNIZED) { + continue; + } + list.add(Gauge.builder(PDMetrics.PREFIX + ".store.state", this, + m -> m.state(storeId, state)) + .description("1 if the store is in this state, 0 otherwise") + .tag("store", store) + .tag("state", state.name()) + .register(this.registry)); + } + list.add(Gauge.builder(PDMetrics.PREFIX + ".store.leader.count", this, + m -> m.leaderCount(storeId)) + .description("Number of shard groups whose leader is on the store") + .tag("store", store) + .register(this.registry)); + return list; + } +} diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/pulse/PDPulseSubject.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/pulse/PDPulseSubject.java index 86b95cb880..58d886abee 100644 --- a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/pulse/PDPulseSubject.java +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/pulse/PDPulseSubject.java @@ -89,6 +89,27 @@ public class PDPulseSubject { TimeUnit.SECONDS); } + /** Stop periodic queue work and wait while Raft can still serve its writes. */ + public static void stopScheduling() { + scheduledExecutor.shutdown(); + boolean interrupted = Thread.interrupted(); + try { + while (!scheduledExecutor.isTerminated()) { + try { + if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("Waiting for PD pulse scheduler to stop"); + } + } catch (InterruptedException e) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + private static void doSchedule() { appendQueue(); expireQueue(); diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/StoreAPI.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/StoreAPI.java index 4fcf3660f5..e4fea9d16e 100644 --- a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/StoreAPI.java +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/StoreAPI.java @@ -17,7 +17,6 @@ package org.apache.hugegraph.pd.rest; -import java.io.Serializable; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -49,6 +48,7 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import com.alipay.sofa.jraft.core.State; import com.google.protobuf.util.JsonFormat; import lombok.Data; @@ -388,16 +388,22 @@ class StoreStatistics { * This interface is used to check the health status of the service by accessing the /health * path via a GET request. *

- * This is a liveness signal only: it answers 200 as soon as the REST listener is up and - * does not consult the raft state. Use {@link #checkReady()} to find out whether this PD - * can actually serve. + * This liveness signal answers 503 for a fatal raft error or a stopped state machine. + * Elections and temporary quorum loss remain healthy. Use {@link #checkReady()} to find + * out whether this PD can actually serve. * * @return Returns a string indicating the service's health status. Typically, an empty * string indicates the service is healthy. */ + // TODO: bounded metadata-store LOCK retry is tracked separately in + // https://github.com/apache/hugegraph/issues/3226; this probe reads only Raft state. @GetMapping(value = "/health", produces = MediaType.TEXT_PLAIN_VALUE) - public Serializable checkHealthy() { - return ""; + public ResponseEntity checkHealthy() { + String state = RaftEngine.getInstance().getRaftStatus().getState(); + boolean failed = State.STATE_ERROR.name().equals(state) || + State.STATE_SHUTDOWN.name().equals(state); + return ResponseEntity.status(failed ? HttpStatus.SERVICE_UNAVAILABLE : HttpStatus.OK) + .body(""); } /** diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/TaskAPI.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/TaskAPI.java index 60aad2d554..eaa53908d1 100644 --- a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/TaskAPI.java +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/rest/TaskAPI.java @@ -89,6 +89,12 @@ public String splitPartitions() { } } + // TODO: make the task routes tell a real run from a no-op and a follower answer (a follower + // returns an empty success without doing anything), and return a body that names what was + // scheduled. Part 1 (#3233) replaced the bare 500 with a refusal body; this is part 2. The + // Helm chart (helm/hugegraph) documents the leader-first sequence and the 180 s spacing in its + // README until then. + // https://github.com/apache/hugegraph/issues/3231 @GetMapping(value = "/balanceLeaders", produces = MediaType.APPLICATION_JSON_VALUE) @ResponseBody public String balanceLeaders() { diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/KvServiceGrpcImpl.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/KvServiceGrpcImpl.java index e49a4dc48b..94b41a0f0a 100644 --- a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/KvServiceGrpcImpl.java +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/KvServiceGrpcImpl.java @@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicLong; import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; import org.apache.hugegraph.pd.KvService; import org.apache.hugegraph.pd.common.PDException; @@ -86,6 +87,32 @@ public void init() { }, 0, KvWatchSubject.WATCH_TTL * 1 / 3, TimeUnit.MILLISECONDS); } + /** Stop periodic watch work and wait while Raft can still serve its writes. */ + @PreDestroy + public void stopScheduling() { + ScheduledExecutorService current = this.executor; + if (current == null) { + return; + } + current.shutdown(); + boolean interrupted = Thread.interrupted(); + try { + while (!current.isTerminated()) { + try { + if (!current.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("Waiting for PD KV watch scheduler to stop"); + } + } catch (InterruptedException e) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + /** * Ordinary put * diff --git a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/PDService.java b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/PDService.java index 94d136a844..f40b2f5801 100644 --- a/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/PDService.java +++ b/hugegraph-pd/hg-pd-service/src/main/java/org/apache/hugegraph/pd/service/PDService.java @@ -53,6 +53,7 @@ import org.apache.hugegraph.pd.StoreNodeService; import org.apache.hugegraph.pd.StoreStatusListener; import org.apache.hugegraph.pd.TaskScheduleService; +import org.apache.hugegraph.pd.boot.ShutdownHook; import org.apache.hugegraph.pd.common.KVPair; import org.apache.hugegraph.pd.common.PDException; import org.apache.hugegraph.pd.config.PDConfig; @@ -177,6 +178,22 @@ public void setInitConfig(PDConfig pdConfig) { */ @PostConstruct public void init() throws PDException { + try { + initService(); + } catch (PDException | RuntimeException | Error failure) { + // Failed @PostConstruct beans are not registered for destruction. + try { + ShutdownHook.closeOwners(this); + } catch (RuntimeException | Error cleanup) { + if (cleanup != failure) { + failure.addSuppressed(cleanup); + } + } + throw failure; + } + } + + private void initService() throws PDException { log.info("PDService init………… {}", pdConfig); configService = new ConfigService(pdConfig); diff --git a/hugegraph-pd/hg-pd-service/src/main/resources/application.yml b/hugegraph-pd/hg-pd-service/src/main/resources/application.yml index f8c1ecea84..fb44a3a4c1 100644 --- a/hugegraph-pd/hg-pd-service/src/main/resources/application.yml +++ b/hugegraph-pd/hg-pd-service/src/main/resources/application.yml @@ -72,6 +72,12 @@ raft: peers-list: 127.0.0.1:8610 # The read and write timeout period of the raft rpc, in milliseconds rpc-timeout: 10000 + # Timeout for opening a connection to a peer, in milliseconds; an election waits this + # long for each peer that accepts the connection but does not answer + rpc-connect-timeout: 1000 + # Timeout for sending a snapshot to a follower that fell behind the log, in milliseconds; + # 0 uses rpc-timeout + rpc-install-snapshot-timeout: 0 # The interval between snapshot generation, in seconds snapshotInterval: 300 metrics: true diff --git a/hugegraph-pd/hg-pd-service/src/test/java/org/apache/hugegraph/pd/metrics/StoreStateMetricsTest.java b/hugegraph-pd/hg-pd-service/src/test/java/org/apache/hugegraph/pd/metrics/StoreStateMetricsTest.java new file mode 100644 index 0000000000..79a01e13af --- /dev/null +++ b/hugegraph-pd/hg-pd-service/src/test/java/org/apache/hugegraph/pd/metrics/StoreStateMetricsTest.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.hugegraph.pd.metrics; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hugegraph.pd.common.PDException; +import org.apache.hugegraph.pd.grpc.Metapb; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.prometheus.PrometheusConfig; +import io.micrometer.prometheus.PrometheusMeterRegistry; + +public class StoreStateMetricsTest { + + private SimpleMeterRegistry registry; + private List stores; + private List groups; + private boolean failRead; + private StoreStateMetrics metrics; + + @Before + public void setup() { + this.registry = new SimpleMeterRegistry(); + this.stores = new ArrayList<>(); + this.groups = new ArrayList<>(); + this.failRead = false; + this.metrics = new StoreStateMetrics(this.registry, () -> { + if (this.failRead) { + throw new PDException(-1, "raft is not ready"); + } + return this.stores; + }, () -> this.groups); + } + + @Test + public void testStateAndLeaderCount() { + this.stores.add(store(1, Metapb.StoreState.Up)); + this.stores.add(store(2, Metapb.StoreState.Up)); + this.stores.add(store(3, Metapb.StoreState.Offline)); + // store 3 went down, so both groups elected leaders on store 1 + this.groups.add(group(1, 1, 2, 3)); + this.groups.add(group(2, 1, 2, 3)); + this.metrics.refresh(); + + Assert.assertEquals(1.0, state(1, "Up"), 0); + Assert.assertEquals(0.0, state(1, "Offline"), 0); + Assert.assertEquals(1.0, state(3, "Offline"), 0); + Assert.assertEquals(0.0, state(3, "Up"), 0); + Assert.assertEquals(2.0, leaders(1), 0); + Assert.assertEquals(0.0, leaders(2), 0); + Assert.assertEquals(0.0, leaders(3), 0); + // One gauge per state except UNRECOGNIZED, for each store + int states = Metapb.StoreState.values().length - 1; + Assert.assertEquals(3 * states, + this.registry.find("hg.store.state").gauges().size()); + } + + @Test + public void testStoresJoinAndLeave() { + this.stores.add(store(1, Metapb.StoreState.Up)); + this.metrics.refresh(); + Assert.assertNull(gauge("hg.store.leader.count", 2)); + + this.stores.add(store(2, Metapb.StoreState.Pending)); + this.metrics.refresh(); + Assert.assertEquals(1.0, state(2, "Pending"), 0); + + this.stores.remove(0); + this.metrics.refresh(); + Assert.assertNull(gauge("hg.store.leader.count", 1)); + Assert.assertTrue(this.registry.find("hg.store.state").tag("store", "1") + .gauges().isEmpty()); + Assert.assertNotNull(gauge("hg.store.leader.count", 2)); + } + + @Test + public void testReadErrorKeepsLastValues() { + this.stores.add(store(1, Metapb.StoreState.Up)); + this.metrics.refresh(); + + this.failRead = true; + this.stores.clear(); + this.metrics.refresh(); + Assert.assertEquals(1.0, state(1, "Up"), 0); + } + + @Test + public void testPrometheusScrape() { + var prometheus = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + var reads = new AtomicInteger(); + var scraped = new StoreStateMetrics(prometheus, () -> { + reads.incrementAndGet(); + return this.stores; + }, () -> this.groups); + // As hg.stores in PDMetrics: the refresh registers meters while a scrape runs + Gauge.builder("hg.stores", scraped::storeCount).register(prometheus); + this.stores.add(store(7, Metapb.StoreState.Up)); + this.groups.add(group(1, 7)); + + prometheus.scrape(); + String body = prometheus.scrape(); + // Both scrapes fall in one refresh interval, and hg.stores reuses the refreshed list + Assert.assertEquals(1, reads.get()); + Assert.assertTrue(body, body.contains("hg_stores 1.0")); + Assert.assertTrue(body, body.contains("hg_store_state{state=\"Up\",store=\"7\",} 1.0")); + Assert.assertTrue(body, + body.contains("hg_store_state{state=\"Offline\",store=\"7\",} 0.0")); + Assert.assertTrue(body, body.contains("hg_store_leader_count{store=\"7\",} 1.0")); + } + + private double state(long storeId, String state) { + return this.registry.get("hg.store.state") + .tag("store", String.valueOf(storeId)) + .tag("state", state) + .gauge().value(); + } + + private double leaders(long storeId) { + return gauge("hg.store.leader.count", storeId).value(); + } + + private Gauge gauge(String name, long storeId) { + return this.registry.find(name).tag("store", String.valueOf(storeId)).gauge(); + } + + private static Metapb.Store store(long id, Metapb.StoreState state) { + return Metapb.Store.newBuilder().setId(id).setState(state).build(); + } + + private static Metapb.ShardGroup group(int id, long leader, long... followers) { + var builder = Metapb.ShardGroup.newBuilder().setId(id); + builder.addShards(Metapb.Shard.newBuilder().setStoreId(leader) + .setRole(Metapb.ShardRole.Leader)); + for (long follower : followers) { + if (follower != leader) { + builder.addShards(Metapb.Shard.newBuilder().setStoreId(follower) + .setRole(Metapb.ShardRole.Follower)); + } + } + return builder.build(); + } +} diff --git a/hugegraph-pd/hg-pd-service/src/test/java/org/apache/hugegraph/pd/rest/StoreAPIReadyTest.java b/hugegraph-pd/hg-pd-service/src/test/java/org/apache/hugegraph/pd/rest/StoreAPIReadyTest.java index 75e9581cb1..1391715fef 100644 --- a/hugegraph-pd/hg-pd-service/src/test/java/org/apache/hugegraph/pd/rest/StoreAPIReadyTest.java +++ b/hugegraph-pd/hg-pd-service/src/test/java/org/apache/hugegraph/pd/rest/StoreAPIReadyTest.java @@ -35,6 +35,7 @@ import com.alipay.sofa.jraft.core.State; import com.alipay.sofa.jraft.entity.LeaderChangeContext; import com.alipay.sofa.jraft.entity.PeerId; +import com.alipay.sofa.jraft.error.RaftException; /** * Pins the status a readiness probe actually receives from {@code GET /v1/ready}: 503 while @@ -120,6 +121,83 @@ private static LeaderChangeContext context() { return new LeaderChangeContext(LEADER, 5, Status.OK()); } + @Test + public void testFatalErrorSurvivesLeaderStopAndAnswers503() { + Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", untouchableNode()); + this.stateMachine.onLeaderStart(5); + this.stateMachine.onError(new RaftException()); + this.stateMachine.onLeaderStop(new Status(-1, "snapshot failed")); + + Assert.assertEquals(HttpStatus.SERVICE_UNAVAILABLE, + this.api.checkHealthy().getStatusCode()); + Assert.assertEquals("", this.api.checkHealthy().getBody()); + ResponseEntity> response = this.api.checkReady(); + Assert.assertEquals(HttpStatus.SERVICE_UNAVAILABLE, response.getStatusCode()); + Assert.assertEquals(State.STATE_ERROR.name(), response.getBody().get("state")); + Assert.assertEquals(Boolean.FALSE, response.getBody().get("isLeader")); + } + + @Test + public void testQuorumLossRemainsLive() { + Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", untouchableNode()); + this.stateMachine.onLeaderStart(5); + Assert.assertEquals(HttpStatus.OK, this.api.checkHealthy().getStatusCode()); + this.stateMachine.onLeaderStop(Status.OK()); + Assert.assertEquals(HttpStatus.OK, this.api.checkHealthy().getStatusCode()); + this.stateMachine.onStartFollowing(context()); + Assert.assertEquals(HttpStatus.OK, this.api.checkHealthy().getStatusCode()); + this.stateMachine.onStopFollowing(context()); + Assert.assertEquals(HttpStatus.OK, this.api.checkHealthy().getStatusCode()); + Assert.assertEquals(HttpStatus.SERVICE_UNAVAILABLE, + this.api.checkReady().getStatusCode()); + } + + @Test + public void testQueuedFollowerCallbacksCannotRecoverFatalError() { + Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", untouchableNode()); + this.stateMachine.onLeaderStart(5); + this.stateMachine.onError(new RaftException()); + this.stateMachine.onLeaderStop(Status.OK()); + this.stateMachine.onStartFollowing(context()); + this.stateMachine.onStopFollowing(context()); + assertTerminal(State.STATE_ERROR); + } + + @Test + public void testQueuedLeaderStartCannotRecoverFatalError() { + Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", untouchableNode()); + this.stateMachine.onError(new RaftException()); + this.stateMachine.onLeaderStart(6); + this.stateMachine.onLeaderStop(Status.OK()); + assertTerminal(State.STATE_ERROR); + } + + @Test + public void testQueuedTransitionsCannotRecoverShutdown() { + Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", untouchableNode()); + this.stateMachine.onShutdown(); + this.stateMachine.onLeaderStart(6); + this.stateMachine.onLeaderStop(Status.OK()); + this.stateMachine.onStartFollowing(context()); + this.stateMachine.onStopFollowing(context()); + assertTerminal(State.STATE_SHUTDOWN); + } + + private void assertTerminal(State state) { + Assert.assertEquals(HttpStatus.SERVICE_UNAVAILABLE, this.api.checkHealthy().getStatusCode()); + ResponseEntity> response = this.api.checkReady(); + Assert.assertEquals(HttpStatus.SERVICE_UNAVAILABLE, response.getStatusCode()); + Assert.assertEquals(state.name(), response.getBody().get("state")); + Assert.assertEquals(Boolean.FALSE, response.getBody().get("isLeader")); + } + + @Test + public void testShutdownIsNotLive() { + this.stateMachine.onShutdown(); + Assert.assertEquals(HttpStatus.SERVICE_UNAVAILABLE, + this.api.checkHealthy().getStatusCode()); + } + /** * A started raft node that fails the test if the endpoint calls it. Readiness is served * from the state machine callbacks precisely so that it never waits on the node lock. diff --git a/hugegraph-pd/hg-pd-test/pom.xml b/hugegraph-pd/hg-pd-test/pom.xml index 72cdef81aa..bca134ce4e 100644 --- a/hugegraph-pd/hg-pd-test/pom.xml +++ b/hugegraph-pd/hg-pd-test/pom.xml @@ -32,7 +32,6 @@ true - 2.0.0-RC.3 @@ -46,7 +45,6 @@ org.jacoco jacoco-maven-plugin - 0.8.8 coverage-report @@ -80,12 +78,12 @@ org.springframework spring-context-support - 5.3.20 + ${spring-framework.version} org.springframework spring-test - 5.3.20 + ${spring-framework.version} test @@ -139,23 +137,20 @@ com.fasterxml.jackson.core jackson-databind - 2.13.0 com.fasterxml.jackson.core jackson-core - 2.13.0 com.fasterxml.jackson.core jackson-annotations - 2.13.0 org.springframework.boot spring-boot-starter-test - 2.5.14 + ${spring-boot.version} org.springframework.boot @@ -164,39 +159,6 @@ - - org.powermock - powermock-classloading-xstream - ${powermock.version} - - - org.powermock - powermock-module-junit4-rule - ${powermock.version} - - - org.powermock - powermock-api-support - ${powermock.version} - - - org.powermock - powermock-module-junit4 - 2.0.0-RC.3 - compile - - - org.powermock - powermock-api-mockito2 - 2.0.0-RC.3 - compile - - - org.apache.tinkerpop - gremlin-shaded - 3.5.1 - compile - @@ -204,11 +166,24 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 + + + @{argLine} + "@${project.basedir}/../hg-pd-dist/src/assembly/static/bin/jvm-module.options" + + + + + org.apache.maven.surefire + surefire-junit4 + ${maven.surefire.plugin.version} + + pd-client-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -221,6 +196,7 @@ pd-core-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -233,6 +209,7 @@ pd-common-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -245,6 +222,7 @@ pd-rest-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -259,7 +237,6 @@ org.jacoco jacoco-maven-plugin - 0.8.8 org/apache/hugegraph/pd/rest/*.class diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/client/PDClientTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/client/PDClientTest.java index 3676122612..3e7bbda39a 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/client/PDClientTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/client/PDClientTest.java @@ -21,7 +21,7 @@ import java.util.ArrayList; import java.util.List; -import org.apache.tinkerpop.shaded.minlog.Log; +import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -323,14 +323,12 @@ public void testChangePeerList() { @Test public void testSplitData() { try { - Metapb.PDConfig config = pdClient.getPDConfig(); - pdClient.setPDConfig(config.toBuilder() - .setMaxShardsPerStore(12) - .build()); - System.out.println(pdClient.getPDConfig()); pdClient.splitData(); + Assert.fail("Expected splitData() to reject an unready cluster"); } catch (PDException e) { - Log.error("testSplitData", e); + Assert.assertEquals( + Pdpb.ErrorType.Cluster_State_Forbid_Splitting_VALUE, + e.getErrorCode()); } } diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/LogServiceTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/LogServiceTest.java index 9588d6d171..0aea282cb8 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/LogServiceTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/LogServiceTest.java @@ -40,11 +40,11 @@ public void setUp() { public void testGetLog() throws Exception { this.logServiceUnderTest.insertLog("action", "message", Any.newBuilder().build()); - // Run the test + // The query end is exclusive, so include logs inserted in the current millisecond. final List result = - this.logServiceUnderTest.getLog("action", 0L, System.currentTimeMillis()); + this.logServiceUnderTest.getLog("action", 0L, System.currentTimeMillis() + 1L); // Verify the results - Assert.assertEquals(result.size(), 1); + Assert.assertEquals(1, result.size()); } } diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java index ddc1fad7f9..c0c15975e8 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreSuiteTest.java @@ -17,12 +17,21 @@ package org.apache.hugegraph.pd.core; +import org.apache.hugegraph.pd.core.meta.IdMetaStoreReadIndexTest; import org.apache.hugegraph.pd.core.meta.MetadataKeyHelperTest; import org.apache.hugegraph.pd.core.store.HgKVStoreImplTest; +import org.apache.hugegraph.pd.core.store.RaftKVStoreTest; import org.apache.hugegraph.pd.raft.IpAuthHandlerTest; +import org.apache.hugegraph.pd.raft.KVOperationTest; +import org.apache.hugegraph.pd.raft.PeerUtilTest; import org.apache.hugegraph.pd.raft.RaftEngineIpAuthIntegrationTest; import org.apache.hugegraph.pd.raft.RaftEngineLeaderAddressTest; +import org.apache.hugegraph.pd.raft.RaftEngineReadIndexTest; import org.apache.hugegraph.pd.raft.RaftEngineReadinessTest; +import org.apache.hugegraph.pd.raft.RaftEngineRpcTimeoutTest; +import org.apache.hugegraph.pd.raft.RaftReflectionUtilTest; +import org.apache.hugegraph.pd.raft.RaftStateMachineSnapshotTest; +import org.apache.hugegraph.pd.raft.ZipUtilsTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -32,7 +41,9 @@ @Suite.SuiteClasses({ RaftStateMachineTest.class, MetadataKeyHelperTest.class, + IdMetaStoreReadIndexTest.class, HgKVStoreImplTest.class, + PDNativeOwnerTest.class, PDConfigTest.class, ConfigServiceTest.class, IdServiceTest.class, @@ -46,6 +57,14 @@ RaftEngineIpAuthIntegrationTest.class, RaftEngineLeaderAddressTest.class, RaftEngineReadinessTest.class, + RaftReflectionUtilTest.class, + KVOperationTest.class, + PeerUtilTest.class, + ZipUtilsTest.class, + RaftKVStoreTest.class, + RaftEngineReadIndexTest.class, + RaftStateMachineSnapshotTest.class, + RaftEngineRpcTimeoutTest.class, // StoreNodeServiceTest.class, }) @Slf4j diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDNativeOwnerTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDNativeOwnerTest.java new file mode 100644 index 0000000000..e7c6c1d050 --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDNativeOwnerTest.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. + */ + +package org.apache.hugegraph.pd.core; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReadWriteLock; + +import org.apache.hugegraph.pd.config.PDConfig; +import org.apache.hugegraph.pd.meta.MetadataFactory; +import org.apache.hugegraph.pd.raft.RaftEngine; +import org.apache.hugegraph.pd.raft.RaftRpcClient; +import org.apache.hugegraph.pd.raft.RaftStateMachine; +import org.apache.hugegraph.pd.store.HgKVStore; +import org.apache.hugegraph.pd.store.HgKVStoreImpl; +import org.apache.hugegraph.testutil.Whitebox; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; + +import com.alipay.sofa.jraft.Node; +import com.alipay.sofa.jraft.RaftGroupService; +import com.alipay.sofa.jraft.rpc.RpcServer; + +import static org.mockito.Mockito.doAnswer; +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; + +public class PDNativeOwnerTest { + + @Rule + public TemporaryFolder temporary = new TemporaryFolder(); + + @Test(timeout = 10000) + public void testCloseWaitsForReadersAndReleasesNativeOwners() throws Exception { + RocksDB.loadLibrary(); + Path path = this.temporary.newFolder().toPath(); + PDConfig config = new PDConfig(); + config.setDataPath(path.toString()); + HgKVStoreImpl store = new HgKVStoreImpl(); + store.init(config); + RocksDB db = Whitebox.getInternalState(store, "db"); + Options options = Whitebox.getInternalState(store, "dbOptions"); + ReadWriteLock lock = Whitebox.getInternalState(store, "readWriteLock"); + store.put(new byte[]{1}, new byte[]{2}); + store.put(new byte[]{3}, new byte[]{4}); + Assert.assertEquals(1, store.scanPrefix(new byte[]{1}).size()); + Assert.assertEquals(2, store.scanRange(new byte[]{0}, new byte[]{4}).size()); + store.removeByPrefix(new byte[]{1}); + Assert.assertEquals(0, store.scanPrefix(new byte[]{1}).size()); + CountDownLatch closing = new CountDownLatch(1); + FutureTask close = new FutureTask<>(() -> { + closing.countDown(); + store.close(); + return null; + }); + Thread closer = new Thread(close, "pd-owner-close"); + lock.readLock().lock(); + try { + closer.start(); + Assert.assertTrue(closing.await(1, TimeUnit.SECONDS)); + Assert.assertThrows(TimeoutException.class, () -> close.get(50, TimeUnit.MILLISECONDS)); + Assert.assertTrue(db.isOwningHandle()); + Assert.assertTrue(options.isOwningHandle()); + } finally { + lock.readLock().unlock(); + } + close.get(2, TimeUnit.SECONDS); + store.close(); + Assert.assertFalse(db.isOwningHandle()); + Assert.assertFalse(options.isOwningHandle()); + HgKVStoreImpl reopened = new HgKVStoreImpl(); + reopened.init(config); + try { + Assert.assertArrayEquals(new byte[]{4}, reopened.get(new byte[]{3})); + } finally { + reopened.close(); + } + } + + @Test + public void testInitializationFailureReleasesOptions() { + RocksDB.loadLibrary(); + HgKVStoreImpl store = new HgKVStoreImpl(); + PDConfig config = mock(PDConfig.class); + AtomicReference allocated = new AtomicReference<>(); + IllegalStateException injected = new IllegalStateException("injected configuration failure"); + when(config.getDataPath()).thenAnswer(invocation -> { + allocated.set(Whitebox.getInternalState(store, "dbOptions")); + throw injected; + }); + IllegalStateException failure = Assert.assertThrows(IllegalStateException.class, () -> store.init(config)); + Assert.assertSame(injected, failure.getCause()); + Assert.assertFalse(allocated.get().isOwningHandle()); + store.close(); + } + + @Test + public void testTerminalFactoryCloseRejectsLazyReopen() throws Exception { + Field storeField = MetadataFactory.class.getDeclaredField("store"); + Field shutdownField = MetadataFactory.class.getDeclaredField("shutdown"); + storeField.setAccessible(true); + shutdownField.setAccessible(true); + synchronized (MetadataFactory.class) { + Object previous = storeField.get(null); + boolean shutdown = shutdownField.getBoolean(null); + HgKVStore owned = mock(HgKVStore.class); + try { + storeField.set(null, owned); + shutdownField.setBoolean(null, false); + MetadataFactory.shutdownStore(); + MetadataFactory.shutdownStore(); + verify(owned, times(1)).close(); + Assert.assertThrows(IllegalStateException.class, () -> MetadataFactory.getStore(null)); + } finally { + storeField.set(null, previous); + shutdownField.setBoolean(null, shutdown); + } + } + } + + @Test + public void testInterruptedRaftJoinRetainsOwnerAndClosesRpcClient() throws Exception { + RaftEngine engine = newEngine(); + RaftGroupService group = mock(RaftGroupService.class); + Node node = mock(Node.class); + RpcServer server = mock(RpcServer.class); + RaftRpcClient client = mock(RaftRpcClient.class); + Whitebox.setInternalState(engine, "raftGroupService", group); + Whitebox.setInternalState(engine, "raftNode", node); + Whitebox.setInternalState(engine, "rpcServer", server); + Whitebox.setInternalState(engine, "raftRpcClient", client); + AtomicInteger attempts = new AtomicInteger(); + doAnswer(invocation -> { + Assert.assertSame(node, engine.getRaftNode()); + if (attempts.incrementAndGet() == 1) { + throw new InterruptedException("injected join interruption"); + } + return null; + }).when(group).join(); + try { + engine.shutDown(); + Assert.assertTrue(Thread.interrupted()); + Assert.assertEquals(2, attempts.get()); + Assert.assertNull(engine.getRaftNode()); + engine.shutDown(); + verify(group, times(1)).shutdown(); + verify(node, never()).shutdown(); + verify(server, times(1)).shutdown(); + verify(client, times(1)).shutdown(); + } finally { + Thread.interrupted(); + } + } + + @Test(timeout = 10000) + public void testLeaderCallbacksDrainBeforeStoppingRaft() throws Exception { + RaftEngine engine = newEngine(); + RaftStateMachine state = Whitebox.getInternalState(engine, "stateMachine"); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicInteger calls = new AtomicInteger(); + state.addStateListener(() -> { + calls.incrementAndGet(); + entered.countDown(); + try { + Assert.assertTrue(release.await(3, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + }); + state.onLeaderStart(1); + Assert.assertTrue(entered.await(1, TimeUnit.SECONDS)); + FutureTask stop = new FutureTask<>(() -> { + engine.stopLeaderCallbacks(); + return null; + }); + Thread stopper = new Thread(stop, "pd-leader-drain"); + try { + stopper.start(); + Assert.assertThrows(TimeoutException.class, () -> stop.get(50, TimeUnit.MILLISECONDS)); + release.countDown(); + stop.get(2, TimeUnit.SECONDS); + state.onLeaderStart(2); + engine.stopLeaderCallbacks(); + Assert.assertEquals(1, calls.get()); + } finally { + release.countDown(); + stopper.join(3000); + } + } + + private static RaftEngine newEngine() throws Exception { + Constructor constructor = RaftEngine.class.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } +} diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/meta/IdMetaStoreReadIndexTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/meta/IdMetaStoreReadIndexTest.java new file mode 100644 index 0000000000..e3d9dfb2ad --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/meta/IdMetaStoreReadIndexTest.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.hugegraph.pd.core.meta; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.hugegraph.pd.common.PDException; +import org.apache.hugegraph.pd.config.PDConfig; +import org.apache.hugegraph.pd.grpc.Pdpb; +import org.apache.hugegraph.pd.meta.IdMetaStore; +import org.apache.hugegraph.pd.meta.MetadataFactory; +import org.apache.hugegraph.pd.raft.KVOperation; +import org.apache.hugegraph.pd.raft.RaftEngine; +import org.apache.hugegraph.pd.store.HgKVStore; +import org.apache.hugegraph.pd.store.KV; +import org.apache.hugegraph.pd.store.RaftKVStore; +import org.apache.hugegraph.testutil.Whitebox; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import com.alipay.sofa.jraft.Status; +import com.alipay.sofa.jraft.entity.Task; +import com.alipay.sofa.jraft.error.RaftError; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +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; + +/** + * {@link IdMetaStore#getId} and both {@code getCId} overloads read local state and then write + * through raft as two steps, so the read must follow a raft ReadIndex: a leader elected a + * moment ago may not have applied its predecessor's last write yet, and would hand out the + * same id twice. The write must also carry the leader term of the read: a request that pauses + * between the two could otherwise write its stale result after the node lost the leadership + * and won it back. + */ +public class IdMetaStoreReadIndexTest { + + private HgKVStore originalStore; + private HgKVStore store; + private IdMetaStore idMetaStore; + + @Before + public void setUp() { + // The metadata stores share one factory-held store; swap in a mock for this test + this.originalStore = Whitebox.getInternalState(MetadataFactory.class, "store"); + this.store = mock(HgKVStore.class); + Whitebox.setInternalState(MetadataFactory.class, "store", this.store); + this.idMetaStore = new IdMetaStore(new PDConfig()); + } + + @After + public void tearDown() { + Whitebox.setInternalState(MetadataFactory.class, "store", this.originalStore); + } + + @Test + public void testGetIdReadsOnlyAfterTheReadIndex() throws PDException { + when(this.store.waitReadIndexInTerm()).thenReturn(7L); + when(this.store.get(any())).thenReturn(IdMetaStore.longToBytes(100L)); + + Assert.assertEquals(100L, this.idMetaStore.getId("read-index", 10)); + + InOrder order = inOrder(this.store); + order.verify(this.store).waitReadIndexInTerm(); + order.verify(this.store).get(any()); + // The write carries the term of the read + order.verify(this.store).put(any(), any(), eq(7L)); + verify(this.store, never()).put(any(), any()); + } + + @Test + public void testGetIdLeavesTheCounterAloneWhenTheReadIndexFails() throws PDException { + doThrow(new PDException(Pdpb.ErrorType.NOT_LEADER_VALUE, "not leader")) + .when(this.store).waitReadIndexInTerm(); + + PDException e = Assert.assertThrows(PDException.class, () -> { + this.idMetaStore.getId("read-index", 10); + }); + + Assert.assertEquals(Pdpb.ErrorType.NOT_LEADER_VALUE, e.getErrorCode()); + verify(this.store, times(1)).waitReadIndexInTerm(); + verify(this.store, never()).get(any()); + verify(this.store, never()).put(any(), any()); + verify(this.store, never()).put(any(), any(), anyLong()); + } + + @Test + public void testGetCIdReadsOnlyAfterTheReadIndex() throws PDException { + when(this.store.waitReadIndexInTerm()).thenReturn(7L); + when(this.store.get(any())).thenReturn(IdMetaStore.longToBytes(5L)); + + Assert.assertEquals(5L, this.idMetaStore.getCId("read-index", 16)); + + InOrder order = inOrder(this.store); + order.verify(this.store).waitReadIndexInTerm(); + order.verify(this.store).get(any()); + order.verify(this.store).scanRange(any(), any()); + order.verify(this.store, times(2)).put(any(), any(), eq(7L)); + verify(this.store, never()).put(any(), any()); + } + + @Test + public void testGetCIdByNameReadsTheDelayedSlotOnlyAfterTheReadIndex() + throws PDException { + // A minimal in-memory store, so the delayed slot is written the way PD writes it + Map data = new HashMap<>(); + doAnswer(inv -> data.put(ByteBuffer.wrap(inv.getArgument(0)), inv.getArgument(1))) + .when(this.store).put(any(), any()); + when(this.store.get(any())).thenAnswer(inv -> { + return data.get(ByteBuffer.wrap(inv.getArgument(0))); + }); + this.idMetaStore.delCIdDelay("read-index", "graph", 7L); + clearInvocations(this.store); + when(this.store.waitReadIndexInTerm()).thenReturn(7L); + + Assert.assertEquals(7L, this.idMetaStore.getCId("read-index", "graph", 16)); + + InOrder order = inOrder(this.store); + order.verify(this.store).waitReadIndexInTerm(); + order.verify(this.store).scanPrefix(any()); + order.verify(this.store).get(any()); + order.verify(this.store).remove(any(), eq(7L)); + verify(this.store, times(1)).waitReadIndexInTerm(); + verify(this.store, never()).remove(any()); + verify(this.store, never()).put(any(), any()); + verify(this.store, never()).put(any(), any(), anyLong()); + } + + @Test + public void testGetCIdByNameWithoutADelayedSlotWaitsOnce() throws PDException { + // A second wait would return the second term + when(this.store.waitReadIndexInTerm()).thenReturn(7L, 8L); + + Assert.assertEquals(0L, this.idMetaStore.getCId("read-index", "graph", 16)); + + // The new cid is written in the term of the wait that covered the delayed slot + verify(this.store, times(1)).waitReadIndexInTerm(); + verify(this.store, times(2)).put(any(), any(), eq(7L)); + verify(this.store, never()).put(any(), any(), eq(8L)); + verify(this.store, never()).put(any(), any()); + } + + @Test + public void testGetCIdLeavesTheSlotsAloneWhenTheReadIndexFails() throws PDException { + doThrow(new PDException(Pdpb.ErrorType.NOT_LEADER_VALUE, "not leader")) + .when(this.store).waitReadIndexInTerm(); + + PDException e = Assert.assertThrows(PDException.class, () -> { + this.idMetaStore.getCId("read-index", 16); + }); + Assert.assertEquals(Pdpb.ErrorType.NOT_LEADER_VALUE, e.getErrorCode()); + e = Assert.assertThrows(PDException.class, () -> { + this.idMetaStore.getCId("read-index", "graph", 16); + }); + Assert.assertEquals(Pdpb.ErrorType.NOT_LEADER_VALUE, e.getErrorCode()); + + verify(this.store, never()).get(any()); + verify(this.store, never()).scanPrefix(any()); + verify(this.store, never()).scanRange(any(), any()); + verify(this.store, never()).put(any(), any()); + verify(this.store, never()).put(any(), any(), anyLong()); + verify(this.store, never()).remove(any()); + verify(this.store, never()).remove(any(), anyLong()); + } + + @Test + public void testRaftStoreWaitsOnTheRaftEngine() throws PDException { + RaftEngine engine = mock(RaftEngine.class); + HgKVStore local = mock(HgKVStore.class); + + new RaftKVStore(engine, local).waitReadIndex(); + + verify(engine).waitReadIndex(); + } + + @Test + public void testRaftStoreBindsOnlyATermWriteToItsTerm() throws PDException { + RaftEngine engine = mock(RaftEngine.class); + when(engine.waitReadIndexInTerm()).thenReturn(7L); + List expectedTerms = new ArrayList<>(); + doAnswer(inv -> { + Task task = inv.getArgument(0); + expectedTerms.add(task.getExpectedTerm()); + task.getDone().run(Status.OK()); + return null; + }).when(engine).addTask(any()); + RaftKVStore raftStore = new RaftKVStore(engine, mock(HgKVStore.class)); + + Assert.assertEquals(7L, raftStore.waitReadIndexInTerm()); + raftStore.put(new byte[]{1}, new byte[]{1}, 7L); + raftStore.remove(new byte[]{1}, 7L); + // -1 is jraft's "any term": the writes that follow no read stay as they were + raftStore.put(new byte[]{1}, new byte[]{1}); + raftStore.remove(new byte[]{1}); + + Assert.assertEquals(List.of(7L, 7L, -1L, -1L), expectedTerms); + } + + @Test + public void testCounterDoesNotRollBackAcrossALeaderChangeAndBack() throws PDException { + FencedNode nodeA = new FencedNode(); + IdMetaStore allocator = nodeA.allocator(); + // The request on A reads counter 0 in term 1 and pauses before its put. B leads + // term 2 and allocates [0,10) and [10,20). A applies both and leads again in term 3. + nodeA.pauseBeforeFirstWrite(() -> { + nodeA.term.set(2L); + Assert.assertEquals(0L, allocator.getId("aba", 10)); + Assert.assertEquals(10L, allocator.getId("aba", 10)); + nodeA.term.set(3L); + }); + + // Written in term 3 from the read of term 1, the put would set the counter back + // from 20 to 10 and return [0,10) a second time + Assert.assertEquals(20L, allocator.getId("aba", 10)); + + // The put of term 1 was rejected and the allocation read again in term 3 + Assert.assertEquals(List.of(1L), nodeA.rejectedTerms); + Assert.assertEquals(List.of(2L, 2L, 3L), nodeA.acceptedTerms); + // A new request does not get [10,20) or [20,30) a second time + Assert.assertEquals(30L, allocator.getId("aba", 10)); + } + + @Test + public void testCyclicIdIsNotHandedOutTwiceAcrossALeaderChangeAndBack() + throws PDException { + FencedNode nodeA = new FencedNode(); + IdMetaStore allocator = nodeA.allocator(); + // A reads counter 0 and no slot in term 1 and pauses. B leads term 2 and hands out + // the cids 0 and 1. A applies that and leads again in term 3. + nodeA.pauseBeforeFirstWrite(() -> { + nodeA.term.set(2L); + Assert.assertEquals(0L, allocator.getCId("aba", 16)); + Assert.assertEquals(1L, allocator.getCId("aba", 16)); + nodeA.term.set(3L); + }); + + // Not 0, which B handed out in term 2 + Assert.assertEquals(2L, allocator.getCId("aba", 16)); + + Assert.assertEquals(List.of(1L), nodeA.rejectedTerms); + Assert.assertEquals(3L, allocator.getCId("aba", 16)); + } + + @Test + public void testAllocationGivesUpWhenItsWritesKeepBeingRejected() throws PDException { + when(this.store.waitReadIndexInTerm()).thenReturn(7L); + doThrow(new PDException(Pdpb.ErrorType.NOT_LEADER_VALUE, "another term")) + .when(this.store).put(any(), any(), anyLong()); + + PDException e = Assert.assertThrows(PDException.class, () -> { + this.idMetaStore.getId("read-index", 10); + }); + + Assert.assertEquals(Pdpb.ErrorType.NOT_LEADER_VALUE, e.getErrorCode()); + verify(this.store, times(3)).waitReadIndexInTerm(); + verify(this.store, times(3)).put(any(), any(), eq(7L)); + } + + @Test + public void testAllocationDoesNotRepeatAWriteThatFailedForAnotherReason() + throws PDException { + doThrow(new PDException(Pdpb.ErrorType.UNKNOWN_VALUE, "apply failed")) + .when(this.store).put(any(), any(), anyLong()); + + PDException e = Assert.assertThrows(PDException.class, () -> { + this.idMetaStore.getId("read-index", 10); + }); + + Assert.assertEquals(Pdpb.ErrorType.UNKNOWN_VALUE, e.getErrorCode()); + verify(this.store, times(1)).put(any(), any(), anyLong()); + } + + /** + * PD node A as the allocator sees it: the production {@link IdMetaStore} and + * {@link RaftKVStore} over a raft engine that accepts a task the way jraft's + * {@code NodeImpl.executeApplyingTasks} does, only when the task's expected term is -1 + * or the current term, and applies an accepted task to the local state at once. + */ + private static final class FencedNode { + + private final TreeMap applied = new TreeMap<>(Arrays::compareUnsigned); + private final AtomicLong term = new AtomicLong(1L); + private final List acceptedTerms = new ArrayList<>(); + private final List rejectedTerms = new ArrayList<>(); + private final HgKVStore local = mock(HgKVStore.class); + private final RaftEngine engine = mock(RaftEngine.class); + private final RaftKVStore raftStore = new RaftKVStore(this.engine, this.local); + private Pause beforeFirstWrite; + + private FencedNode() throws PDException { + when(this.local.get(any())).thenAnswer(inv -> { + return this.applied.get(inv.getArgument(0)); + }); + when(this.local.scanRange(any(), any())).thenAnswer(inv -> { + List kvs = new ArrayList<>(); + this.applied.subMap(inv.getArgument(0), inv.getArgument(1)) + .forEach((key, value) -> kvs.add(new KV(key, value))); + return kvs; + }); + doAnswer(inv -> this.applied.put(inv.getArgument(0), inv.getArgument(1))) + .when(this.local).put(any(), any()); + when(this.engine.waitReadIndexInTerm()).thenAnswer(inv -> this.term.get()); + doAnswer(inv -> { + Pause pause = this.beforeFirstWrite; + this.beforeFirstWrite = null; + if (pause != null) { + pause.run(); + } + Task task = inv.getArgument(0); + long expected = task.getExpectedTerm(); + if (expected != -1L && expected != this.term.get()) { + this.rejectedTerms.add(expected); + task.getDone().run(new Status(RaftError.EPERM, + "expected_term=%d doesn't match " + + "current_term=%d", expected, + this.term.get())); + return null; + } + this.acceptedTerms.add(expected); + this.raftStore.invoke(KVOperation.fromByteArray(task.getData().array()), + null); + task.getDone().run(Status.OK()); + return null; + }).when(this.engine).addTask(any()); + } + + private IdMetaStore allocator() { + Whitebox.setInternalState(MetadataFactory.class, "store", this.raftStore); + return new IdMetaStore(new PDConfig()); + } + + /** + * Runs {@code then} where the first request has read and is about to write. The + * lock of an id key is reentrant, so {@code then} can allocate as another leader. + */ + private void pauseBeforeFirstWrite(Pause then) { + this.beforeFirstWrite = then; + } + } + + private interface Pause { + + void run() throws PDException; + } +} diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/store/HgKVStoreImplTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/store/HgKVStoreImplTest.java index 5e77b6a829..36ef499038 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/store/HgKVStoreImplTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/store/HgKVStoreImplTest.java @@ -24,6 +24,8 @@ import org.apache.commons.io.FileUtils; import org.apache.hugegraph.pd.common.PDException; import org.apache.hugegraph.pd.config.PDConfig; +import org.apache.hugegraph.pd.grpc.Metapb; +import org.apache.hugegraph.pd.meta.MetadataKeyHelper; import org.apache.hugegraph.pd.store.HgKVStore; import org.apache.hugegraph.pd.store.HgKVStoreImpl; import org.junit.Assert; @@ -71,6 +73,29 @@ public void Test() throws PDException { kvStore.close(); } + @Test + public void testScanLogRangeExcludesEnd() throws PDException { + long timestamp = 1700000000000L; + Metapb.LogRecord record = Metapb.LogRecord.newBuilder() + .setAction("range-boundary") + .setTimestamp(timestamp) + .build(); + byte[] key = MetadataKeyHelper.getLogKey(record); + byte[] start = MetadataKeyHelper.getLogKeyPrefix(record.getAction(), 0L); + byte[] end = MetadataKeyHelper.getLogKeyPrefix(record.getAction(), timestamp + 1L); + HgKVStore kvStore = new HgKVStoreImpl(); + kvStore.init(pdConfig); + try { + kvStore.put(key, record.toByteArray()); + + Assert.assertEquals(0, kvStore.scanRange(start, key).size()); + Assert.assertEquals(1, kvStore.scanRange(key, end).size()); + Assert.assertEquals(1, kvStore.scanRange(start, end).size()); + } finally { + kvStore.close(); + } + } + @Test public void TestSnapshot() throws PDException { HgKVStore kvStore = new HgKVStoreImpl(); diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/store/RaftKVStoreTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/store/RaftKVStoreTest.java new file mode 100644 index 0000000000..eb74e5c1f5 --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/store/RaftKVStoreTest.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.hugegraph.pd.core.store; + +import java.lang.reflect.Constructor; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.hugegraph.pd.common.PDException; +import org.apache.hugegraph.pd.config.PDConfig; +import org.apache.hugegraph.pd.grpc.Pdpb; +import org.apache.hugegraph.pd.raft.KVOperation; +import org.apache.hugegraph.pd.raft.RaftEngine; +import org.apache.hugegraph.pd.raft.RaftStateMachine; +import org.apache.hugegraph.pd.store.HgKVStore; +import org.apache.hugegraph.pd.store.KV; +import org.apache.hugegraph.pd.store.RaftKVStore; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.alipay.sofa.jraft.Iterator; +import com.alipay.sofa.jraft.Status; +import com.alipay.sofa.jraft.entity.Task; +import com.alipay.sofa.jraft.error.RaftError; + +/** + * Covers RaftKVStore as both sides of a raft round trip: writes become serialized raft tasks, + * and the decoded operations are dispatched back onto the local store by invoke(). + */ +public class RaftKVStoreTest { + + private static final byte[] KEY = "key".getBytes(); + private static final byte[] VALUE = "value".getBytes(); + + private HgKVStore store; + private RaftEngine engine; + private RaftKVStore raftStore; + + @Before + public void setUp() { + this.store = Mockito.mock(HgKVStore.class); + this.engine = Mockito.mock(RaftEngine.class); + this.raftStore = new RaftKVStore(this.engine, this.store); + } + + /** + * Makes the mocked engine behave like a single-node leader: the task payload is decoded + * exactly as a follower would decode the log entry, applied, then completed with status. + */ + private void applyTasksAs(Status status) { + Mockito.doAnswer(invocation -> { + Task task = invocation.getArgument(0); + if (status.isOk()) { + ByteBuffer data = task.getData(); + byte[] bytes = new byte[data.remaining()]; + data.get(bytes); + this.raftStore.invoke(KVOperation.fromByteArray(bytes), null); + } + task.getDone().run(status); + return null; + }).when(this.engine).addTask(Mockito.any(Task.class)); + } + + @Test + public void testInitRegistersAsRaftTaskHandler() { + PDConfig config = new PDConfig(); + + this.raftStore.init(config); + + Mockito.verify(this.store).init(config); + Mockito.verify(this.engine).addTaskHandler(this.raftStore); + } + + @Test + public void testReadsAreServedLocallyWithoutRaft() throws PDException { + List kvs = Collections.singletonList(new KV(KEY, VALUE)); + Mockito.when(this.store.get(KEY)).thenReturn(VALUE); + Mockito.when(this.store.getWithTTL(KEY)).thenReturn(VALUE); + Mockito.when(this.store.getListWithTTL(KEY)).thenReturn(Collections.singletonList(VALUE)); + Mockito.when(this.store.scanPrefix(KEY)).thenReturn(kvs); + Mockito.when(this.store.scanRange(KEY, VALUE)).thenReturn(kvs); + + Assert.assertArrayEquals(VALUE, this.raftStore.get(KEY)); + Assert.assertArrayEquals(VALUE, this.raftStore.getWithTTL(KEY)); + Assert.assertEquals(1, this.raftStore.getListWithTTL(KEY).size()); + Assert.assertSame(kvs, this.raftStore.scanPrefix(KEY)); + Assert.assertSame(kvs, this.raftStore.scanRange(KEY, VALUE)); + + Mockito.verify(this.engine, Mockito.never()).addTask(Mockito.any()); + } + + @Test + public void testWritesReachStoreOnlyThroughRaft() throws PDException { + applyTasksAs(Status.OK()); + + this.raftStore.put(KEY, VALUE); + this.raftStore.putWithTTL(KEY, VALUE, 60L); + this.raftStore.putWithTTL(KEY, VALUE, 5L, TimeUnit.MINUTES); + this.raftStore.remove(KEY); + this.raftStore.removeByPrefix(KEY); + this.raftStore.removeWithTTL(KEY); + this.raftStore.clear(); + + Mockito.verify(this.engine, Mockito.times(7)).addTask(Mockito.any(Task.class)); + Mockito.verify(this.store).put(KEY, VALUE); + Mockito.verify(this.store).putWithTTL(KEY, VALUE, 60L); + Mockito.verify(this.store).putWithTTL(KEY, VALUE, 5L, TimeUnit.MINUTES); + Mockito.verify(this.store).remove(KEY); + Mockito.verify(this.store).removeByPrefix(KEY); + Mockito.verify(this.store).removeWithTTL(KEY); + Mockito.verify(this.store).clear(); + Mockito.verifyNoMoreInteractions(this.store); + } + + @Test + public void testWriteOnNonLeaderIsRejectedAndNotApplied() throws Exception { + // A RaftEngine that has not joined a group is never leader + RaftKVStore notLeader = new RaftKVStore(newEngine(), this.store); + + PDException e = Assert.assertThrows(PDException.class, () -> notLeader.put(KEY, VALUE)); + + Assert.assertTrue(e.getMessage(), + e.getMessage().contains("Error code = " + + Pdpb.ErrorType.NOT_LEADER_VALUE)); + Mockito.verifyNoInteractions(this.store); + } + + @Test + public void testFailedRaftStatusFailsEveryWrite() { + applyTasksAs(new Status(RaftError.EINTERNAL, "injected raft failure")); + + assertWriteFails(() -> this.raftStore.put(KEY, VALUE)); + assertWriteFails(() -> this.raftStore.putWithTTL(KEY, VALUE, 1L)); + assertWriteFails(() -> this.raftStore.putWithTTL(KEY, VALUE, 1L, TimeUnit.SECONDS)); + assertWriteFails(() -> this.raftStore.remove(KEY)); + assertWriteFails(() -> this.raftStore.removeByPrefix(KEY)); + assertWriteFails(() -> this.raftStore.removeWithTTL(KEY)); + assertWriteFails(() -> this.raftStore.clear()); + Mockito.verifyNoInteractions(this.store); + } + + @Test + public void testEngineExceptionFailsWrite() { + Mockito.doThrow(new IllegalStateException("engine closed")) + .when(this.engine).addTask(Mockito.any(Task.class)); + + PDException e = Assert.assertThrows(PDException.class, + () -> this.raftStore.put(KEY, VALUE)); + Assert.assertTrue(e.getMessage(), e.getMessage().contains("engine closed")); + Mockito.verifyNoInteractions(this.store); + } + + @Test + public void testInvokeDispatchesSnapshotPathFromAttach() throws PDException { + this.raftStore.invoke(KVOperation.createSaveSnapshot("/snap/save"), null); + this.raftStore.invoke(KVOperation.createLoadSnapshot("/snap/load"), null); + + Mockito.verify(this.store).saveSnapshot("/snap/save"); + Mockito.verify(this.store).loadSnapshot("/snap/load"); + } + + @Test + public void testInvokeIgnoresGetAndUnknownOperations() throws PDException { + Assert.assertFalse(this.raftStore.invoke(KVOperation.createGet(KEY), null)); + Assert.assertFalse(this.raftStore.invoke(new KVOperation(KEY, VALUE, null, (byte) 0x7F), + null)); + + Mockito.verifyNoInteractions(this.store); + } + + @Test + public void testFollowerReplayAppliesTtlWithUnit() throws Exception { + RaftStateMachine stateMachine = new RaftStateMachine(); + stateMachine.addTaskHandler(this.raftStore); + byte[] entry = KVOperation.createPutWithTTL(KEY, VALUE, 3L, TimeUnit.HOURS) + .toByteArray(); + Iterator iter = Mockito.mock(Iterator.class); + Mockito.when(iter.hasNext()).thenReturn(true, false); + // A follower has no closure for the entry, so it must decode the log data + Mockito.when(iter.done()).thenReturn(null); + Mockito.when(iter.getData()).thenReturn(ByteBuffer.wrap(entry)); + + stateMachine.onApply(iter); + + Mockito.verify(this.store).putWithTTL(KEY, VALUE, 3L, TimeUnit.HOURS); + Mockito.verify(iter).next(); + Mockito.verify(iter, Mockito.never()).setErrorAndRollback(Mockito.anyLong(), + Mockito.any()); + } + + @Test + public void testFollowerReplayStopsOnStoreFailure() throws Exception { + RaftStateMachine stateMachine = new RaftStateMachine(); + stateMachine.addTaskHandler(this.raftStore); + Mockito.doThrow(new PDException(Pdpb.ErrorType.ROCKSDB_WRITE_ERROR_VALUE, "disk full")) + .when(this.store).put(KEY, VALUE); + Iterator iter = Mockito.mock(Iterator.class); + Mockito.when(iter.hasNext()).thenReturn(true, false); + Mockito.when(iter.getData()) + .thenReturn(ByteBuffer.wrap(KVOperation.createPut(KEY, VALUE).toByteArray())); + + stateMachine.onApply(iter); + + Mockito.verify(iter).setErrorAndRollback(Mockito.eq(1L), Mockito.argThat( + status -> status.getRaftError() == RaftError.ESTATEMACHINE && + "disk full".equals(status.getErrorMsg()))); + Mockito.verify(iter, Mockito.never()).next(); + } + + private interface Write { + + void run() throws PDException; + } + + private static void assertWriteFails(Write write) { + PDException e = Assert.assertThrows(PDException.class, write::run); + Assert.assertEquals(Pdpb.ErrorType.UNKNOWN_VALUE, e.getErrorCode()); + Assert.assertTrue(e.getMessage(), + e.getMessage().contains("Error code = " + Pdpb.ErrorType.UNKNOWN_VALUE)); + } + + private static RaftEngine newEngine() throws Exception { + Constructor constructor = RaftEngine.class.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } +} diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/KVOperationTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/KVOperationTest.java new file mode 100644 index 0000000000..8556a969b0 --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/KVOperationTest.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.hugegraph.pd.raft; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; +import org.junit.Test; + +import com.caucho.hessian.io.Hessian2Output; + +/** + * Pins the wire format that a follower replays from the raft log: the op byte followed by + * Hessian-encoded key, value and arg, decoded through the class whitelist. + */ +public class KVOperationTest { + + private static final byte[] KEY = {1, 2, 3}; + private static final byte[] VALUE = {4, 5, 6, 7}; + + private static KVOperation roundTrip(KVOperation op) throws IOException { + byte[] bytes = op.toByteArray(); + Assert.assertEquals(op.getOp(), bytes[0]); + return KVOperation.fromByteArray(bytes); + } + + @Test + public void testPutRoundTrip() throws IOException { + KVOperation decoded = roundTrip(KVOperation.createPut(KEY, VALUE)); + + Assert.assertEquals(KVOperation.PUT, decoded.getOp()); + Assert.assertArrayEquals(KEY, decoded.getKey()); + Assert.assertArrayEquals(VALUE, decoded.getValue()); + Assert.assertNull(decoded.getArg()); + } + + @Test + public void testGetRoundTripKeepsEmptyValue() throws IOException { + KVOperation decoded = roundTrip(KVOperation.createGet(KEY)); + + Assert.assertEquals(KVOperation.GET, decoded.getOp()); + Assert.assertArrayEquals(KEY, decoded.getKey()); + Assert.assertArrayEquals(new byte[0], decoded.getValue()); + } + + @Test + public void testRemoveOperationsCarryKeyAsValue() throws IOException { + KVOperation[] ops = { + KVOperation.createRemove(KEY), + KVOperation.createRemoveByPrefix(KEY), + KVOperation.createRemoveWithTTL(KEY) + }; + byte[] expectedOps = {KVOperation.REMOVE, KVOperation.REMOVE_BY_PREFIX, + KVOperation.REMOVE_WITH_TTL}; + for (int i = 0; i < ops.length; i++) { + KVOperation decoded = roundTrip(ops[i]); + Assert.assertEquals(expectedOps[i], decoded.getOp()); + Assert.assertArrayEquals(KEY, decoded.getKey()); + Assert.assertArrayEquals(KEY, decoded.getValue()); + } + } + + @Test + public void testPutWithTtlRoundTripKeepsLongArg() throws IOException { + long ttl = Integer.MAX_VALUE + 10L; + KVOperation decoded = roundTrip(KVOperation.createPutWithTTL(KEY, VALUE, ttl)); + + Assert.assertEquals(KVOperation.PUT_WITH_TTL, decoded.getOp()); + Assert.assertArrayEquals(VALUE, decoded.getValue()); + // RaftKVStore.invoke unboxes the arg with a (long) cast, so it must stay a Long + Assert.assertEquals(Long.valueOf(ttl), decoded.getArg()); + } + + @Test + public void testPutWithSmallTtlStillDecodesAsLong() throws IOException { + KVOperation decoded = roundTrip(KVOperation.createPutWithTTL(KEY, VALUE, 1L)); + + Assert.assertTrue(decoded.getArg() instanceof Long); + Assert.assertEquals(1L, (long) decoded.getArg()); + } + + @Test + public void testPutWithTtlUnitRoundTripKeepsTtlAndUnit() throws IOException { + KVOperation decoded = roundTrip(KVOperation.createPutWithTTL(KEY, VALUE, 30L, + TimeUnit.SECONDS)); + + Assert.assertEquals(KVOperation.PUT_WITH_TTL_UNIT, decoded.getOp()); + Object[] arg = (Object[]) decoded.getArg(); + Assert.assertEquals(2, arg.length); + Assert.assertEquals(30L, (long) arg[0]); + Assert.assertSame(TimeUnit.SECONDS, arg[1]); + } + + @Test + public void testClearRoundTripHasNullKeyAndValue() throws IOException { + KVOperation decoded = roundTrip(KVOperation.createClear()); + + Assert.assertEquals(KVOperation.CLEAR, decoded.getOp()); + Assert.assertNull(decoded.getKey()); + Assert.assertNull(decoded.getValue()); + } + + @Test + public void testSnapshotPathTravelsOnlyInAttach() throws IOException { + KVOperation save = KVOperation.createSaveSnapshot("/data/snapshot"); + Assert.assertEquals("/data/snapshot", save.getAttach()); + + // Snapshot operations are created locally by the state machine and never replicated: + // the path is held in the transient attach field and is not part of the encoding. + KVOperation decoded = roundTrip(save); + Assert.assertEquals(KVOperation.SAVE_SNAPSHOT, decoded.getOp()); + Assert.assertNull(decoded.getAttach()); + Assert.assertNull(decoded.getArg()); + + Assert.assertEquals(KVOperation.LOAD_SNAPSHOT, + KVOperation.createLoadSnapshot("/data/snapshot").getOp()); + } + + @Test + public void testFactoriesRejectNullArguments() { + assertNpe(() -> KVOperation.createPut(null, VALUE)); + assertNpe(() -> KVOperation.createPut(KEY, null)); + assertNpe(() -> KVOperation.createGet(null)); + assertNpe(() -> KVOperation.createPutWithTTL(null, VALUE, 1L)); + assertNpe(() -> KVOperation.createPutWithTTL(KEY, null, 1L, TimeUnit.SECONDS)); + assertNpe(() -> KVOperation.createRemove(null)); + assertNpe(() -> KVOperation.createRemoveByPrefix(null)); + assertNpe(() -> KVOperation.createRemoveWithTTL(null)); + assertNpe(() -> KVOperation.createSaveSnapshot(null)); + assertNpe(() -> KVOperation.createLoadSnapshot(null)); + } + + @Test + public void testTruncatedEntryFailsToDecode() throws IOException { + byte[] bytes = KVOperation.createPut(KEY, VALUE).toByteArray(); + byte[] truncated = Arrays.copyOf(bytes, bytes.length - 2); + + Assert.assertThrows(IOException.class, () -> KVOperation.fromByteArray(truncated)); + } + + @Test + public void testDecodeNeverInstantiatesClassOutsideWhitelist() throws IOException { + byte[] bytes = encode(KVOperation.PUT, KEY, VALUE, new NotWhitelisted("payload")); + + KVOperation decoded = KVOperation.fromByteArray(bytes); + + // The whitelist must keep the class from being instantiated + Assert.assertFalse(decoded.getArg() instanceof NotWhitelisted); + Assert.assertArrayEquals(KEY, decoded.getKey()); + } + + @Test + public void testDecodeAllowsWhitelistedCollections() throws IOException { + byte[] bytes = encode(KVOperation.PUT, KEY, VALUE, + new ArrayList<>(Arrays.asList("a", "b"))); + + KVOperation decoded = KVOperation.fromByteArray(bytes); + Assert.assertEquals(Arrays.asList("a", "b"), decoded.getArg()); + } + + private static byte[] encode(byte op, byte[] key, byte[] value, Object arg) + throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(op); + Hessian2Output output = new Hessian2Output(bos); + output.writeObject(key); + output.writeObject(value); + output.writeObject(arg); + output.flush(); + return bos.toByteArray(); + } + + private static void assertNpe(Runnable action) { + Assert.assertThrows(NullPointerException.class, action::run); + } + + public static class NotWhitelisted implements Serializable { + + private static final long serialVersionUID = 1L; + public String data; + + public NotWhitelisted(String data) { + this.data = data; + } + } +} diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/PeerUtilTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/PeerUtilTest.java new file mode 100644 index 0000000000..067f8d523e --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/PeerUtilTest.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.hugegraph.pd.raft; + +import java.util.List; + +import org.apache.hugegraph.pd.common.KVPair; +import org.junit.Assert; +import org.junit.Test; + +import com.alipay.sofa.jraft.entity.PeerId; + +public class PeerUtilTest { + + @Test + public void testIsPeerEqualsComparesOnlyIpAndPort() { + PeerId peer = new PeerId("10.0.0.1", 8610); + // Same endpoint with a different replica index and priority still names the same peer + PeerId sameEndpoint = new PeerId("10.0.0.1", 8610, 3, 100); + + Assert.assertTrue(PeerUtil.isPeerEquals(peer, sameEndpoint)); + Assert.assertFalse(PeerUtil.isPeerEquals(peer, new PeerId("10.0.0.1", 8611))); + Assert.assertFalse(PeerUtil.isPeerEquals(peer, new PeerId("10.0.0.2", 8610))); + } + + @Test + public void testIsPeerEqualsHandlesNull() { + PeerId peer = new PeerId("10.0.0.1", 8610); + + Assert.assertTrue(PeerUtil.isPeerEquals(null, null)); + Assert.assertFalse(PeerUtil.isPeerEquals(peer, null)); + Assert.assertFalse(PeerUtil.isPeerEquals(null, peer)); + } + + @Test + public void testParseConfigAssignsRolesFromSuffix() { + List> peers = PeerUtil.parseConfig( + "10.0.0.1:8610/leader,10.0.0.2:8610/learner,10.0.0.3:8610/follower," + + "10.0.0.4:8610"); + + Assert.assertEquals(4, peers.size()); + assertPeer(peers.get(0), "leader", "10.0.0.1", 8610); + assertPeer(peers.get(1), "learner", "10.0.0.2", 8610); + assertPeer(peers.get(2), "follower", "10.0.0.3", 8610); + // A peer without a role suffix defaults to follower + assertPeer(peers.get(3), "follower", "10.0.0.4", 8610); + } + + @Test + public void testParseConfigKeepsReplicaIndex() { + List> peers = PeerUtil.parseConfig("10.0.0.1:8610:2/learner"); + + Assert.assertEquals(1, peers.size()); + Assert.assertEquals("learner", peers.get(0).getKey()); + Assert.assertEquals(2, peers.get(0).getValue().getIdx()); + } + + @Test + public void testParseConfigOfEmptyInputIsEmpty() { + Assert.assertTrue(PeerUtil.parseConfig(null).isEmpty()); + Assert.assertTrue(PeerUtil.parseConfig("").isEmpty()); + } + + @Test + public void testParseConfigRejectsMalformedPeer() { + Assert.assertThrows(IllegalArgumentException.class, + () -> PeerUtil.parseConfig("10.0.0.1:8610,not-a-peer")); + Assert.assertThrows(IllegalArgumentException.class, + () -> PeerUtil.parseConfig("10.0.0.1:port/leader")); + // An unknown role is not stripped, so the peer string itself becomes invalid + Assert.assertThrows(IllegalArgumentException.class, + () -> PeerUtil.parseConfig("10.0.0.1:8610/witness")); + } + + private static void assertPeer(KVPair pair, String role, String ip, + int port) { + Assert.assertEquals(role, pair.getKey()); + Assert.assertEquals(ip, pair.getValue().getIp()); + Assert.assertEquals(port, pair.getValue().getPort()); + } +} diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadIndexTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadIndexTest.java new file mode 100644 index 0000000000..85d250bc45 --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadIndexTest.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.hugegraph.pd.raft; + +import java.util.Arrays; +import java.util.Deque; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.hugegraph.pd.common.PDException; +import org.apache.hugegraph.pd.config.PDConfig; +import org.apache.hugegraph.pd.grpc.Pdpb; +import org.apache.hugegraph.testutil.Whitebox; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.alipay.sofa.jraft.Node; +import com.alipay.sofa.jraft.Status; +import com.alipay.sofa.jraft.closure.ReadIndexClosure; +import com.alipay.sofa.jraft.error.RaftError; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Covers {@link RaftEngine#waitReadIndex()}, the barrier a leader runs before a local + * read-then-write such as the id counter. The raft node is a mock that answers each + * ReadIndex call the way jraft does: it sets the result and runs the closure. + */ +public class RaftEngineReadIndexTest { + + // Queued in place of a status: the closure is never run, as when no quorum answers + private static final Status NO_ANSWER = new Status(RaftError.UNKNOWN, "no answer"); + + private Node originalRaftNode; + private PDConfig.Raft originalConfig; + private long originalLeaderTerm; + // The term jraft announced this node as the leader of, -1 while it is not the leader + private AtomicLong leaderTerm; + private Node mockNode; + // One status per readIndex call, OK once the queue is empty + private final Deque answers = new ConcurrentLinkedDeque<>(); + + @Before + public void setUp() { + RaftEngine engine = RaftEngine.getInstance(); + this.originalRaftNode = engine.getRaftNode(); + this.mockNode = mock(Node.class); + doAnswer(invocation -> { + ReadIndexClosure closure = invocation.getArgument(1); + Status status = this.answers.isEmpty() ? Status.OK() : this.answers.poll(); + if (status != NO_ANSWER) { + closure.setResult(status.isOk() ? 42L : ReadIndexClosure.INVALID_LOG_INDEX, + invocation.getArgument(0)); + closure.run(status); + } + return null; + }).when(this.mockNode).readIndex(any(byte[].class), any(ReadIndexClosure.class)); + Whitebox.setInternalState(engine, "raftNode", this.mockNode); + + this.originalConfig = engine.getConfig(); + PDConfig.Raft config = new PDConfig().new Raft(); + config.setRpcTimeout(1000); + Whitebox.setInternalState(engine, "config", config); + RaftStateMachine machine = Whitebox.getInternalState(engine, "stateMachine"); + this.leaderTerm = Whitebox.getInternalState(machine, "leaderTerm"); + this.originalLeaderTerm = this.leaderTerm.get(); + } + + @After + public void tearDown() { + RaftEngine engine = RaftEngine.getInstance(); + Whitebox.setInternalState(engine, "raftNode", this.originalRaftNode); + Whitebox.setInternalState(engine, "config", this.originalConfig); + this.leaderTerm.set(this.originalLeaderTerm); + } + + @Test + public void testReturnsOnceTheReadIndexIsApplied() throws PDException { + RaftEngine.getInstance().waitReadIndex(1000L); + + verify(this.mockNode, times(1)).readIndex(any(byte[].class), + any(ReadIndexClosure.class)); + } + + @Test + public void testRetriesWhileTheNewLeaderHasNoCommitInItsTerm() throws PDException { + this.answers.addAll(Arrays.asList(new Status(RaftError.EAGAIN, "no commit yet"), + new Status(RaftError.EBUSY, "transferring"), + Status.OK())); + + RaftEngine.getInstance().waitReadIndex(1000L); + + verify(this.mockNode, times(3)).readIndex(any(byte[].class), + any(ReadIndexClosure.class)); + } + + @Test + public void testRetriesPastTheFirstHundredMillisecondsUntilTheLeaderCommits() + throws PDException { + // 15 answers 20 ms apart: the leader needs about 300 ms to commit in its term + for (int i = 0; i < 15; i++) { + this.answers.add(new Status(RaftError.EAGAIN, "no commit yet")); + } + + RaftEngine.getInstance().waitReadIndex(5000L); + + verify(this.mockNode, times(16)).readIndex(any(byte[].class), + any(ReadIndexClosure.class)); + } + + @Test + public void testRetriesStopAtTheDeadline() throws InterruptedException { + for (int i = 0; i < 1000; i++) { + this.answers.add(new Status(RaftError.EAGAIN, "no commit yet")); + } + + long start = System.nanoTime(); + PDException e = Assert.assertThrows(PDException.class, () -> { + RaftEngine.getInstance().waitReadIndex(200L); + }); + Assert.assertTrue(System.nanoTime() - start < 1_000_000_000L); + Assert.assertEquals(Pdpb.ErrorType.UNKNOWN_VALUE, e.getErrorCode()); + + // No retry is left running once the wait has given up + int calls = 1000 - this.answers.size(); + Thread.sleep(100L); + Assert.assertEquals(calls, 1000 - this.answers.size()); + } + + @Test + public void testBusyNodeIsRetriedLessAndLessOften() throws InterruptedException { + // EBUSY is also jraft's answer while its read index queue is full. A retry every + // 20 ms would be about 25 calls in 500 ms; doubled delays give calls at 0, 20, 60, + // 140 and 300 ms. + for (int i = 0; i < 1000; i++) { + this.answers.add(new Status(RaftError.EBUSY, "read index queue is full")); + } + + PDException e = Assert.assertThrows(PDException.class, () -> { + RaftEngine.getInstance().waitReadIndex(500L); + }); + + Assert.assertEquals(Pdpb.ErrorType.UNKNOWN_VALUE, e.getErrorCode()); + int calls = 1000 - this.answers.size(); + Assert.assertTrue("ReadIndex calls: " + calls, calls >= 3 && calls <= 6); + Thread.sleep(100L); + Assert.assertEquals(calls, 1000 - this.answers.size()); + } + + @Test + public void testBackoffStartsOverAfterAnAnswerThatIsNotBusy() throws PDException { + // 20 + 40 + 80 + 160 ms for four EBUSY answers, 20 ms for EAGAIN, then 20 ms again + // for the next EBUSY: 340 ms. It would wait 320 ms, 640 ms in all, had it kept + // the count + for (int i = 0; i < 4; i++) { + this.answers.add(new Status(RaftError.EBUSY, "transferring")); + } + this.answers.addAll(Arrays.asList(new Status(RaftError.EAGAIN, "no commit yet"), + new Status(RaftError.EBUSY, "transferring"))); + + long start = System.nanoTime(); + RaftEngine.getInstance().waitReadIndex(5000L); + long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + Assert.assertTrue("Returned after " + elapsedMs + " ms", elapsedMs < 500L); + verify(this.mockNode, times(7)).readIndex(any(byte[].class), + any(ReadIndexClosure.class)); + } + + @Test + public void testReadIndexInTermReturnsTheLeaderTerm() throws PDException { + this.leaderTerm.set(5L); + + Assert.assertEquals(5L, RaftEngine.getInstance().waitReadIndexInTerm()); + + verify(this.mockNode, times(1)).readIndex(any(byte[].class), + any(ReadIndexClosure.class)); + } + + @Test + public void testReadIndexInTermKeepsTheTermTakenBeforeTheWait() throws PDException { + // The node loses the leadership of term 5 and wins term 7 during the wait. The + // caller must get 5: its write is then rejected, as the wait began in another term + this.leaderTerm.set(5L); + doAnswer(invocation -> { + this.leaderTerm.set(7L); + ReadIndexClosure closure = invocation.getArgument(1); + closure.setResult(42L, invocation.getArgument(0)); + closure.run(Status.OK()); + return null; + }).when(this.mockNode).readIndex(any(byte[].class), any(ReadIndexClosure.class)); + + Assert.assertEquals(5L, RaftEngine.getInstance().waitReadIndexInTerm()); + } + + @Test + public void testReadIndexInTermWaitsAgainOnceANewLeaderIsAnnounced() throws PDException { + // Elected, but not announced before the first entry of the term is applied: the + // first wait ends after that, the second one runs inside the announced term + this.leaderTerm.set(-1L); + doAnswer(invocation -> { + this.leaderTerm.set(4L); + ReadIndexClosure closure = invocation.getArgument(1); + closure.setResult(42L, invocation.getArgument(0)); + closure.run(Status.OK()); + return null; + }).when(this.mockNode).readIndex(any(byte[].class), any(ReadIndexClosure.class)); + + Assert.assertEquals(4L, RaftEngine.getInstance().waitReadIndexInTerm()); + + verify(this.mockNode, times(2)).readIndex(any(byte[].class), + any(ReadIndexClosure.class)); + } + + @Test + public void testReadIndexInTermFailsAsNotLeaderOnAFollower() { + // A follower's ReadIndex succeeds through the leader, but it has no term to write in + this.leaderTerm.set(-1L); + + PDException e = Assert.assertThrows(PDException.class, () -> { + RaftEngine.getInstance().waitReadIndexInTerm(); + }); + + Assert.assertEquals(Pdpb.ErrorType.NOT_LEADER_VALUE, e.getErrorCode()); + verify(this.mockNode, times(1)).readIndex(any(byte[].class), + any(ReadIndexClosure.class)); + } + + @Test + public void testNodeThatLostLeadershipFailsAsNotLeader() { + this.answers.add(new Status(RaftError.EPERM, "not leader")); + + PDException e = Assert.assertThrows(PDException.class, () -> { + RaftEngine.getInstance().waitReadIndex(1000L); + }); + + Assert.assertEquals(Pdpb.ErrorType.NOT_LEADER_VALUE, e.getErrorCode()); + verify(this.mockNode, times(1)).readIndex(any(byte[].class), + any(ReadIndexClosure.class)); + } + + @Test + public void testWaitIsBoundedWhenNoQuorumAnswers() { + this.answers.add(NO_ANSWER); + + long start = System.nanoTime(); + Assert.assertThrows(PDException.class, () -> { + RaftEngine.getInstance().waitReadIndex(100L); + }); + Assert.assertTrue(System.nanoTime() - start < 1_000_000_000L); + } + + @Test + public void testEveryTimeoutReportsHowLongTheWaitWas() { + // No time left when the ReadIndex would be sent + PDException e = Assert.assertThrows(PDException.class, () -> { + RaftEngine.getInstance().waitReadIndex(0L); + }); + Assert.assertEquals("Raft read index timed out after 0 ms", e.getMessage()); + verify(this.mockNode, never()).readIndex(any(byte[].class), + any(ReadIndexClosure.class)); + + // No answer: the closure's own timer runs it with ETIMEDOUT about when the wait ends + this.answers.add(NO_ANSWER); + e = Assert.assertThrows(PDException.class, () -> { + RaftEngine.getInstance().waitReadIndex(100L); + }); + Assert.assertEquals("Raft read index timed out after 100 ms", e.getMessage()); + } + + @Test + public void testOneReadIndexMayWaitPastTheJraftDefaultTimeout() { + // No answer at all: the wait lasts until its own deadline. A closure created without + // a timeout passes this too, since its ETIMEDOUT at 2 s is left to the waiter; the + // next test tells the two apart + this.answers.add(NO_ANSWER); + + long start = System.nanoTime(); + Assert.assertThrows(PDException.class, () -> { + RaftEngine.getInstance().waitReadIndex(2600L); + }); + long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + Assert.assertTrue("Failed after " + elapsedMs + " ms", elapsedMs >= 2400L); + } + + @Test + public void testAnswerAfterTheJraftDefaultTimeoutStillCompletesTheWait() + throws PDException { + // The leader answers at 2.5 s, after the 2 s default timer of a ReadIndexClosure + // created without a timeout and well before the 5 s deadline of this wait. A closure + // that timed out drops the answer, and the wait would fail at its deadline. + ScheduledExecutorService leader = Executors.newSingleThreadScheduledExecutor(); + try { + doAnswer(invocation -> { + ReadIndexClosure closure = invocation.getArgument(1); + byte[] context = invocation.getArgument(0); + leader.schedule(() -> { + closure.setResult(42L, context); + closure.run(Status.OK()); + }, 2500L, TimeUnit.MILLISECONDS); + return null; + }).when(this.mockNode).readIndex(any(byte[].class), any(ReadIndexClosure.class)); + + long start = System.nanoTime(); + RaftEngine.getInstance().waitReadIndex(5000L); + long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + Assert.assertTrue("Returned after " + elapsedMs + " ms", elapsedMs >= 2400L); + } finally { + leader.shutdownNow(); + } + } + + @Test + public void testNodeShuttingDownFailsAsNotLeader() { + doThrow(new IllegalStateException("Node is shutting down")) + .when(this.mockNode).readIndex(any(byte[].class), any(ReadIndexClosure.class)); + + PDException e = Assert.assertThrows(PDException.class, () -> { + RaftEngine.getInstance().waitReadIndex(1000L); + }); + + Assert.assertEquals(Pdpb.ErrorType.NOT_LEADER_VALUE, e.getErrorCode()); + } + + @Test + public void testShutdownClosureThatRunsFirstFailsAsNotLeader() { + // jraft runs the closure with ENODESHUTDOWN on another thread and then throws; + // here the closure always completes the wait before the exception is caught + this.answerShutdown(RaftError.ENODESHUTDOWN, "Node is shutting down"); + + PDException e = Assert.assertThrows(PDException.class, () -> { + RaftEngine.getInstance().waitReadIndex(1000L); + }); + + Assert.assertEquals(Pdpb.ErrorType.NOT_LEADER_VALUE, e.getErrorCode()); + } + + @Test + public void testStoppedReadServiceClosureThatRunsFirstFailsAsNotLeader() { + // The read service fails the closure with EHOSTDOWN before it throws + this.answerShutdown(RaftError.EHOSTDOWN, "Service already shutdown."); + + PDException e = Assert.assertThrows(PDException.class, () -> { + RaftEngine.getInstance().waitReadIndex(1000L); + }); + + Assert.assertEquals(Pdpb.ErrorType.NOT_LEADER_VALUE, e.getErrorCode()); + } + + private void answerShutdown(RaftError error, String message) { + doAnswer(invocation -> { + ReadIndexClosure closure = invocation.getArgument(1); + closure.setResult(ReadIndexClosure.INVALID_LOG_INDEX, invocation.getArgument(0)); + closure.run(new Status(error, message)); + throw new IllegalStateException(message); + }).when(this.mockNode).readIndex(any(byte[].class), any(ReadIndexClosure.class)); + } + + @Test + public void testMissingRaftNodeFailsAsNotLeader() { + Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", null); + + PDException e = Assert.assertThrows(PDException.class, () -> { + RaftEngine.getInstance().waitReadIndex(1000L); + }); + + Assert.assertEquals(Pdpb.ErrorType.NOT_LEADER_VALUE, e.getErrorCode()); + } +} diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadinessTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadinessTest.java index 08c35287d8..0f73ca894e 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadinessTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadinessTest.java @@ -17,10 +17,12 @@ package org.apache.hugegraph.pd.raft; +import java.lang.reflect.Constructor; import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.apache.hugegraph.testutil.Whitebox; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -50,32 +52,24 @@ public class RaftEngineReadinessTest { private static final PeerId LEADER = new PeerId("10.0.0.1", 8610); - private Node originalRaftNode; - private RaftStateMachine originalStateMachine; - + private RaftEngine engine; private Node mockNode; private RaftStateMachine stateMachine; @Before - public void setUp() { - RaftEngine engine = RaftEngine.getInstance(); - originalRaftNode = engine.getRaftNode(); - originalStateMachine = Whitebox.getInternalState(engine, "stateMachine"); - - // A fresh machine so callbacks fired here reach no listeners other tests registered + public void setUp() throws Exception { + // Other suites leave scheduled tasks reading the shared engine. Keep this mock + // private to the test so their isLeader() calls cannot affect interaction checks. + engine = newEngine(); mockNode = mock(Node.class); - stateMachine = new RaftStateMachine(); + stateMachine = Whitebox.getInternalState(engine, "stateMachine"); Whitebox.setInternalState(engine, "raftNode", mockNode); - Whitebox.setInternalState(engine, "stateMachine", stateMachine); - // The count is cached on the singleton, so clear what an earlier test published - engine.refreshAlivePeerCount(); } - @After - public void tearDown() { - RaftEngine engine = RaftEngine.getInstance(); - Whitebox.setInternalState(engine, "raftNode", originalRaftNode); - Whitebox.setInternalState(engine, "stateMachine", originalStateMachine); + private static RaftEngine newEngine() throws Exception { + Constructor constructor = RaftEngine.class.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); } private static LeaderChangeContext ctx() { @@ -84,8 +78,8 @@ private static LeaderChangeContext ctx() { @Test public void testNotReadyBeforeRaftNodeStarts() { - Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", null); - RaftEngine engine = RaftEngine.getInstance(); + Whitebox.setInternalState(this.engine, "raftNode", null); + RaftEngine engine = this.engine; engine.refreshAlivePeerCount(); RaftEngine.RaftStatus status = engine.getRaftStatus(); @@ -98,7 +92,7 @@ public void testNotReadyBeforeRaftNodeStarts() { @Test public void testStartedNodeWithoutAnyCallbackIsNotReady() { - RaftEngine.RaftStatus status = RaftEngine.getInstance().getRaftStatus(); + RaftEngine.RaftStatus status = this.engine.getRaftStatus(); Assert.assertFalse(status.isReady()); Assert.assertEquals(State.STATE_UNINITIALIZED.name(), status.getState()); } @@ -108,7 +102,7 @@ public void testLeaderIsReady() { stateMachine.onLeaderStart(5); when(mockNode.listAlivePeers()).thenReturn(Arrays.asList(LEADER, new PeerId("b", 1), new PeerId("c", 1))); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; engine.refreshAlivePeerCount(); RaftEngine.RaftStatus status = engine.getRaftStatus(); @@ -122,7 +116,7 @@ public void testLeaderIsReady() { @Test public void testFollowerWithLeaderIsReady() { stateMachine.onStartFollowing(ctx()); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; engine.refreshAlivePeerCount(); RaftEngine.RaftStatus status = engine.getRaftStatus(); @@ -139,7 +133,7 @@ public void testFollowerLosingItsLeaderTurnsNotReady() { // The quorum-loss shape from the issue: heartbeats stop, jraft announces the loss stateMachine.onStartFollowing(ctx()); stateMachine.onStopFollowing(ctx()); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; RaftEngine.RaftStatus status = engine.getRaftStatus(); Assert.assertFalse(status.isReady()); @@ -151,7 +145,7 @@ public void testFollowerLosingItsLeaderTurnsNotReady() { public void testLeaderSteppingDownTurnsNotReady() { stateMachine.onLeaderStart(5); stateMachine.onLeaderStop(Status.OK()); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; engine.refreshAlivePeerCount(); RaftEngine.RaftStatus status = engine.getRaftStatus(); @@ -165,14 +159,14 @@ public void testLeaderSteppingDownTurnsNotReady() { public void testErrorAndShutdownAreNotReadyEvenAfterLeadership() { stateMachine.onLeaderStart(5); stateMachine.onError(mock(RaftException.class)); - Assert.assertFalse(RaftEngine.getInstance().getRaftStatus().isReady()); + Assert.assertFalse(this.engine.getRaftStatus().isReady()); Assert.assertEquals(State.STATE_ERROR.name(), - RaftEngine.getInstance().getRaftStatus().getState()); + this.engine.getRaftStatus().getState()); stateMachine.onShutdown(); - Assert.assertFalse(RaftEngine.getInstance().getRaftStatus().isReady()); + Assert.assertFalse(this.engine.getRaftStatus().isReady()); Assert.assertEquals(State.STATE_SHUTDOWN.name(), - RaftEngine.getInstance().getRaftStatus().getState()); + this.engine.getRaftStatus().getState()); } @Test @@ -182,7 +176,7 @@ public void testProbeNeverTouchesTheRaftNode() { // getAlivePeerCount() included: it reads what the refresher thread published stateMachine.onStartFollowing(ctx()); stateMachine.onStopFollowing(ctx()); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; engine.getRaftStatus(); engine.hasLeader(); @@ -195,9 +189,9 @@ public void testProbeNeverTouchesTheRaftNode() { public void testAlivePeerCountSurvivesLeadershipLossRace() { stateMachine.onLeaderStart(5); when(mockNode.listAlivePeers()).thenThrow(new IllegalStateException("Not leader")); - RaftEngine.getInstance().refreshAlivePeerCount(); + this.engine.refreshAlivePeerCount(); - Assert.assertEquals(-1, RaftEngine.getInstance().getAlivePeerCount()); + Assert.assertEquals(-1, this.engine.getAlivePeerCount()); } @Test @@ -207,8 +201,8 @@ public void testShutdownStateSurvivesTheNodeBeingDropped() { // uninitialized, which would read as a PD that has not started yet stateMachine.onLeaderStart(5); stateMachine.onShutdown(); - Whitebox.setInternalState(RaftEngine.getInstance(), "raftNode", null); - RaftEngine engine = RaftEngine.getInstance(); + Whitebox.setInternalState(this.engine, "raftNode", null); + RaftEngine engine = this.engine; RaftEngine.RaftStatus status = engine.getRaftStatus(); Assert.assertFalse(status.isReady()); @@ -217,13 +211,29 @@ public void testShutdownStateSurvivesTheNodeBeingDropped() { Assert.assertFalse(engine.hasLeader()); } + @Test + public void testNewNodeResetAllowsLeadershipAfterShutdown() throws Exception { + stateMachine.onLeaderStart(5); + stateMachine.onShutdown(); + stateMachine.stopLeaderCallbacks(); + CountDownLatch notified = new CountDownLatch(1); + stateMachine.addStateListener(notified::countDown); + stateMachine.resetForNewNode(); + Assert.assertEquals(State.STATE_UNINITIALIZED.name(), engine.getRaftStatus().getState()); + Assert.assertFalse(stateMachine.isLeader()); + stateMachine.onLeaderStart(7); + Assert.assertEquals(State.STATE_LEADER.name(), engine.getRaftStatus().getState()); + Assert.assertTrue(stateMachine.isLeader()); + Assert.assertTrue(notified.await(5, TimeUnit.SECONDS)); + } + @Test public void testAlivePeerCountStopsAfterALeaderErrorsOrShutsDown() { // The terminal callbacks clear the leader term, so the refresher stops reading the // count off a node that errored or shut down and the gauge falls back to NaN stateMachine.onLeaderStart(5); when(mockNode.listAlivePeers()).thenReturn(Arrays.asList(LEADER, new PeerId("b", 1))); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; engine.refreshAlivePeerCount(); Assert.assertEquals(2, engine.getAlivePeerCount()); @@ -233,13 +243,13 @@ public void testAlivePeerCountStopsAfterALeaderErrorsOrShutsDown() { stateMachine.onLeaderStart(6); engine.refreshAlivePeerCount(); - Assert.assertEquals(2, engine.getAlivePeerCount()); + Assert.assertEquals(-1, engine.getAlivePeerCount()); stateMachine.onShutdown(); engine.refreshAlivePeerCount(); Assert.assertEquals(-1, engine.getAlivePeerCount()); - // Twice, once per leader phase: the terminal states never reached the node - verify(mockNode, times(2)).listAlivePeers(); + // Only the initial leader phase reads the node; queued starts cannot recover terminal state. + verify(mockNode, times(1)).listAlivePeers(); } @Test @@ -248,7 +258,7 @@ public void testAlivePeerCountOnALeaderAlsoSkipsTheNode() { // value: listAlivePeers takes the node read lock before it checks for leadership, so // a scrape that called it would wait out an election that holds the write lock stateMachine.onLeaderStart(5); - RaftEngine engine = RaftEngine.getInstance(); + RaftEngine engine = this.engine; engine.getAlivePeerCount(); diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineRpcTimeoutTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineRpcTimeoutTest.java new file mode 100644 index 0000000000..3c22da393b --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineRpcTimeoutTest.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.hugegraph.pd.raft; + +import org.apache.hugegraph.pd.config.PDConfig; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; + +import com.alipay.sofa.jraft.option.NodeOptions; + +/** + * A candidate opens a connection to every peer while it holds the raft node lock, so the + * connect timeout bounds how long one unanswering peer stalls an election. It used to be + * {@code raft.rpc-timeout} (10 s), shared with the install-snapshot timeout; each now has + * its own option, and the install-snapshot timeout still follows rpc-timeout unless set. + */ +public class RaftEngineRpcTimeoutTest { + + @Test + public void testEachRpcTimeoutComesFromItsOwnOption() { + PDConfig.Raft raft = new PDConfig().new Raft(); + raft.setRpcTimeout(7000); + raft.setRpcConnectTimeout(900); + raft.setRpcInstallSnapshotTimeout(120000); + NodeOptions options = new NodeOptions(); + + RaftEngine.setRpcTimeouts(options, raft); + + Assert.assertEquals(900, options.getRpcConnectTimeoutMs()); + Assert.assertEquals(7000, options.getRpcDefaultTimeout()); + Assert.assertEquals(120000, options.getRpcInstallSnapshotTimeout()); + } + + @Test + public void testDefaults() throws NoSuchFieldException { + // The connect timeout takes jraft's default of 1 s + Assert.assertEquals("${raft.rpc-connect-timeout:1000}", + valueOf("rpcConnectTimeout")); + Assert.assertEquals("${raft.rpc-install-snapshot-timeout:0}", + valueOf("rpcInstallSnapshotTimeout")); + Assert.assertEquals("${raft.rpc-timeout:10000}", valueOf("rpcTimeout")); + + PDConfig.Raft raft = new PDConfig().new Raft(); + Assert.assertEquals(1000, raft.getRpcConnectTimeout()); + Assert.assertEquals(new NodeOptions().getRpcConnectTimeoutMs(), + raft.getRpcConnectTimeout()); + } + + @Test + public void testInstallSnapshotTimeoutFollowsRpcTimeoutWhenUnset() { + PDConfig.Raft raft = new PDConfig().new Raft(); + raft.setRpcTimeout(10000); + NodeOptions options = new NodeOptions(); + + RaftEngine.setRpcTimeouts(options, raft); + + Assert.assertEquals(10000, options.getRpcInstallSnapshotTimeout()); + } + + private static String valueOf(String field) throws NoSuchFieldException { + return PDConfig.Raft.class.getDeclaredField(field).getAnnotation(Value.class).value(); + } +} diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftStateMachineSnapshotTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftStateMachineSnapshotTest.java new file mode 100644 index 0000000000..6f06de04e0 --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftStateMachineSnapshotTest.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.hugegraph.pd.raft; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +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; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.io.FileUtils; +import org.apache.hugegraph.pd.common.PDException; +import org.apache.hugegraph.pd.grpc.Pdpb; +import org.apache.hugegraph.pd.service.MetadataService; +import org.apache.hugegraph.testutil.Whitebox; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import com.alipay.sofa.jraft.Closure; +import com.alipay.sofa.jraft.Status; +import com.alipay.sofa.jraft.entity.LocalFileMetaOutter.LocalFileMeta; +import com.alipay.sofa.jraft.error.RaftError; +import com.alipay.sofa.jraft.storage.snapshot.SnapshotReader; +import com.alipay.sofa.jraft.storage.snapshot.SnapshotWriter; +import com.google.protobuf.Message; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * jraft calls {@code onSnapshotSave} on the state machine thread with the snapshot index + * set to the last applied index, and applies the next entry once it returns. The checkpoint + * must therefore be taken before the call returns: a checkpoint taken later on a job thread + * holds entries past the snapshot index, which a node installing it applies a second time. + *

+ * {@code onSnapshotLoad} must return false for an archive it did not install: jraft moves + * its applied index to the snapshot index when the load returns true. + */ +public class RaftStateMachineSnapshotTest { + + private ThreadPoolExecutor originalJobs; + private ThreadPoolExecutor jobs; + private File snapshotPath; + private SnapshotWriter writer; + + @Before + public void setUp() throws IOException { + this.originalJobs = MetadataService.getUninterruptibleJobs(); + this.jobs = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>()); + Whitebox.setInternalState(MetadataService.class, "uninterruptibleJobs", this.jobs); + + this.snapshotPath = Files.createTempDirectory("pd-snapshot-save").toFile(); + this.writer = mock(SnapshotWriter.class); + when(this.writer.getPath()).thenReturn(this.snapshotPath.getAbsolutePath()); + when(this.writer.addFile(anyString(), any(Message.class))).thenReturn(true); + } + + @After + public void tearDown() throws IOException { + Whitebox.setInternalState(MetadataService.class, "uninterruptibleJobs", + this.originalJobs); + this.jobs.shutdownNow(); + FileUtils.deleteDirectory(this.snapshotPath); + } + + @Test + public void testCheckpointIsTakenBeforeTheNextEntryIsApplied() throws Exception { + AtomicLong applied = new AtomicLong(10L); + AtomicReference checkpointThread = new AtomicReference<>(); + AtomicLong checkpointed = new AtomicLong(-1L); + RaftStateMachine machine = new RaftStateMachine(); + machine.addTaskHandler((op, response) -> { + if (op.getOp() == KVOperation.SAVE_SNAPSHOT) { + checkpointThread.set(Thread.currentThread()); + checkpointed.set(applied.get()); + writeCheckpoint((String) op.getAttach(), applied.get()); + } + return false; + }); + RecordingClosure done = new RecordingClosure(); + + machine.onSnapshotSave(this.writer, done); + // What the state machine thread does next: apply the entry after the snapshot + applied.incrementAndGet(); + + Assert.assertSame(Thread.currentThread(), checkpointThread.get()); + Assert.assertEquals(10L, checkpointed.get()); + Assert.assertTrue(done.await()); + Assert.assertEquals(1, done.statuses.size()); + Assert.assertTrue(done.statuses.get(0).isOk()); + Assert.assertTrue(new File(this.snapshotPath, "snapshot.zip").isFile()); + } + + @Test + public void testFailedCheckpointCompletesTheSnapshotOnceWithAnError() throws Exception { + RaftStateMachine machine = new RaftStateMachine(); + machine.addTaskHandler((op, response) -> { + if (op.getOp() == KVOperation.SAVE_SNAPSHOT) { + throw new PDException(Pdpb.ErrorType.ROCKSDB_SAVE_SNAPSHOT_ERROR_VALUE, + "checkpoint failed"); + } + return false; + }); + RecordingClosure done = new RecordingClosure(); + + machine.onSnapshotSave(this.writer, done); + + Assert.assertTrue(done.await()); + // Drain the job thread so a second completion, if any, has run + this.jobs.submit(() -> { }).get(10, TimeUnit.SECONDS); + Assert.assertEquals(1, done.statuses.size()); + Assert.assertEquals(RaftError.EIO, done.statuses.get(0).getRaftError()); + Assert.assertFalse(new File(this.snapshotPath, "snapshot.zip").exists()); + } + + @Test + public void testSnapshotWithAWrongChecksumIsNotReportedAsInstalled() throws Exception { + AtomicLong loads = new AtomicLong(); + RaftStateMachine machine = this.machineThatCountsLoads(loads); + LocalFileMeta meta = this.saveSnapshot(machine); + LocalFileMeta wrong = LocalFileMeta.newBuilder().setChecksum("0").build(); + Assert.assertNotEquals(wrong.getChecksum(), meta.getChecksum()); + + Assert.assertFalse(machine.onSnapshotLoad(this.readerWith(wrong))); + + Assert.assertEquals(0L, loads.get()); + } + + @Test + public void testSnapshotThatIsNotAnArchiveIsNotReportedAsInstalled() throws Exception { + AtomicLong loads = new AtomicLong(); + RaftStateMachine machine = this.machineThatCountsLoads(loads); + + // No snapshot.zip in the snapshot directory + Assert.assertFalse(machine.onSnapshotLoad( + this.readerWith(LocalFileMeta.newBuilder().build()))); + + Assert.assertEquals(0L, loads.get()); + } + + @Test + public void testSnapshotWithItsChecksumIsInstalled() throws Exception { + AtomicLong loads = new AtomicLong(); + RaftStateMachine machine = this.machineThatCountsLoads(loads); + LocalFileMeta meta = this.saveSnapshot(machine); + + Assert.assertTrue(machine.onSnapshotLoad(this.readerWith(meta))); + + Assert.assertEquals(1L, loads.get()); + } + + private RaftStateMachine machineThatCountsLoads(AtomicLong loads) { + RaftStateMachine machine = new RaftStateMachine(); + machine.addTaskHandler((op, response) -> { + if (op.getOp() == KVOperation.SAVE_SNAPSHOT) { + writeCheckpoint((String) op.getAttach(), 10L); + } else if (op.getOp() == KVOperation.LOAD_SNAPSHOT) { + loads.incrementAndGet(); + } + return false; + }); + return machine; + } + + /** + * Saves a snapshot into the snapshot directory and returns the file meta, with the + * checksum of the archive, that the state machine handed to jraft + */ + private LocalFileMeta saveSnapshot(RaftStateMachine machine) throws Exception { + RecordingClosure done = new RecordingClosure(); + machine.onSnapshotSave(this.writer, done); + Assert.assertTrue(done.await()); + Assert.assertTrue(done.statuses.get(0).isOk()); + ArgumentCaptor meta = ArgumentCaptor.forClass(Message.class); + verify(this.writer).addFile(eq("snapshot.zip"), meta.capture()); + return (LocalFileMeta) meta.getValue(); + } + + private SnapshotReader readerWith(LocalFileMeta meta) { + SnapshotReader reader = mock(SnapshotReader.class); + when(reader.getPath()).thenReturn(this.snapshotPath.getAbsolutePath()); + when(reader.getFileMeta("snapshot.zip")).thenReturn(meta); + return reader; + } + + private static void writeCheckpoint(String dir, long index) { + try { + FileUtils.forceMkdir(new File(dir)); + Files.write(new File(dir, "CURRENT").toPath(), + Long.toString(index).getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static final class RecordingClosure implements Closure { + + private final List statuses = new CopyOnWriteArrayList<>(); + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + public void run(Status status) { + this.statuses.add(status); + this.latch.countDown(); + } + + private boolean await() throws InterruptedException { + return this.latch.await(10, TimeUnit.SECONDS); + } + } +} diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/ZipUtilsTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/ZipUtilsTest.java new file mode 100644 index 0000000000..c6a5f22a1f --- /dev/null +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/ZipUtilsTest.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.hugegraph.pd.raft; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import com.alipay.sofa.jraft.util.CRC64; + +/** + * Covers the archive step of PD raft snapshots: RaftStateMachine compares the checksum + * computed while compressing with the one computed while decompressing. + */ +public class ZipUtilsTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Test + public void testCompressDecompressRoundTripWithNestedDirectories() throws IOException { + File root = tmp.newFolder("writer"); + File snapshot = new File(root, "snapshot"); + write(new File(snapshot, "CURRENT"), "MANIFEST-000001"); + write(new File(snapshot, "sst/000001.sst"), "data-1"); + write(new File(snapshot, "sst/deep/000002.sst"), "data-2"); + File archive = new File(root, "snapshot.zip"); + + CRC64 compressSum = new CRC64(); + ZipUtils.compress(root.getPath(), "snapshot", archive.getPath(), compressSum); + Assert.assertTrue(archive.length() > 0); + + File reader = tmp.newFolder("reader"); + CRC64 decompressSum = new CRC64(); + ZipUtils.decompress(archive.getPath(), reader.getPath(), decompressSum); + + Assert.assertEquals("MANIFEST-000001", read(new File(reader, "snapshot/CURRENT"))); + Assert.assertEquals("data-1", read(new File(reader, "snapshot/sst/000001.sst"))); + Assert.assertEquals("data-2", read(new File(reader, "snapshot/sst/deep/000002.sst"))); + // The follower verifies the archive with the checksum the leader recorded + Assert.assertEquals(compressSum.getValue(), decompressSum.getValue()); + } + + @Test + public void testChecksumDiffersForDifferentContent() throws IOException { + Assert.assertNotEquals(checksumOf("data-1"), checksumOf("data-2")); + } + + @Test + public void testCompressEmptyDirectoryProducesEmptyArchive() throws IOException { + File root = tmp.newFolder("empty-writer"); + Assert.assertTrue(new File(root, "snapshot").mkdir()); + File archive = new File(root, "snapshot.zip"); + + ZipUtils.compress(root.getPath(), "snapshot", archive.getPath(), new CRC64()); + + File reader = tmp.newFolder("empty-reader"); + ZipUtils.decompress(archive.getPath(), reader.getPath(), new CRC64()); + String[] extracted = reader.list(); + Assert.assertNotNull(extracted); + Assert.assertEquals(0, extracted.length); + } + + @Test + public void testDecompressSkipsEntriesEscapingOutputDir() throws IOException { + File base = tmp.newFolder("slip"); + File output = new File(base, "out"); + Assert.assertTrue(output.mkdir()); + File archive = new File(base, "evil.zip"); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(archive))) { + putEntry(zos, "../escaped.txt", "evil"); + // A sibling whose name shares the output dir prefix is outside it as well + putEntry(zos, "../out-sibling/escaped.txt", "evil"); + putEntry(zos, "snapshot/kept.txt", "good"); + } + + ZipUtils.decompress(archive.getPath(), output.getPath(), new CRC64()); + + Assert.assertFalse(new File(base, "escaped.txt").exists()); + Assert.assertFalse(new File(base, "out-sibling").exists()); + Assert.assertEquals("good", read(new File(output, "snapshot/kept.txt"))); + } + + @Test + public void testDecompressMissingArchiveFails() { + File missing = new File(tmp.getRoot(), "missing.zip"); + + Assert.assertThrows(IOException.class, + () -> ZipUtils.decompress(missing.getPath(), tmp.getRoot().getPath(), + new CRC64())); + } + + private long checksumOf(String content) throws IOException { + File root = tmp.newFolder(); + write(new File(root, "snapshot/file"), content); + CRC64 checksum = new CRC64(); + ZipUtils.compress(root.getPath(), "snapshot", new File(root, "a.zip").getPath(), + checksum); + return checksum.getValue(); + } + + private static void putEntry(ZipOutputStream zos, String name, String content) + throws IOException { + zos.putNextEntry(new ZipEntry(name)); + zos.write(content.getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + } + + private static void write(File file, String content) throws IOException { + Files.createDirectories(file.getParentFile().toPath()); + Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8)); + } + + private static String read(File file) throws IOException { + return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + } +} diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/service/LogServiceTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/service/LogServiceTest.java index ba99ae6728..cdad8a6f32 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/service/LogServiceTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/service/LogServiceTest.java @@ -44,11 +44,11 @@ public void testGetLog() throws Exception { logServiceUnderTest.insertLog("action", "message", Any.newBuilder().build()); - // Run the test + // The query end is exclusive, so include logs inserted in the current millisecond. final List result = logServiceUnderTest.getLog( - "action", 0L, System.currentTimeMillis()); + "action", 0L, System.currentTimeMillis() + 1L); // Verify the results - Assert.assertEquals(result.size(), 1); + Assert.assertEquals(1, result.size()); } } diff --git a/hugegraph-pd/pom.xml b/hugegraph-pd/pom.xml index 83b39c8377..12c9500e5c 100644 --- a/hugegraph-pd/pom.xml +++ b/hugegraph-pd/pom.xml @@ -74,7 +74,6 @@ org.jacoco jacoco-maven-plugin - 0.8.8 true @@ -151,7 +150,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 pd-common-test @@ -175,7 +173,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 pd-core-test @@ -199,7 +196,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 pd-client-test @@ -223,7 +219,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 pd-rest-test diff --git a/hugegraph-server/Dockerfile b/hugegraph-server/Dockerfile index 67866974cf..694acd6f21 100644 --- a/hugegraph-server/Dockerfile +++ b/hugegraph-server/Dockerfile @@ -18,7 +18,7 @@ # Dockerfile for HugeGraph Server # 1st stage: build source code -FROM --platform=$BUILDPLATFORM maven:3.9.0-eclipse-temurin-11 AS build +FROM --platform=$BUILDPLATFORM maven:3.9.16-eclipse-temurin-17 AS build WORKDIR /pkg @@ -35,13 +35,13 @@ RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sh # 2nd stage: runtime env # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13 -FROM eclipse-temurin:11-jre-jammy +FROM eclipse-temurin:17-jdk-jammy +# Local Arthas attach requires the JDK attach and compiler modules. LABEL maintainer="HugeGraph Docker Maintainers " # TODO: use g1gc or zgc as default -# Note: --add-exports is required for Java 11+ to access jdk.internal.reflect for auth proxy -ENV JAVA_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:MaxRAMPercentage=50 -XshowSettings:vm --add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED" \ +ENV JAVA_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:MaxRAMPercentage=50 -XshowSettings:vm" \ HUGEGRAPH_HOME="hugegraph-server" \ STDOUT_MODE="true" diff --git a/hugegraph-server/Dockerfile-hstore b/hugegraph-server/Dockerfile-hstore index e90979563b..351b116d6b 100644 --- a/hugegraph-server/Dockerfile-hstore +++ b/hugegraph-server/Dockerfile-hstore @@ -18,7 +18,7 @@ # Dockerfile for HugeGraph Server (hstore backend) # 1st stage: build source code -FROM --platform=$BUILDPLATFORM maven:3.9.0-eclipse-temurin-11 AS build +FROM --platform=$BUILDPLATFORM maven:3.9.16-eclipse-temurin-17 AS build WORKDIR /pkg @@ -35,13 +35,15 @@ RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sh # 2nd stage: runtime env # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13 -FROM eclipse-temurin:11-jre-jammy +FROM eclipse-temurin:17-jdk-jammy +# Local Arthas attach requires the JDK attach and compiler modules. LABEL maintainer="HugeGraph Docker Maintainers " # TODO: use g1gc or zgc as default ENV JAVA_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:MaxRAMPercentage=50 -XshowSettings:vm" \ - HUGEGRAPH_HOME="hugegraph-server" + HUGEGRAPH_HOME="hugegraph-server" \ + STDOUT_MODE="true" WORKDIR /hugegraph-server/ diff --git a/hugegraph-server/README.md b/hugegraph-server/README.md index 819bece457..f559004bb7 100644 --- a/hugegraph-server/README.md +++ b/hugegraph-server/README.md @@ -1,15 +1,27 @@ # HugeGraph Server + + HugeGraph Server consists of two layers of functionality: the graph engine layer, and the storage layer. - Graph Engine Layer: - REST Server: Provides a RESTful API for querying graph/schema information, supports the [Gremlin](https://tinkerpop.apache.org/gremlin.html) and [Cypher](https://en.wikipedia.org/wiki/Cypher) query languages, and offers APIs for service monitoring and operations. + - See [Cypher HTTP contracts](../docs/server-runtime.md#cypher-http-contracts) for request forms, verified behavior, and + unverified scope. - Graph Engine: Supports both OLTP and OLAP graph computation types, with OLTP implementing the [Apache TinkerPop3](https://tinkerpop.apache.org) framework. - Backend Interface: Implements the storage of graph data to the backend. - Storage Layer: - Storage Backend: Includes RocksDB (default, embedded), HStore (distributed), HBase (deprecated and planned for removal in 2.0), and the test-only Memory backend. Users can extend custom backends without modifying the existing source code. +See [launcher GC options](../docs/server-runtime.md#launcher-gc-options) for explicit collector selection +and caller JVM tuning. +See [cache RPC graph lifecycle](../docs/server-runtime.md#cache-rpc-graph-lifecycle) for dynamic +graph registration, resource cleanup and notification delivery limits. +See [local loading ownership](../docs/server-runtime.md#local-loading-ownership) for +concurrent import mode protection and its compatibility limits. +See [pairwise same neighbors](../docs/same-neighbors-batch.md) for the batch traverser request and response. + ## Backend Evolution and Compatibility The current mainline does not include implementations for the historical backends. The following timeline distinguishes current support from legacy compatibility guidance: diff --git a/hugegraph-server/hugegraph-api/pom.xml b/hugegraph-server/hugegraph-api/pom.xml index e5a81be208..c6aeffa306 100644 --- a/hugegraph-server/hugegraph-api/pom.xml +++ b/hugegraph-server/hugegraph-api/pom.xml @@ -44,9 +44,7 @@ io.grpc grpc-netty-shaded - - - 1.47.0 + ${grpc.version} diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/API.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/API.java index b50195b85d..b5653d5977 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/API.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/API.java @@ -24,7 +24,7 @@ import java.util.function.Consumer; import org.apache.commons.lang.mutable.MutableLong; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/arthas/ArthasAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/arthas/ArthasAPI.java index 7cd9a98f8a..f2dd9e9809 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/arthas/ArthasAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/arthas/ArthasAPI.java @@ -17,45 +17,109 @@ package org.apache.hugegraph.api.arthas; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.HashMap; import org.apache.hugegraph.api.API; +import org.apache.hugegraph.auth.HugeAuthenticator; +import org.apache.hugegraph.config.ConfigOption; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.config.ServerOptions; +import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.util.JsonUtil; +import org.apache.hugegraph.util.Log; +import org.slf4j.Logger; import com.codahale.metrics.annotation.Timed; import com.taobao.arthas.agent.attach.ArthasAgent; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Singleton; +import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; @Path("arthas") @Singleton @Tag(name = "ArthasAPI") public class ArthasAPI extends API { + private static final Logger LOG = Log.logger(ArthasAPI.class); + @Context private jakarta.inject.Provider configProvider; @PUT @Timed @Produces(APPLICATION_JSON_WITH_CHARSET) + @RolesAllowed("admin") @Operation(summary = "start arthas agent") - public Object startArthas() { + public Object startArthas(@Context GraphManager manager, + @Context SecurityContext context) { + checkAccess(manager, context); HugeConfig config = this.configProvider.get(); + HashMap configMap = arthasConfig(config); + String response = JsonUtil.toJson(configMap); + // Keep the optional MCP endpoint disabled without extending the activation response. + configMap.put("arthas.mcpEndpoint", ""); + // Do not let external Arthas properties override the configured listener. + configMap.put("arthas.config.overrideAll", "false"); + configMap.put("arthas.tunnelServer", ""); + ArthasAgent.attach(configMap); + return response; + } + + static void checkAccess(GraphManager manager, SecurityContext context) { + // No-auth requests receive an anonymous admin role, so a role check alone is insufficient. + if (!manager.requireAuthentication()) { + throw new ForbiddenException("HTTP Arthas activation requires authentication; " + + "use bin/arthas.sh on the server host instead"); + } + if (context == null || context.getUserPrincipal() == null || + HugeAuthenticator.USER_ANONY.equals(context.getUserPrincipal().getName()) || + !context.isUserInRole("admin")) { + throw new ForbiddenException("Arthas activation requires an authenticated administrator"); + } + } + + static HashMap arthasConfig(HugeConfig config) { HashMap configMap = new HashMap<>(4); - configMap.put("arthas.telnetPort", config.get(ServerOptions.ARTHAS_TELNET_PORT)); - configMap.put("arthas.httpPort", config.get(ServerOptions.ARTHAS_HTTP_PORT)); - configMap.put("arthas.ip", config.get(ServerOptions.ARTHAS_IP)); + configMap.put("arthas.telnetPort", + option(config, ServerOptions.ARTHAS_TELNET_PORT, + ServerOptions.ARTHAS_TELNET_PORT_LEGACY)); + configMap.put("arthas.httpPort", + option(config, ServerOptions.ARTHAS_HTTP_PORT, + ServerOptions.ARTHAS_HTTP_PORT_LEGACY)); + String ip = config.get(ServerOptions.ARTHAS_IP); + ip = "localhost".equalsIgnoreCase(ip) ? "127.0.0.1" : ip; + try { + InetAddress.getByName(ip); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("Invalid or unresolvable Arthas bind address '" + ip + "'", e); + } + configMap.put("arthas.ip", ip); configMap.put("arthas.disabledCommands", - config.get(ServerOptions.ARTHAS_DISABLED_COMMANDS)); - ArthasAgent.attach(configMap); - return JsonUtil.toJson(configMap); + option(config, ServerOptions.ARTHAS_DISABLED_COMMANDS, + ServerOptions.ARTHAS_DISABLED_COMMANDS_LEGACY)); + return configMap; + } + + private static String option(HugeConfig config, ConfigOption canonical, + ConfigOption legacy) { + if (!config.containsKey(canonical.name())) { + return config.get(legacy); + } + String value = config.get(canonical); + if (config.containsKey(legacy.name()) && !value.equals(config.get(legacy))) { + LOG.warn("Conflicting Arthas options '{}' and '{}': '{}' takes precedence", + canonical.name(), legacy.name(), canonical.name()); + } + return value; } } diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java index 117da8e9ae..578685feb1 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/AccessAPI.java @@ -26,7 +26,7 @@ import org.apache.hugegraph.auth.HugeAccess; import org.apache.hugegraph.auth.HugePermission; import org.apache.hugegraph.auth.HugeTarget; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; import org.apache.hugegraph.exception.NotFoundException; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/BelongAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/BelongAPI.java index d5e3f105bb..5f2d6d5663 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/BelongAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/BelongAPI.java @@ -24,7 +24,7 @@ import org.apache.hugegraph.api.filter.StatusFilter.Status; import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.HugeBelong; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; import org.apache.hugegraph.exception.NotFoundException; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java index d41de962e2..eb3b7c1ec2 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceGroupAPI.java @@ -29,7 +29,7 @@ import org.apache.hugegraph.auth.HugeGroup; import org.apache.hugegraph.auth.HugeUser; import org.apache.hugegraph.auth.StandardAuthManagerV2; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; import org.apache.hugegraph.exception.NotFoundException; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GroupAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GroupAPI.java index ae13beb4a6..f19200c65a 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GroupAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/GroupAPI.java @@ -22,7 +22,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter.Status; import org.apache.hugegraph.auth.HugeGroup; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; import org.apache.hugegraph.exception.NotFoundException; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/ProjectAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/ProjectAPI.java index 4380093ba0..322d434bff 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/ProjectAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/ProjectAPI.java @@ -27,7 +27,7 @@ import org.apache.hugegraph.api.filter.StatusFilter.Status; import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.HugeProject; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; import org.apache.hugegraph.exception.NotFoundException; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java index 7504ad7325..d5e5288f49 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/auth/UserAPI.java @@ -23,8 +23,8 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter.Status; import org.apache.hugegraph.auth.HugeUser; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; import org.apache.hugegraph.exception.NotFoundException; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java index a30bff73b8..7bac7e18c0 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java @@ -20,6 +20,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -27,13 +28,22 @@ import org.apache.commons.lang3.tuple.Pair; import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.CompressInterceptor; +import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; +import org.apache.tinkerpop.shaded.jackson.core.JsonProcessingException; +import org.apache.tinkerpop.shaded.jackson.databind.DeserializationFeature; +import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper; +import org.apache.tinkerpop.shaded.jackson.databind.ObjectReader; import org.slf4j.Logger; import com.codahale.metrics.annotation.Timed; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.inject.Singleton; import jakarta.ws.rs.Consumes; @@ -55,7 +65,10 @@ public class CypherAPI extends API { private static final Logger LOG = Log.logger(CypherAPI.class); private static final Charset UTF8 = StandardCharsets.UTF_8; private static final String CLIENT_CONF = "conf/remote-objects.yaml"; - private final Base64.Decoder decoder = Base64.getUrlDecoder(); + private static final ObjectReader REQUEST_READER = new ObjectMapper() + .readerFor(Object.class) + .with(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); + private final Base64.Decoder decoder = Base64.getDecoder(); private final String basic = "Basic "; private final String bearer = "Bearer "; @@ -72,7 +85,8 @@ private CypherManager cypherManager() { @Timed @CompressInterceptor.Compress(buffer = (1024 * 40)) @Produces(APPLICATION_JSON_WITH_CHARSET) - public CypherModel query(@Context HttpHeaders headers, + public CypherModel query(@Context GraphManager manager, + @Context HttpHeaders headers, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphspace, @Parameter(description = "The graph name") @@ -80,7 +94,8 @@ public CypherModel query(@Context HttpHeaders headers, @Parameter(description = "The cypher query string") @QueryParam("cypher") String cypher) { - return this.queryByCypher(headers, graphspace, graph, cypher); + return this.queryByCypher(manager, headers, graphspace, graph, cypher, + Collections.emptyMap()); } @@ -89,35 +104,89 @@ public CypherModel query(@Context HttpHeaders headers, @CompressInterceptor.Compress @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON_WITH_CHARSET) - public CypherModel post(@Context HttpHeaders headers, + @RequestBody(required = true, + description = "A nonblank raw Cypher query, or a JSON object with a nonblank " + + "string 'cypher' and optional object 'parameters'. Omitted " + + "parameters default to an empty object; null, arrays and " + + "scalars are rejected. The JSON request itself must be an object. " + + "Legacy raw Cypher sent as application/json " + + "is also accepted.", + content = { + @Content(mediaType = APPLICATION_JSON, + schema = @Schema(implementation = Map.class), + examples = @ExampleObject(name = "Parameterized query", + value = "{\"cypher\":\"MATCH (n:person) WHERE " + + "n.name = $name RETURN n.name\"," + + "\"parameters\":{\"name\":\"marko\"}}")) + }) + public CypherModel post(@Context GraphManager manager, + @Context HttpHeaders headers, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphspace, @Parameter(description = "The graph name") @PathParam("graph") String graph, - @Parameter(description = "The cypher query string") String cypher) { - return this.queryByCypher(headers, graphspace, graph, cypher); + Map parameters = Collections.emptyMap(); + if (looksLikeJson(cypher)) { + Object value; + try { + value = REQUEST_READER.readValue(cypher); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid Cypher request JSON", e); + } + E.checkArgument(value instanceof Map, "The Cypher request must be a JSON object"); + Map request = (Map) value; + Object query = request.get("cypher"); + E.checkArgument(query instanceof String, + "The cypher parameter must be a nonblank string"); + cypher = (String) query; + if (request.containsKey("parameters")) { + Object bindings = request.get("parameters"); + E.checkArgument(bindings instanceof Map, + "The parameters parameter must be an object"); + @SuppressWarnings("unchecked") + Map supplied = (Map) bindings; + parameters = supplied; + } + } + return this.queryByCypher(manager, headers, graphspace, graph, cypher, parameters); + } + + private static boolean looksLikeJson(String body) { + if (body == null || body.isBlank()) { + return false; + } + String value = body.strip(); + char first = value.charAt(0); + return first == '{' || first == '[' || first == '"' || first == '-' || + (first >= '0' && first <= '9') || value.equals("null") || + value.equals("true") || value.equals("false"); } - private CypherModel queryByCypher(HttpHeaders headers, String graphspace, - String graph, String cypher) { + private CypherModel queryByCypher(GraphManager manager, HttpHeaders headers, String graphspace, + String graph, String cypher, + Map parameters) { E.checkArgument(graphspace != null && !graphspace.isEmpty(), "The graphspace parameter can't be null or empty"); E.checkArgument(graph != null && !graph.isEmpty(), "The graph parameter can't be null or empty"); - E.checkArgument(cypher != null && !cypher.isEmpty(), - "The cypher parameter can't be null or empty"); + E.checkArgument(cypher != null && !cypher.isBlank(), + "The cypher parameter must be a nonblank string"); String graphInfo = graphspace + "-" + graph; Map aliases = new HashMap<>(2, 1); aliases.put("graph", graphInfo); aliases.put("g", "__g_" + graphInfo); - return this.client(headers).submitQuery(cypher, aliases); + return this.client(manager, headers).submitQuery(cypher, aliases, parameters); } - private CypherClient client(HttpHeaders headers) { + private CypherClient client(GraphManager manager, HttpHeaders headers) { + if (!manager.requireAuthentication()) { + return this.cypherManager().getAnonymousClient(); + } + String auth = headers.getHeaderString(HttpHeaders.AUTHORIZATION); if (auth != null && !auth.isEmpty()) { @@ -159,7 +228,7 @@ private Pair toUserPass(String auth) { String encoded = auth.substring(basic.length()); byte[] userPass = this.decoder.decode(encoded); String authorization = new String(userPass, UTF8); - split = authorization.split(":"); + split = authorization.split(":", 2); } catch (Exception e) { LOG.error("Failed convert auth to credential.", e); return null; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java index 92ae18c54d..c9b19c71ac 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java @@ -17,7 +17,11 @@ package org.apache.hugegraph.api.cypher; +import java.lang.reflect.Array; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -29,25 +33,32 @@ import javax.annotation.concurrent.ThreadSafe; import org.apache.commons.configuration2.Configuration; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; import org.apache.tinkerpop.gremlin.driver.Client; import org.apache.tinkerpop.gremlin.driver.Cluster; import org.apache.tinkerpop.gremlin.driver.Result; import org.apache.tinkerpop.gremlin.driver.ResultSet; -import org.apache.tinkerpop.gremlin.driver.Tokens; -import org.apache.tinkerpop.gremlin.driver.message.RequestMessage; +import org.apache.tinkerpop.gremlin.process.traversal.Path; +import org.apache.tinkerpop.gremlin.util.Tokens; +import org.apache.tinkerpop.gremlin.util.message.RequestMessage; import org.slf4j.Logger; @ThreadSafe public final class CypherClient { private static final Logger LOG = Log.logger(CypherClient.class); + private static final int NORMALIZE_MAX_DEPTH = 32; private final Supplier configurationSupplier; private String userName; private String password; private String token; + CypherClient(Supplier configurationSupplier) { + this.configurationSupplier = configurationSupplier; + } + CypherClient(String userName, String password, Supplier configurationSupplier) { this.userName = userName; @@ -61,38 +72,50 @@ public final class CypherClient { } public CypherModel submitQuery(String cypherQuery, @Nullable Map aliases) { - E.checkArgument(cypherQuery != null && !cypherQuery.isEmpty(), - "The cypher-query parameter can't be null or empty"); + return this.submitQuery(cypherQuery, aliases, Collections.emptyMap()); + } - Cluster cluster = Cluster.open(getConfig()); - Client client = cluster.connect(); + public CypherModel submitQuery(String cypherQuery, @Nullable Map aliases, + Map parameters) { + E.checkArgument(cypherQuery != null && !cypherQuery.isBlank(), + "The cypher-query parameter must be a nonblank string"); + E.checkArgument(parameters != null, "The parameters parameter must be an object"); - if (aliases != null && !aliases.isEmpty()) { - client = client.alias(aliases); - } - - RequestMessage request = createRequest(cypherQuery); + RequestMessage request = createRequest(cypherQuery, parameters); + Cluster cluster = Cluster.open(getConfig()); + Client client = null; CypherModel res; try { + client = cluster.connect(); + if (aliases != null && !aliases.isEmpty()) { + client = client.alias(aliases); + } List list = this.doQueryList(client, request); res = CypherModel.dataOf(request.getRequestId().toString(), list); } catch (Exception e) { LOG.error(String.format("Failed to submit cypher-query: [ %s ], caused by:", cypherQuery), e); - res = CypherModel.failOf(request.getRequestId().toString(), e.getMessage()); + res = CypherModel.failOf(request.getRequestId().toString(), + e.getMessage(), CypherErrorMapper.map(e)); } finally { - client.close(); - cluster.close(); + try { + if (client != null) { + client.close(); + } + } finally { + cluster.close(); + } } return res; } - private RequestMessage createRequest(String cypherQuery) { + static RequestMessage createRequest(String cypherQuery, Map parameters) { return RequestMessage.build(Tokens.OPS_EVAL) .processor("cypher") .add(Tokens.ARGS_GREMLIN, cypherQuery) + .add(Tokens.ARGS_BINDINGS, parameters) .create(); } @@ -105,12 +128,102 @@ private List doQueryList(Client client, RequestMessage request) while (iter.hasNext()) { Result data = iter.next(); - list.add(data.getObject()); + list.add(normalize(data.getObject())); } return list; } + static Object normalize(Object value) { + return normalize(value, 0, new IdentityHashMap<>()); + } + + private static Object normalize(Object value, int depth, + IdentityHashMap seen) { + if (value == null) { + return null; + } + if (value instanceof Id) { + return ((Id) value).asObject(); + } + boolean composite = value instanceof Map || value instanceof Path || + value instanceof Iterable || + value.getClass().isArray(); + if (!composite) { + return value; + } + if (depth >= NORMALIZE_MAX_DEPTH) { + throw new IllegalArgumentException( + "Exceeded max normalization depth 32"); + } + if (value instanceof Map) { + if (seen.put(value, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "Detected cyclic Cypher result"); + } + Map normalized = new LinkedHashMap<>(); + try { + for (Map.Entry entry : ((Map) value).entrySet()) { + normalized.put(normalize(entry.getKey(), depth + 1, seen), + normalize(entry.getValue(), depth + 1, seen)); + } + } finally { + seen.remove(value); + } + return normalized; + } + if (value instanceof Path) { + if (seen.put(value, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "Detected cyclic Cypher result"); + } + Map normalized = new LinkedHashMap<>(); + try { + Path path = (Path) value; + normalized.put("labels", + normalize(path.labels(), depth + 1, seen)); + normalized.put("objects", + normalize(path.objects(), depth + 1, seen)); + } finally { + seen.remove(value); + } + return normalized; + } + if (value instanceof Iterable) { + if (seen.put(value, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "Detected cyclic Cypher result"); + } + List normalized = new LinkedList<>(); + try { + for (Object item : (Iterable) value) { + normalized.add(normalize(item, depth + 1, seen)); + } + } finally { + seen.remove(value); + } + return normalized; + } + if (value.getClass().isArray()) { + if (seen.put(value, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "Detected cyclic Cypher result"); + } + List normalized = new LinkedList<>(); + try { + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + normalized.add(normalize(Array.get(value, i), depth + 1, + seen)); + } + } finally { + seen.remove(value); + } + return normalized; + } + return value; + } + /** * As Sasl does not support a token, which is a coded string to indicate a legal user, * we had to use a trick to fix it. When the token is set, the password will be set to @@ -120,6 +233,12 @@ private List doQueryList(Client client, RequestMessage request) */ private Configuration getConfig() { Configuration conf = this.configurationSupplier.get(); + if (this.userName == null && this.token == null) { + // Anonymous queries must not inherit a service identity from the YAML file. + conf.clearProperty("username"); + conf.clearProperty("password"); + return conf; + } conf.addProperty("username", this.token == null ? this.userName : this.token); conf.addProperty("password", this.token == null ? this.password : ""); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java new file mode 100644 index 0000000000..7333e5b144 --- /dev/null +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.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.hugegraph.api.cypher; + +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Classify cypher execution failures into stable error codes and attach + * actionable hints for the most common, cryptic translator messages. + */ +public final class CypherErrorMapper { + + public static final String SYNTAX_ERROR = "HugeGraph.Cypher.SyntaxError"; + public static final String UNSUPPORTED_FEATURE = + "HugeGraph.Cypher.UnsupportedFeature"; + public static final String EXECUTION_ERROR = "HugeGraph.Cypher.ExecutionError"; + + private static final Pattern UNDEFINED_VARIABLE = + Pattern.compile("variable `[^`]+` not defined"); + + private static final Pattern PARSER_POSITION = + Pattern.compile("\\(line \\d+, column \\d+ \\(offset: \\d+\\)\\)$"); + + private CypherErrorMapper() { + } + + public static CypherModel.CypherError map(Throwable e) { + String message = e.getMessage() != null ? e.getMessage() : e.toString(); + // strip noisy exception class prefixes, e.g. "...driver.exception.ResponseException: " + message = message.replaceFirst( + "^([a-zA-Z0-9_]+\\.)+[A-Za-z0-9_]+(Exception|Error):\\s*", ""); + String lower = message.toLowerCase(Locale.ROOT); + + if (lower.contains("undefined vertex label") + || lower.contains("undefined edge label") + || lower.contains("undefined property key") + || lower.contains("undefined index label")) { + return new CypherModel.CypherError(EXECUTION_ERROR, message, + "Create the schema element via the schema API before " + + "running the query"); + } + if (UNDEFINED_VARIABLE.matcher(lower).find()) { + return new CypherModel.CypherError(SYNTAX_ERROR, message, + "Declare the variable in a MATCH, UNWIND or WITH clause " + + "before referencing it"); + } + if (lower.contains("invalid input") || lower.contains("unknown function") || + PARSER_POSITION.matcher(lower).find()) { + return new CypherModel.CypherError(SYNTAX_ERROR, message, + "Check the query syntax and function names supported by " + + "the built-in Cypher translator"); + } + if (lower.contains("expected exactly one statement")) { + return new CypherModel.CypherError(SYNTAX_ERROR, message, + "Submit exactly one Cypher statement per request"); + } + if (lower.startsWith("unsupported clause:") || + lower.startsWith("unsupported match pattern:") || + lower.startsWith("unsupported create pattern:") || + lower.startsWith("unsupported property type:") || + lower.startsWith("multiple labels are not supported:") || + lower.startsWith("custom functions and predicates are not supported on target implementation:")) { + return new CypherModel.CypherError(UNSUPPORTED_FEATURE, message, + "This construct is not covered by the built-in translator, " + + "see the Cypher compatibility guide for supported syntax"); + } + return new CypherModel.CypherError(EXECUTION_ERROR, message, ""); + } +} diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherManager.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherManager.java index 95f72d2e4a..ee23b6aa41 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherManager.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherManager.java @@ -44,6 +44,10 @@ private CypherManager(String configurationFile) { this.configurationFile = configurationFile; } + public CypherClient getAnonymousClient() { + return new CypherClient(this::cloneConfig); + } + public CypherClient getClient(String userName, String password) { E.checkArgument(userName != null && !userName.isEmpty(), "The userName parameter can't be null or empty"); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java index e7c3900605..14741a0786 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java @@ -46,10 +46,19 @@ public static CypherModel dataOf(String requestId, List data) { } public static CypherModel failOf(String requestId, String message) { + return failOf(requestId, message, null); + } + + public static CypherModel failOf(String requestId, String message, + CypherError error) { CypherModel res = new CypherModel(); res.requestId = requestId; res.status.code = 400; res.status.message = message; + if (error != null) { + res.status.attributes = Collections.singletonMap( + "errors", Collections.singletonList(error)); + } return res; } @@ -77,4 +86,22 @@ private static class Result { public Map meta = Collections.EMPTY_MAP; } + public static class CypherError { + + @Schema(description = "Stable error classification code") + public String code; + + @Schema(description = "The error message") + public String message; + + @Schema(description = "Actionable hint, empty when unavailable") + public String hint = ""; + + public CypherError(String code, String message, String hint) { + this.code = code; + this.message = message; + this.hint = hint; + } + } + } diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/AuthenticationFilter.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/AuthenticationFilter.java index 96eb273be7..ed37c6592c 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/AuthenticationFilter.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/AuthenticationFilter.java @@ -20,6 +20,7 @@ import static org.apache.hugegraph.config.ServerOptions.WHITE_IP_STATUS; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.Principal; import java.util.HashMap; import java.util.List; @@ -39,7 +40,6 @@ import org.apache.hugegraph.util.Log; import org.apache.tinkerpop.gremlin.server.auth.AuthenticationException; import org.glassfish.grizzly.http.server.Request; -import org.glassfish.grizzly.utils.Charsets; import org.gridkit.jvmtool.cmd.AntPathMatcher; import org.slf4j.Logger; @@ -75,6 +75,10 @@ public class AuthenticationFilter implements ContainerRequestFilter, ContainerRe private static final Logger LOG = Log.logger(AuthenticationFilter.class); private static final AntPathMatcher MATCHER = new AntPathMatcher(); + // TODO: add the unauthenticated /readiness probe (#3221) here once it lands. The Helm chart + // (helm/hugegraph) defaults server.readinessPath to /versions, which stays 200 with zero + // Stores, and offers /readiness as an opt-in; flip that default when this set grows. + // https://github.com/apache/hugegraph/issues/3212 private static final Set FIXED_WHITE_API_SET = ImmutableSet.of( "versions", "openapi.json" @@ -180,14 +184,16 @@ protected User authenticate(ContainerRequestContext context) { if (auth.startsWith(BASIC_AUTH_PREFIX)) { auth = auth.substring(BASIC_AUTH_PREFIX.length()); - auth = new String(DatatypeConverter.parseBase64Binary(auth), Charsets.ASCII_CHARSET); - String[] values = auth.split(":"); - if (values.length != 2) { + // RFC 7617 section 2.1 allows only UTF-8 as the declared charset, and it bars a + // colon from the user-id alone, so split on the first one and keep the rest + auth = new String(DatatypeConverter.parseBase64Binary(auth), StandardCharsets.UTF_8); + int colon = auth.indexOf(':'); + if (colon < 0) { throw new BadRequestException("Invalid syntax for username and password"); } - final String username = values[0]; - final String password = values[1]; + final String username = auth.substring(0, colon); + final String password = auth.substring(colon + 1); if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { throw new BadRequestException("Invalid syntax for username and password"); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/CompressInterceptor.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/CompressInterceptor.java index f38ffd78f3..1cede41e07 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/CompressInterceptor.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/CompressInterceptor.java @@ -22,15 +22,21 @@ import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.text.ParseException; +import java.util.regex.Pattern; import java.util.zip.GZIPOutputStream; import org.apache.hugegraph.api.filter.CompressInterceptor.Compress; import org.apache.hugegraph.util.Log; +import org.glassfish.jersey.message.internal.AcceptableToken; +import org.glassfish.jersey.message.internal.HttpHeaderReader; import org.slf4j.Logger; import jakarta.inject.Singleton; import jakarta.ws.rs.NameBinding; import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.ext.Provider; import jakarta.ws.rs.ext.WriterInterceptor; @@ -45,16 +51,28 @@ public class CompressInterceptor implements WriterInterceptor { private static final Logger LOG = Log.logger(CompressInterceptor.class); + private static final Pattern TRAILING_DOT_QUALITY = + Pattern.compile("(;[ \t]*[qQ]=[01])\\.(?=[ \t]*(?:[,;]|\\z))"); + // Set compress output buffer size to 4KB (about 40~600 vertices) public static final int BUFFER_SIZE = 1024 * 4; + @Context + private jakarta.inject.Provider requestHeaders; + @Override public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { - // If there is no annotation(like exception), we don't compress it - if (context.getAnnotations().length > 0) { + // Exceptions and other responses without @Compress remain unchanged. + Compress compression = getCompressAnnotation(context); + if (compression != null && !context.getHeaders().containsKey(HttpHeaders.CONTENT_ENCODING)) { + addVaryHeader(context.getHeaders()); + } + if (compression != null && + !context.getHeaders().containsKey(HttpHeaders.CONTENT_ENCODING) && + this.acceptsGzip()) { try { - this.compress(context); + this.compress(context, compression); } catch (Throwable e) { LOG.warn("Failed to compress response", e); /* @@ -68,10 +86,49 @@ public void aroundWriteTo(WriterInterceptorContext context) context.proceed(); } - private void compress(WriterInterceptorContext context) + private boolean acceptsGzip() { + String accept = this.requestHeaders.get().getHeaderString(HttpHeaders.ACCEPT_ENCODING); + if (accept == null || accept.isEmpty()) { + return false; + } + // Jersey 3.0.3 misreads legal q=0. and q=1.; retain its token/quality validation. + accept = TRAILING_DOT_QUALITY.matcher(accept).replaceAll("$1"); + int gzip = -1; + int wildcard = 0; + int identity = 0; + try { + // Jersey parses HTTP tokens and quality factors; explicit encodings override '*'. + for (AcceptableToken token : HttpHeaderReader.readAcceptToken(accept)) { + if (GZIP.equalsIgnoreCase(token.getToken())) { + gzip = token.getQuality(); + } else if ("*".equals(token.getToken())) { + wildcard = token.getQuality(); + } else if ("identity".equalsIgnoreCase(token.getToken())) { + identity = token.getQuality(); + } + } + } catch (ParseException e) { + return false; + } + int quality = gzip >= 0 ? gzip : wildcard; + // If no encoding is acceptable, retain the plain response (RFC 9110, 12.4.1). + // Rejecting it here could hide a POST that has already changed graph data. + return quality > 0 && quality >= identity; + } + + private static void addVaryHeader(MultivaluedMap headers) { + for (Object value : headers.getOrDefault(HttpHeaders.VARY, java.util.Collections.emptyList())) { + for (String field : value.toString().split(",")) { + if ("*".equals(field.trim()) || HttpHeaders.ACCEPT_ENCODING.equalsIgnoreCase(field.trim())) { + return; + } + } + } + headers.add(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING); + } + + private void compress(WriterInterceptorContext context, Compress compression) throws IOException { - // Get compress info from the @Compress annotation - final Compress compression = getCompressAnnotation(context); final String encoding = compression.value(); final int buffer = compression.buffer(); @@ -97,7 +154,7 @@ private static Compress getCompressAnnotation(WriterInterceptorContext c) { return (Compress) annotation; } } - throw new AssertionError("Unable find @Compress annotation"); + return null; } @NameBinding diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/ExceptionFilter.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/ExceptionFilter.java index 20dc1808ab..e59c86d991 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/ExceptionFilter.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/ExceptionFilter.java @@ -26,7 +26,7 @@ import javax.json.JsonObjectBuilder; import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.api.API; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.config.ServerOptions; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/RedirectFilter.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/RedirectFilter.java deleted file mode 100644 index 2a2a9142ef..0000000000 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/RedirectFilter.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.api.filter; - -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.apache.commons.lang3.StringUtils; -import org.apache.http.client.utils.URIBuilder; -import org.apache.hugegraph.core.GraphManager; -import org.apache.hugegraph.masterelection.GlobalMasterInfo; -import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.Log; -import org.glassfish.hk2.api.IterableProvider; -import org.glassfish.hk2.api.ServiceHandle; -import org.glassfish.jersey.message.internal.HeaderUtils; -import org.slf4j.Logger; - -import jakarta.ws.rs.NameBinding; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.client.Invocation; -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.core.Response; - -public class RedirectFilter implements ContainerRequestFilter { - - private static final Logger LOG = Log.logger(RedirectFilter.class); - - private static final String X_HG_REDIRECT = "x-hg-redirect"; - - private static volatile Client client = null; - - @Context - private IterableProvider managerProvider; - - private static final Set MUST_BE_NULL = new HashSet<>(); - - static { - MUST_BE_NULL.add("DELETE"); - MUST_BE_NULL.add("GET"); - MUST_BE_NULL.add("HEAD"); - MUST_BE_NULL.add("TRACE"); - } - - @Override - public void filter(ContainerRequestContext context) throws IOException { - ServiceHandle handle = this.managerProvider.getHandle(); - E.checkState(handle != null, "Context GraphManager is absent"); - GraphManager manager = handle.getService(); - E.checkState(manager != null, "Context GraphManager is absent"); - - String redirectTag = context.getHeaderString(X_HG_REDIRECT); - if (StringUtils.isNotEmpty(redirectTag)) { - return; - } - - GlobalMasterInfo globalNodeInfo = manager.globalNodeRoleInfo(); - if (globalNodeInfo == null || !globalNodeInfo.supportElection()) { - return; - } - GlobalMasterInfo.NodeInfo masterInfo = globalNodeInfo.masterInfo(); - if (masterInfo == null || masterInfo.isMaster() || - StringUtils.isEmpty(masterInfo.nodeUrl())) { - return; - } - String url = masterInfo.nodeUrl(); - - URI redirectUri; - try { - URIBuilder redirectURIBuilder = new URIBuilder(context.getUriInfo().getRequestUri()); - URI masterURI = URI.create(url); - redirectURIBuilder.setHost(masterURI.getHost()); - redirectURIBuilder.setPort(masterURI.getPort()); - redirectURIBuilder.setScheme(masterURI.getScheme()); - - redirectUri = redirectURIBuilder.build(); - } catch (URISyntaxException e) { - LOG.error("Redirect request exception occurred", e); - return; - } - this.initClientIfNeeded(); - Response response = this.forwardRequest(context, redirectUri); - context.abortWith(response); - } - - private Response forwardRequest(ContainerRequestContext requestContext, URI redirectUri) { - MultivaluedMap headers = requestContext.getHeaders(); - MultivaluedMap newHeaders = HeaderUtils.createOutbound(); - if (headers != null) { - for (Map.Entry> entry : headers.entrySet()) { - for (String value : entry.getValue()) { - newHeaders.add(entry.getKey(), value); - } - } - } - newHeaders.add(X_HG_REDIRECT, new Date().getTime()); - Invocation.Builder builder = client.target(redirectUri) - .request() - .headers(newHeaders); - Response response; - if (MUST_BE_NULL.contains(requestContext.getMethod())) { - response = builder.method(requestContext.getMethod()); - } else { - response = builder.method(requestContext.getMethod(), - Entity.json(requestContext.getEntityStream())); - } - return response; - } - - private void initClientIfNeeded() { - if (client != null) { - return; - } - - synchronized (RedirectFilter.class) { - if (client != null) { - return; - } - client = ClientBuilder.newClient(); - } - } - - @NameBinding - @Retention(RetentionPolicy.RUNTIME) - public @interface RedirectMasterRole { - - } -} diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/graph/BatchAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/graph/BatchAPI.java index 85beb142db..fc0ff2cc98 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/graph/BatchAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/graph/BatchAPI.java @@ -21,7 +21,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicInteger; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; import org.apache.hugegraph.config.HugeConfig; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/graph/EdgeAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/graph/EdgeAPI.java index 1f229cd6b1..c9c741a657 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/graph/EdgeAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/graph/EdgeAPI.java @@ -29,17 +29,17 @@ import org.apache.hugegraph.api.filter.CompressInterceptor.Compress; import org.apache.hugegraph.api.filter.DecompressInterceptor.Decompress; import org.apache.hugegraph.api.filter.StatusFilter.Status; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.config.ServerOptions; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.UpdateStrategy; import org.apache.hugegraph.exception.NotFoundException; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.optimize.QueryHolder; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/graph/VertexAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/graph/VertexAPI.java index af1433ac46..446b2774e0 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/graph/VertexAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/graph/VertexAPI.java @@ -30,16 +30,16 @@ import org.apache.hugegraph.api.filter.CompressInterceptor.Compress; import org.apache.hugegraph.api.filter.DecompressInterceptor.Decompress; import org.apache.hugegraph.api.filter.StatusFilter.Status; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.config.ServerOptions; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.UpdateStrategy; import org.apache.hugegraph.exception.NotFoundException; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.optimize.QueryHolder; import org.apache.hugegraph.traversal.optimize.Text; @@ -85,7 +85,8 @@ public class VertexAPI extends BatchAPI { @Status(Status.CREATED) @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON_WITH_CHARSET) - @RolesAllowed({"space_member", "$owner=$graph $action=vertex_write"}) + @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + + "$action=vertex_write"}) public String create(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, @@ -108,7 +109,8 @@ public String create(@Context GraphManager manager, @Status(Status.CREATED) @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON_WITH_CHARSET) - @RolesAllowed({"space_member", "$owner=$graph $action=vertex_write"}) + @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + + "$action=vertex_write"}) public String create(@Context HugeConfig config, @Context GraphManager manager, @Parameter(description = "The graph space name") @@ -143,7 +145,8 @@ public String create(@Context HugeConfig config, @Path("batch") @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON_WITH_CHARSET) - @RolesAllowed({"space_member", "$owner=$graph $action=vertex_write"}) + @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + + "$action=vertex_write"}) public String update(@Context HugeConfig config, @Context GraphManager manager, @Parameter(description = "The graph space name") @@ -195,7 +198,8 @@ public String update(@Context HugeConfig config, @Path("{id}") @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON_WITH_CHARSET) - @RolesAllowed({"space_member", "$owner=$graph $action=vertex_write"}) + @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + + "$action=vertex_write"}) public String update(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, @@ -309,7 +313,8 @@ public String list(@Context GraphManager manager, @Timed @Path("{id}") @Produces(APPLICATION_JSON_WITH_CHARSET) - @RolesAllowed({"space_member", "$owner=$graph $action=vertex_read"}) + @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + + "$action=vertex_read"}) public String get(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, @@ -335,7 +340,8 @@ public String get(@Context GraphManager manager, @Timed @Path("{id}") @Consumes(APPLICATION_JSON) - @RolesAllowed({"space_member", "$owner=$graph $action=vertex_delete"}) + @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + + "$action=vertex_delete"}) public void delete(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/AlgorithmAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/AlgorithmAPI.java index 79933bc371..95be404582 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/AlgorithmAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/AlgorithmAPI.java @@ -21,9 +21,8 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.api.filter.RedirectFilter; import org.apache.hugegraph.api.filter.StatusFilter.Status; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.job.AlgorithmJob; import org.apache.hugegraph.job.JobBuilder; @@ -60,7 +59,6 @@ public class AlgorithmAPI extends API { @Status(Status.CREATED) @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON_WITH_CHARSET) - @RedirectFilter.RedirectMasterRole public Map post(@Context GraphManager manager, @Parameter(description = "The graphspace name") @PathParam("graphspace") String graphSpace, diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/ComputerAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/ComputerAPI.java index d5188385cc..478562566a 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/ComputerAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/ComputerAPI.java @@ -21,9 +21,8 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.api.filter.RedirectFilter; import org.apache.hugegraph.api.filter.StatusFilter.Status; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.job.ComputerJob; import org.apache.hugegraph.job.JobBuilder; @@ -60,7 +59,6 @@ public class ComputerAPI extends API { @Status(Status.CREATED) @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON_WITH_CHARSET) - @RedirectFilter.RedirectMasterRole public Map post(@Context GraphManager manager, @Parameter(description = "The graph name") @PathParam("graph") String graph, diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/GremlinAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/GremlinAPI.java index 779cf19b66..d727ebeb53 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/GremlinAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/GremlinAPI.java @@ -25,12 +25,11 @@ import java.util.HashMap; import java.util.Map; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.api.filter.RedirectFilter; import org.apache.hugegraph.api.filter.StatusFilter.Status; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; import org.apache.hugegraph.job.GremlinJob; @@ -77,7 +76,6 @@ public class GremlinAPI extends API { @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + "$action=gremlin_execute"}) - @RedirectFilter.RedirectMasterRole public Map post(@Context GraphManager manager, @Parameter(description = "The graphspace name") @PathParam("graphspace") String graphSpace, diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/RebuildAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/RebuildAPI.java index 3219c8b3ab..1508e9ff7f 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/RebuildAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/RebuildAPI.java @@ -21,9 +21,8 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.api.filter.RedirectFilter; import org.apache.hugegraph.api.filter.StatusFilter.Status; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.util.Log; import org.slf4j.Logger; @@ -55,7 +54,6 @@ public class RebuildAPI extends API { @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space", "$graphspace=$graphspace $owner=$graph " + "$action=index_label_write"}) - @RedirectFilter.RedirectMasterRole public Map vertexLabelRebuild(@Context GraphManager manager, @Parameter(description = "The graphspace name") @PathParam("graphspace") @@ -100,7 +98,6 @@ public Map edgeLabelRebuild(@Context GraphManager manager, @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space", "$graphspace=$graphspace $owner=$graph " + "$action=index_label_write"}) - @RedirectFilter.RedirectMasterRole public Map indexLabelRebuild(@Context GraphManager manager, @Parameter(description = "The graphspace name") @PathParam("graphspace") diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/TaskAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/TaskAPI.java index 7143efb5f7..f3613434c1 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/TaskAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/TaskAPI.java @@ -26,10 +26,9 @@ import org.apache.groovy.util.Maps; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.api.filter.RedirectFilter; import org.apache.hugegraph.api.filter.StatusFilter.Status; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.page.PageInfo; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.task.HugeTask; @@ -153,7 +152,6 @@ public Map get(@Context GraphManager manager, @DELETE @Timed @Path("{id}") - @RedirectFilter.RedirectMasterRole public void delete(@Context GraphManager manager, @Parameter(description = "The graphspace name") @PathParam("graphspace") String graphSpace, @@ -176,7 +174,6 @@ public void delete(@Context GraphManager manager, @Path("{id}") @Status(Status.ACCEPTED) @Produces(APPLICATION_JSON_WITH_CHARSET) - @RedirectFilter.RedirectMasterRole public Map update(@Context GraphManager manager, @Parameter(description = "The graphspace name") @PathParam("graphspace") diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/metrics/MetricsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/metrics/MetricsAPI.java index b457b66bfc..81fdaa3d97 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/metrics/MetricsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/metrics/MetricsAPI.java @@ -19,29 +19,17 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.apache.hugegraph.metrics.MetricsUtil.COUNT_ATTR; import static org.apache.hugegraph.metrics.MetricsUtil.END_LSTR; -import static org.apache.hugegraph.metrics.MetricsUtil.FIFT_MIN_RATE_ATRR; -import static org.apache.hugegraph.metrics.MetricsUtil.FIVE_MIN_RATE_ATRR; -import static org.apache.hugegraph.metrics.MetricsUtil.GAUGE_TYPE; -import static org.apache.hugegraph.metrics.MetricsUtil.HISTOGRAM_TYPE; -import static org.apache.hugegraph.metrics.MetricsUtil.LEFT_NAME_STR; -import static org.apache.hugegraph.metrics.MetricsUtil.MEAN_RATE_ATRR; import static org.apache.hugegraph.metrics.MetricsUtil.METRICS_PATH_FAILED_COUNTER; import static org.apache.hugegraph.metrics.MetricsUtil.METRICS_PATH_RESPONSE_TIME_HISTOGRAM; import static org.apache.hugegraph.metrics.MetricsUtil.METRICS_PATH_SUCCESS_COUNTER; import static org.apache.hugegraph.metrics.MetricsUtil.METRICS_PATH_TOTAL_COUNTER; -import static org.apache.hugegraph.metrics.MetricsUtil.ONE_MIN_RATE_ATRR; import static org.apache.hugegraph.metrics.MetricsUtil.PROM_HELP_NAME; -import static org.apache.hugegraph.metrics.MetricsUtil.RIGHT_NAME_STR; import static org.apache.hugegraph.metrics.MetricsUtil.SPACE_STR; import static org.apache.hugegraph.metrics.MetricsUtil.STR_HELP; import static org.apache.hugegraph.metrics.MetricsUtil.STR_TYPE; import static org.apache.hugegraph.metrics.MetricsUtil.UNTYPED; import static org.apache.hugegraph.metrics.MetricsUtil.VERSION_STR; -import static org.apache.hugegraph.metrics.MetricsUtil.exportSnapshot; -import static org.apache.hugegraph.metrics.MetricsUtil.replaceDotDashInKey; -import static org.apache.hugegraph.metrics.MetricsUtil.replaceSlashInKey; import java.util.HashMap; import java.util.LinkedHashMap; @@ -64,9 +52,7 @@ import org.slf4j.Logger; import com.codahale.metrics.Counter; -import com.codahale.metrics.Gauge; import com.codahale.metrics.Histogram; -import com.codahale.metrics.Meter; import com.codahale.metrics.Metric; import com.codahale.metrics.annotation.Timed; @@ -234,7 +220,6 @@ public String baseMetricAll() { private String baseMetricPrometheusAll() { StringBuilder promMetric = new StringBuilder(); - ServerReporter reporter = ServerReporter.instance(); String helpName = PROM_HELP_NAME; // build version info promMetric.append(STR_HELP) @@ -247,98 +232,6 @@ private String baseMetricPrometheusAll() { .append(ApiVersion.VERSION.toString()).append("\",}") .append(SPACE_STR + "1.0" + END_LSTR); - // build gauges metric info - for (String key : reporter.gauges().keySet()) { - final Gauge gauge - = reporter.gauges().get(key); - if (gauge != null) { - helpName = replaceDotDashInKey(key); - promMetric.append(STR_HELP) - .append(helpName).append(END_LSTR); - promMetric.append(STR_TYPE) - .append(helpName).append(SPACE_STR + GAUGE_TYPE + END_LSTR); - promMetric.append(helpName) - .append(SPACE_STR + gauge.getValue() + END_LSTR); - } - } - - // build histograms metric info - for (String histogramkey : reporter.histograms().keySet()) { - final Histogram histogram = reporter.histograms().get(histogramkey); - if (histogram != null) { - helpName = replaceDotDashInKey(histogramkey); - promMetric.append(STR_HELP) - .append(helpName).append(END_LSTR); - promMetric.append(STR_TYPE) - .append(helpName) - .append(SPACE_STR + HISTOGRAM_TYPE + END_LSTR); - - promMetric.append(helpName) - .append(COUNT_ATTR) - .append(histogram.getCount() + END_LSTR); - promMetric.append( - exportSnapshot(helpName, histogram.getSnapshot())); - } - } - - // build meters metric info - for (String meterkey : reporter.meters().keySet()) { - final Meter metric = reporter.meters().get(meterkey); - if (metric != null) { - helpName = replaceDotDashInKey(meterkey); - promMetric.append(STR_HELP) - .append(helpName).append(END_LSTR); - promMetric.append(STR_TYPE) - .append(helpName) - .append(SPACE_STR + HISTOGRAM_TYPE + END_LSTR); - - promMetric.append(helpName) - .append(COUNT_ATTR) - .append(metric.getCount() + END_LSTR); - promMetric.append(helpName) - .append(MEAN_RATE_ATRR) - .append(metric.getMeanRate() + END_LSTR); - promMetric.append(helpName) - .append(ONE_MIN_RATE_ATRR) - .append(metric.getOneMinuteRate() + END_LSTR); - promMetric.append(helpName) - .append(FIVE_MIN_RATE_ATRR) - .append(metric.getFiveMinuteRate() + END_LSTR); - promMetric.append(helpName) - .append(FIFT_MIN_RATE_ATRR) - .append(metric.getFifteenMinuteRate() + END_LSTR); - } - } - - // build timer metric info - for (String timerkey : reporter.timers().keySet()) { - final com.codahale.metrics.Timer timer = reporter.timers() - .get(timerkey); - if (timer != null) { - helpName = replaceDotDashInKey(timerkey); - promMetric.append(STR_HELP) - .append(helpName).append(END_LSTR); - promMetric.append(STR_TYPE) - .append(helpName) - .append(SPACE_STR + HISTOGRAM_TYPE + END_LSTR); - - promMetric.append(helpName) - .append(COUNT_ATTR) - .append(timer.getCount() + END_LSTR); - promMetric.append(helpName) - .append(ONE_MIN_RATE_ATRR) - .append(timer.getOneMinuteRate() + END_LSTR); - promMetric.append(helpName) - .append(FIVE_MIN_RATE_ATRR) - .append(timer.getFiveMinuteRate() + END_LSTR); - promMetric.append(helpName) - .append(FIFT_MIN_RATE_ATRR) - .append(timer.getFifteenMinuteRate() + END_LSTR); - promMetric.append( - exportSnapshot(helpName, timer.getSnapshot())); - } - } - MetricsUtil.writePrometheusFormat(promMetric, MetricManager.INSTANCE.getRegistry()); return promMetric.toString(); @@ -411,19 +304,7 @@ private String statisticsProm(Map> metricMap) { .append(ApiVersion.VERSION.toString()).append("\",}") .append(SPACE_STR + "1.0" + END_LSTR); - for (String methodKey : metricMap.keySet()) { - String metricName = replaceSlashInKey(methodKey); - promMetric.append(STR_HELP) - .append(metricName).append(END_LSTR); - promMetric.append(STR_TYPE) - .append(metricName).append(SPACE_STR + GAUGE_TYPE + END_LSTR); - Map itemMetricMap = metricMap.get(methodKey); - for (String labelName : itemMetricMap.keySet()) { - promMetric.append(metricName).append(LEFT_NAME_STR).append(labelName) - .append(RIGHT_NAME_STR).append(itemMetricMap.get(labelName)) - .append(END_LSTR); - } - } + MetricsUtil.writeStatisticsPrometheusFormat(promMetric, metricMap); return promMetric.toString(); } diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java index 94169e4a90..66343de6eb 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/profile/GraphsAPI.java @@ -29,14 +29,17 @@ import java.util.Map; import java.util.Set; import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.LoadingMode; import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.StatusFilter; +import org.apache.hugegraph.api.filter.AuthenticationFilter.Authorizer; import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.HugeAuthenticator.RequiredPerm; import org.apache.hugegraph.auth.HugeGraphAuthProxy; import org.apache.hugegraph.auth.HugePermission; +import org.apache.hugegraph.auth.RolePermission; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.space.GraphSpace; @@ -56,6 +59,7 @@ import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Singleton; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.ClientErrorException; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.GET; @@ -67,7 +71,9 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.SecurityContext; +import jakarta.ws.rs.core.Response; @Path("graphspaces/{graphspace}/graphs") @Singleton @@ -78,6 +84,7 @@ public class GraphsAPI extends API { private static final String CONFIRM_CLEAR = "I'm sure to delete all data"; private static final String CONFIRM_DROP = "I'm sure to drop the graph"; + private static final String CONFIRM_RECOVER = "I'm sure all imports have stopped"; private static final String GRAPH_DESCRIPTION = "description"; private static final String GRAPH_ACTION = "action"; private static final String UPDATE = "update"; @@ -674,7 +681,11 @@ public Map mode(@Context GraphManager manager, E.checkArgument(mode != null, "Graph mode can't be null"); HugeGraph g = graph(manager, graphSpace, name); - g.mode(mode); + try { + g.mode(mode); + } catch (LoadingMode.ConflictException e) { + throw new ClientErrorException(e.getMessage(), Response.Status.CONFLICT, e); + } return ImmutableMap.of("mode", mode); } @@ -693,6 +704,63 @@ public Map mode(@Context GraphManager manager, return ImmutableMap.of("mode", g.mode()); } + @GET + @Timed + @Path("{name}/mode/owners") + @Produces(APPLICATION_JSON_WITH_CHARSET) + @RolesAllowed({"space", "$graphspace=$graphspace $owner=$name"}) + public Map loadingMode(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace, + @PathParam("name") String name) { + return graph(manager, graphSpace, name).loadingMode(); + } + + @PUT + @Timed + @Path("{name}/mode/owners") + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON_WITH_CHARSET) + @RolesAllowed({"space", "$graphspace=$graphspace $owner=$name"}) + public Map loadingMode(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace, + @PathParam("name") String name, + JsonLoadingOwner owner, + @Context ContainerRequestContext requestContext) { + E.checkArgument(owner != null && owner.epoch != null, "Loading owner body and epoch are required"); + if ("recover".equals(owner.action)) { + // Read the filter's Authorizer rather than Jersey's injected SecurityContext wrapper. + SecurityContext securityContext = requestContext == null ? null : requestContext.getSecurityContext(); + if (!(securityContext instanceof Authorizer) || + !RolePermission.isAdmin(((Authorizer) securityContext).role())) { + throw new ForbiddenException("Loading recovery requires administrator permission"); + } + E.checkArgument(CONFIRM_RECOVER.equals(owner.confirm), + "Stop and drain all related imports, then confirm with '%s'", CONFIRM_RECOVER); + E.checkArgument(owner.revision != null && owner.revision >= 0, + "Current loading state revision is required for recovery"); + } + try { + HugeGraph graph = graph(manager, graphSpace, name); + if ("recover".equals(owner.action)) { + return graph.recoverLoadingMode(owner.instance, owner.epoch, owner.revision); + } + return graph.loadingMode(owner.action, owner.instance, owner.epoch, owner.token, + HugeGraphAuthProxy.username()); + } catch (LoadingMode.ConflictException e) { + throw new ClientErrorException(e.getMessage(), Response.Status.CONFLICT, e); + } + } + + public static class JsonLoadingOwner { + + public String action; + public String instance; + public Long epoch; + public String token; + public String confirm; + public Long revision; + } + @PUT @Timed @Path("{name}/graph_read_mode") diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/raft/RaftAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/raft/RaftAPI.java index f868df522e..f3987635f7 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/raft/RaftAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/raft/RaftAPI.java @@ -21,12 +21,11 @@ import java.util.List; import java.util.Map; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.api.filter.RedirectFilter; import org.apache.hugegraph.api.filter.StatusFilter.Status; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.store.raft.RaftAddPeerJob; import org.apache.hugegraph.backend.store.raft.RaftGroupManager; import org.apache.hugegraph.backend.store.raft.RaftRemovePeerJob; @@ -173,7 +172,6 @@ public Map setLeader(@Context GraphManager manager, @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member"}) - @RedirectFilter.RedirectMasterRole public Map addPeer(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, @@ -209,7 +207,6 @@ public Map addPeer(@Context GraphManager manager, @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member"}) - @RedirectFilter.RedirectMasterRole public Map removePeer(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/EdgeLabelAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/EdgeLabelAPI.java index f2026d58bd..3a7db5f85c 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/EdgeLabelAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/EdgeLabelAPI.java @@ -26,13 +26,12 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.api.filter.RedirectFilter; import org.apache.hugegraph.api.filter.StatusFilter.Status; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.Userdata; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.Userdata; import org.apache.hugegraph.type.define.EdgeLabelType; import org.apache.hugegraph.type.define.Frequency; import org.apache.hugegraph.type.define.GraphMode; @@ -75,7 +74,6 @@ public class EdgeLabelAPI extends API { @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + "$action=edge_label_write"}) - @RedirectFilter.RedirectMasterRole public String create(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, @@ -98,7 +96,6 @@ public String create(@Context GraphManager manager, @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + "$action=edge_label_write"}) - @RedirectFilter.RedirectMasterRole public String update(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, @@ -185,7 +182,6 @@ public String get(@Context GraphManager manager, @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + "$action=edge_label_delete"}) - @RedirectFilter.RedirectMasterRole public Map delete(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/IndexLabelAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/IndexLabelAPI.java index b76d532360..bf66d3ea95 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/IndexLabelAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/IndexLabelAPI.java @@ -25,14 +25,13 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.api.filter.RedirectFilter; import org.apache.hugegraph.api.filter.StatusFilter.Status; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.Userdata; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.Userdata; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.GraphMode; import org.apache.hugegraph.type.define.IndexType; @@ -75,7 +74,6 @@ public class IndexLabelAPI extends API { @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + "$action=index_label_write"}) - @RedirectFilter.RedirectMasterRole public String create(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, @@ -97,7 +95,6 @@ public String create(@Context GraphManager manager, @Path("{name}") @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON_WITH_CHARSET) - @RedirectFilter.RedirectMasterRole public String update(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, @@ -183,7 +180,6 @@ public String get(@Context GraphManager manager, @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + "$action=index_label_delete"}) - @RedirectFilter.RedirectMasterRole public Map delete(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/PropertyKeyAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/PropertyKeyAPI.java index 27d6ab1da2..9cf19c819f 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/PropertyKeyAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/PropertyKeyAPI.java @@ -25,15 +25,14 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.api.filter.RedirectFilter; import org.apache.hugegraph.api.filter.StatusFilter.Status; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.Userdata; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.Userdata; import org.apache.hugegraph.type.define.AggregateType; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; @@ -78,7 +77,6 @@ public class PropertyKeyAPI extends API { @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + "$action=property_key_write"}) - @RedirectFilter.RedirectMasterRole public String create(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, @@ -102,7 +100,6 @@ public String create(@Context GraphManager manager, @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + "$action=property_key_write"}) - @RedirectFilter.RedirectMasterRole public String update(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, @@ -205,7 +202,6 @@ public String get(@Context GraphManager manager, @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + "$action=property_key_delete"}) - @RedirectFilter.RedirectMasterRole public Map delete(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/VertexLabelAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/VertexLabelAPI.java index c86622f7e5..ff3320ae74 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/VertexLabelAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/schema/VertexLabelAPI.java @@ -25,13 +25,12 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.api.filter.RedirectFilter; import org.apache.hugegraph.api.filter.StatusFilter.Status; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.Checkable; -import org.apache.hugegraph.schema.Userdata; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.define.GraphMode; import org.apache.hugegraph.type.define.IdStrategy; import org.apache.hugegraph.util.E; @@ -73,7 +72,6 @@ public class VertexLabelAPI extends API { @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + "$action=vertex_label_write"}) - @RedirectFilter.RedirectMasterRole public String create(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, @@ -97,7 +95,6 @@ public String create(@Context GraphManager manager, @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + "$action=vertex_label_write"}) - @RedirectFilter.RedirectMasterRole public String update(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, @@ -186,7 +183,6 @@ public String get(@Context GraphManager manager, @Produces(APPLICATION_JSON_WITH_CHARSET) @RolesAllowed({"space_member", "$graphspace=$graphspace $owner=$graph " + "$action=vertex_label_delete"}) - @RedirectFilter.RedirectMasterRole public Map delete(@Context GraphManager manager, @Parameter(description = "The graph space name") @PathParam("graphspace") String graphSpace, diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java index cffca156cd..e1dab458f6 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/space/SchemaTemplateAPI.java @@ -23,7 +23,7 @@ import java.util.function.Supplier; import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.api.API; import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.api.filter.StatusFilter; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/AdamicAdarAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/AdamicAdarAPI.java index 6bf8bf3b82..11cdc3dec0 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/AdamicAdarAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/AdamicAdarAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_ELEMENTS_LIMIT; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; @@ -24,7 +26,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.PredictionTraverser; import org.apache.hugegraph.type.define.Directions; @@ -77,7 +79,7 @@ public String get(@Context GraphManager manager, Id targetId = VertexAPI.checkAndParseVertexId(other); E.checkArgument(!current.equals(other), "The source and target vertex id can't be same"); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); try (PredictionTraverser traverser = new PredictionTraverser(g)) { diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/AllShortestPathsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/AllShortestPathsAPI.java index 3880b1239e..f02f116a1e 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/AllShortestPathsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/AllShortestPathsAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_CAPACITY; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; @@ -29,7 +31,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; import org.apache.hugegraph.traversal.algorithm.ShortestPathTraverser; @@ -103,7 +105,7 @@ public String get(@Context GraphManager manager, Id sourceId = VertexAPI.checkAndParseVertexId(source); Id targetId = VertexAPI.checkAndParseVertexId(target); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CountAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CountAPI.java index 7dd58dd892..9ac1bf69c3 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CountAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CountAPI.java @@ -27,7 +27,7 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.algorithm.CountTraverser; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CrosspointsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CrosspointsAPI.java index de728b57dc..2d98c4d144 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CrosspointsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CrosspointsAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_CAPACITY; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_PATHS_LIMIT; @@ -25,7 +27,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; import org.apache.hugegraph.traversal.algorithm.PathsTraverser; @@ -78,7 +80,7 @@ public String get(@Context GraphManager manager, ApiMeasurer measure = new ApiMeasurer(); Id sourceId = VertexAPI.checkAndParseVertexId(source); Id targetId = VertexAPI.checkAndParseVertexId(target); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); PathsTraverser traverser = new PathsTraverser(g); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CustomizedCrosspointsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CustomizedCrosspointsAPI.java index 6307ad332c..e9c5028da2 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CustomizedCrosspointsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CustomizedCrosspointsAPI.java @@ -31,7 +31,7 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.CustomizedCrosspointsTraverser; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CustomizedPathsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CustomizedPathsAPI.java index 4f3a783a3b..383fec6e5c 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CustomizedPathsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/CustomizedPathsAPI.java @@ -32,7 +32,7 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.CustomizePathsTraverser; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/EdgeExistenceAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/EdgeExistenceAPI.java index 388f40e6ef..154d031008 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/EdgeExistenceAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/EdgeExistenceAPI.java @@ -23,7 +23,7 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.EdgeExistenceTraverser; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/EdgesAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/EdgesAPI.java index 807fcce92c..9ae331a02d 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/EdgesAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/EdgesAPI.java @@ -25,9 +25,9 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.CompressInterceptor.Compress; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.store.Shard; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.type.HugeType; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/FusiformSimilarityAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/FusiformSimilarityAPI.java index 2c0ea1ff33..6536fc22c3 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/FusiformSimilarityAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/FusiformSimilarityAPI.java @@ -27,7 +27,7 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.FusiformSimilarityTraverser; import org.apache.hugegraph.traversal.algorithm.FusiformSimilarityTraverser.SimilarsMap; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/JaccardSimilarityAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/JaccardSimilarityAPI.java index 59f94f23e2..f410212bb2 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/JaccardSimilarityAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/JaccardSimilarityAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_CAPACITY; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_LIMIT; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; @@ -26,7 +28,7 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.algorithm.JaccardSimilarTraverser; @@ -79,7 +81,7 @@ public String get(@Context GraphManager manager, Id sourceId = VertexAPI.checkAndParseVertexId(vertex); Id targetId = VertexAPI.checkAndParseVertexId(other); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); double similarity; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/KneighborAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/KneighborAPI.java index 83183d08c5..6351c03e44 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/KneighborAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/KneighborAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_ELEMENTS_LIMIT; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.NO_LIMIT; @@ -30,7 +32,7 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.structure.HugeVertex; @@ -104,7 +106,7 @@ public String get(@Context GraphManager manager, ApiMeasurer measure = new ApiMeasurer(); Id source = VertexAPI.checkAndParseVertexId(sourceV); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/KoutAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/KoutAPI.java index e784e38b40..38bb536e0d 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/KoutAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/KoutAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_CAPACITY; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_ELEMENTS_LIMIT; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; @@ -31,7 +33,7 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.structure.HugeVertex; @@ -110,7 +112,7 @@ public String get(@Context GraphManager manager, ApiMeasurer measure = new ApiMeasurer(); Id sourceId = VertexAPI.checkAndParseVertexId(source); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/MultiNodeShortestPathAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/MultiNodeShortestPathAPI.java index a2a7a4fd96..6b42350cf1 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/MultiNodeShortestPathAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/MultiNodeShortestPathAPI.java @@ -25,7 +25,7 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; import org.apache.hugegraph.traversal.algorithm.MultiNodeShortestPathTraverser; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/NeighborRankAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/NeighborRankAPI.java index 08396aa0b3..c5ec47b7da 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/NeighborRankAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/NeighborRankAPI.java @@ -28,7 +28,7 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.algorithm.NeighborRankTraverser; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/PathsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/PathsAPI.java index 5c3a5c5d99..b4345b1d59 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/PathsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/PathsAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_CAPACITY; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEPTH; @@ -30,7 +32,7 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.CollectionPathsTraverser; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; @@ -92,7 +94,7 @@ public String get(@Context GraphManager manager, Id sourceId = VertexAPI.checkAndParseVertexId(source); Id targetId = VertexAPI.checkAndParseVertexId(target); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); PathsTraverser traverser = new PathsTraverser(g); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/PersonalRankAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/PersonalRankAPI.java index 45393e2a2b..5367eed10b 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/PersonalRankAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/PersonalRankAPI.java @@ -26,7 +26,7 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.API; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/RaysAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/RaysAPI.java index fda3d16369..aa3b20310c 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/RaysAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/RaysAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_CAPACITY; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_PATHS_LIMIT; @@ -29,7 +31,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; import org.apache.hugegraph.traversal.algorithm.SubGraphTraverser; @@ -85,7 +87,7 @@ public String get(@Context GraphManager manager, ApiMeasurer measure = new ApiMeasurer(); Id source = VertexAPI.checkAndParseVertexId(sourceV); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/ResourceAllocationAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/ResourceAllocationAPI.java index d3ff2b4ae5..32b2d305f0 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/ResourceAllocationAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/ResourceAllocationAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_ELEMENTS_LIMIT; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; @@ -24,7 +26,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.PredictionTraverser; import org.apache.hugegraph.type.define.Directions; @@ -77,7 +79,7 @@ public String create(@Context GraphManager manager, Id targetId = VertexAPI.checkAndParseVertexId(other); E.checkArgument(!current.equals(other), "The source and target vertex id can't be same"); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); try (PredictionTraverser traverser = new PredictionTraverser(g)) { diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/RingsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/RingsAPI.java index 62726fd2f9..5f2c76172b 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/RingsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/RingsAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_CAPACITY; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_PATHS_LIMIT; @@ -29,7 +31,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; import org.apache.hugegraph.traversal.algorithm.SubGraphTraverser; @@ -88,7 +90,7 @@ public String get(@Context GraphManager manager, ApiMeasurer measure = new ApiMeasurer(); Id source = VertexAPI.checkAndParseVertexId(sourceV); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/SameNeighborsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/SameNeighborsAPI.java index a4e652629e..c89c53c44c 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/SameNeighborsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/SameNeighborsAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_ELEMENTS_LIMIT; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; @@ -30,7 +32,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.algorithm.SameNeighborTraverser; @@ -83,7 +85,7 @@ public String get(@Context GraphManager manager, Id sourceId = VertexAPI.checkAndParseVertexId(vertex); Id targetId = VertexAPI.checkAndParseVertexId(other); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); SameNeighborTraverser traverser = new SameNeighborTraverser(g); @@ -108,7 +110,7 @@ public String sameNeighbors(@Context GraphManager manager, ApiMeasurer measure = new ApiMeasurer(); - Directions dir = Directions.convert(EdgeAPI.parseDirection(request.direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(request.direction)); HugeGraph g = graph(manager, graphSpace, graph); SameNeighborTraverser traverser = new SameNeighborTraverser(g); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/SameNeighborsBatchAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/SameNeighborsBatchAPI.java new file mode 100644 index 0000000000..571e2c5971 --- /dev/null +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/SameNeighborsBatchAPI.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.hugegraph.api.traversers; + +import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_ELEMENTS_LIMIT; +import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.api.API; +import org.apache.hugegraph.api.graph.EdgeAPI; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.structure.HugeVertex; +import org.apache.hugegraph.traversal.algorithm.HugeTraverser; +import org.apache.hugegraph.traversal.algorithm.SameNeighborTraverser; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.Log; +import org.apache.hugegraph.util.TinkerPopUtil; +import org.slf4j.Logger; + +import com.codahale.metrics.annotation.Timed; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.inject.Singleton; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; + +@Path("graphspaces/{graphspace}/graphs/{graph}/traversers/sameneighborsbatch") +@Singleton +@Tag(name = "SameNeighborsBatchAPI") +public class SameNeighborsBatchAPI extends API { + + private static final Logger LOG = Log.logger(SameNeighborsBatchAPI.class); + + @POST + @Timed + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON_WITH_CHARSET) + public String post(@Context GraphManager manager, + @PathParam("graphspace") String graphSpace, + @PathParam("graph") String graph, + Request request) { + E.checkArgumentNotNull(request, "The request body can't be null"); + E.checkArgumentNotNull(request.vertexList, "The vertex_list can't be null"); + E.checkArgument(!request.vertexList.isEmpty(), "The vertex_list can't be empty"); + HugeTraverser.checkDegree(request.maxDegree); + HugeTraverser.checkLimit(request.limit); + E.checkArgument(request.limit <= Integer.MAX_VALUE, + "The limit must be <= %s, but got: %s", + Integer.MAX_VALUE, request.limit); + int limit = Math.toIntExact(request.limit); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(request.direction)); + + // Validate all pairs before accessing the graph or starting traversal. + List> pairs = new ArrayList<>(request.vertexList.size()); + for (List pair : request.vertexList) { + E.checkArgumentNotNull(pair, "The vertex pair can't be null"); + E.checkArgument(pair.size() == 2, + "Each vertex pair must contain exactly 2 ids"); + List ids = new ArrayList<>(2); + for (Object value : pair) { + E.checkArgumentNotNull(value, "The vertex id can't be null"); + if (value instanceof Number) { + E.checkArgument(!(value instanceof Float || value instanceof Double), + "Numeric vertex ids must be whole numbers in the long range"); + try { + new BigDecimal(value.toString()).longValueExact(); + } catch (ArithmeticException | NumberFormatException e) { + throw new IllegalArgumentException( + "Numeric vertex ids must be whole numbers in the long range", e); + } + } + try { + ids.add(HugeVertex.getIdValue(value)); + } catch (UnsupportedOperationException e) { + throw new IllegalArgumentException( + "The vertex id must be formatted as Number/String/UUID", e); + } + } + pairs.add(ids); + } + + LOG.debug("Graph [{}] get same neighbors for vertex pairs '{}'", graph, request); + ApiMeasurer measure = new ApiMeasurer(); + HugeGraph g = graph(manager, graphSpace, graph); + SameNeighborTraverser traverser = new SameNeighborTraverser(g); + List> results = new ArrayList<>(pairs.size()); + for (List pair : pairs) { + results.add(traverser.sameNeighbors(pair.get(0), pair.get(1), dir, + request.label, request.maxDegree, limit)); + } + // The traverser counters accumulate across pairs; add them only once. + measure.addIterCount(traverser.vertexIterCounter.get(), + traverser.edgeIterCounter.get()); + return manager.serializer(g, measure.measures()) + .writeList("same_neighbors", results); + } + + private static class Request { + + @JsonProperty("vertex_list") + public List> vertexList; + @JsonProperty("direction") + public String direction; + @JsonProperty("label") + public String label; + @JsonProperty("max_degree") + public long maxDegree = Long.parseLong(DEFAULT_MAX_DEGREE); + @JsonProperty("limit") + public long limit = Long.parseLong(DEFAULT_ELEMENTS_LIMIT); + + @Override + public String toString() { + return String.format("Request{vertex_list=%s,direction=%s,label=%s," + + "max_degree=%s,limit=%s}", this.vertexList, + this.direction, this.label, this.maxDegree, this.limit); + } + } +} diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/ShortestPathAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/ShortestPathAPI.java index 6a6ecd2317..16a4b26cb8 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/ShortestPathAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/ShortestPathAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_CAPACITY; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; @@ -28,7 +30,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; import org.apache.hugegraph.traversal.algorithm.ShortestPathTraverser; @@ -103,7 +105,7 @@ public String get(@Context GraphManager manager, Id sourceId = VertexAPI.checkAndParseVertexId(source); Id targetId = VertexAPI.checkAndParseVertexId(target); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/SingleSourceShortestPathAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/SingleSourceShortestPathAPI.java index 85e23d9b76..50fc31b154 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/SingleSourceShortestPathAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/SingleSourceShortestPathAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_CAPACITY; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_PATHS_LIMIT; @@ -28,7 +30,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; import org.apache.hugegraph.traversal.algorithm.SingleSourceShortestPathTraverser; @@ -88,7 +90,7 @@ public String get(@Context GraphManager manager, ApiMeasurer measure = new ApiMeasurer(); Id sourceId = VertexAPI.checkAndParseVertexId(source); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); HugeGraph g = graph(manager, graphSpace, graph); SingleSourceShortestPathTraverser traverser = diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/TemplatePathsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/TemplatePathsAPI.java index d264d6b3b2..84f963baa3 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/TemplatePathsAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/TemplatePathsAPI.java @@ -27,7 +27,7 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; import org.apache.hugegraph.traversal.algorithm.TemplatePathsTraverser; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/Vertices.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/Vertices.java index 3efee83ab2..874ab24791 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/Vertices.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/Vertices.java @@ -24,9 +24,9 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.schema.SchemaLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.struct.schema.SchemaLabel; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.optimize.TraversalUtil; import org.apache.hugegraph.type.HugeType; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/VerticesAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/VerticesAPI.java index 2f853ec352..0dc09256fe 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/VerticesAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/VerticesAPI.java @@ -26,9 +26,9 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.filter.CompressInterceptor.Compress; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.store.Shard; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/WeightedShortestPathAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/WeightedShortestPathAPI.java index e705bfba09..a9bbcaf0d3 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/WeightedShortestPathAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/traversers/WeightedShortestPathAPI.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.api.traversers; +import org.apache.hugegraph.util.TinkerPopUtil; + import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_CAPACITY; import static org.apache.hugegraph.traversal.algorithm.HugeTraverser.DEFAULT_MAX_DEGREE; @@ -28,7 +30,7 @@ import org.apache.hugegraph.api.API; import org.apache.hugegraph.api.graph.EdgeAPI; import org.apache.hugegraph.api.graph.VertexAPI; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; @@ -101,7 +103,7 @@ public String get(@Context GraphManager manager, ApiMeasurer measure = new ApiMeasurer(); Id sourceId = VertexAPI.checkAndParseVertexId(source); Id targetId = VertexAPI.checkAndParseVertexId(target); - Directions dir = Directions.convert(EdgeAPI.parseDirection(direction)); + Directions dir = TinkerPopUtil.direction(EdgeAPI.parseDirection(direction)); E.checkArgumentNotNull(weight, "The weight property can't be null"); HugeGraph g = graph(manager, graphSpace, graph); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/ContextGremlinServer.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/ContextGremlinServer.java index e859050132..f183871814 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/ContextGremlinServer.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/ContextGremlinServer.java @@ -21,12 +21,13 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadFactory; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.auth.HugeGraphAuthProxy.Context; import org.apache.hugegraph.auth.HugeGraphAuthProxy.ContextThreadPoolExecutor; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.event.EventHub; +import org.apache.hugegraph.server.HugeGraphWsAndHttpChannelizer; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.util.Events; import org.apache.hugegraph.util.Log; @@ -35,6 +36,7 @@ import org.apache.tinkerpop.gremlin.server.GraphManager; import org.apache.tinkerpop.gremlin.server.GremlinServer; import org.apache.tinkerpop.gremlin.server.Settings; +import org.apache.tinkerpop.gremlin.server.channel.WsAndHttpChannelizer; import org.apache.tinkerpop.gremlin.server.util.ThreadFactoryUtil; import org.apache.tinkerpop.gremlin.structure.Graph; import org.slf4j.Logger; @@ -58,11 +60,18 @@ public ContextGremlinServer(final Settings settings, EventHub eventHub) { /* * pass custom Executor https://github.com/apache/tinkerpop/pull/813 */ - super(settings, newGremlinExecutorService(settings)); + super(configureChannelizer(settings), newGremlinExecutorService(settings)); this.eventHub = eventHub; this.listenChanges(); } + static Settings configureChannelizer(Settings settings) { + if (WsAndHttpChannelizer.class.getName().equals(settings.channelizer)) { + settings.channelizer = HugeGraphWsAndHttpChannelizer.class.getName(); + } + return settings; + } + private void listenChanges() { this.eventHub.listen(Events.GRAPH_CREATE, event -> { LOG.debug("GremlinServer accepts event '{}'", event.name()); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeAuthenticator.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeAuthenticator.java index 243342b63f..e58a7eb5a9 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeAuthenticator.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeAuthenticator.java @@ -22,16 +22,16 @@ import java.util.Map; import java.util.regex.Pattern; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.auth.HugeGraphAuthProxy.Context; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.config.OptionSpace; import org.apache.hugegraph.config.ServerOptions; import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.type.Nameable; +import org.apache.hugegraph.type.Namifiable; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.JsonUtil; import org.apache.tinkerpop.gremlin.groovy.jsr223.dsl.credential.CredentialGraphTokens; @@ -477,12 +477,12 @@ private boolean matchResource(HugePermission requiredAction, // check label String requiredLabel; if (requiredType.isSchema()) { - requiredLabel = ((Nameable) requiredResource.operated()).name(); + requiredLabel = ((Namifiable) requiredResource.operated()).name(); } else if (requiredType.isGraph()) { if (requiredResource.operated() instanceof HugeElement) { requiredLabel = ((HugeElement) requiredResource.operated()).label(); } else { - requiredLabel = ((Nameable) requiredResource.operated()).name(); + requiredLabel = ((Namifiable) requiredResource.operated()).name(); } } else { return true; @@ -651,7 +651,7 @@ public static RequiredPerm fromJson(String json) { } public ResourceObject resourceObject() { - Nameable elem = HugeResource.NameObject.ANY; + Namifiable elem = HugeResource.NameObject.ANY; return ResourceObject.of(this.graphSpace, this.owner, this.resource, elem); } } diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeFactoryAuthProxy.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeFactoryAuthProxy.java index 5ff2925e69..98f9ea8bc4 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeFactoryAuthProxy.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeFactoryAuthProxy.java @@ -27,7 +27,7 @@ import java.util.Set; import org.apache.commons.configuration2.Configuration; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.StandardHugeGraph; @@ -38,7 +38,7 @@ import org.apache.hugegraph.backend.tx.IndexableTransaction; import org.apache.hugegraph.concurrent.LockManager; import org.apache.hugegraph.metrics.ServerReporter; -import org.apache.hugegraph.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaElement; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.schema.builder.AbstractBuilder; import org.apache.hugegraph.schema.builder.EdgeLabelBuilder; @@ -185,7 +185,7 @@ private static void registerPrivateActions() { "this$0"); Reflection.registerFieldsToFilter(HugeGraphAuthProxy.Context.class, "ADMIN", "user"); Reflection.registerFieldsToFilter(HugeGraphAuthProxy.ContextTask.class, "runner", - "context"); + "cleanup", "context"); Reflection.registerFieldsToFilter(StandardHugeGraph.class, "LOG", "started", "closed", "mode", "variables", "name", "params", "configuration", "schemaEventHub", "graphEventHub", "indexEventHub", @@ -203,7 +203,8 @@ private static void registerPrivateActions() { "access$14", "access$15", "access$16", "access$17", "access$18", "serializer", "loadSchemaStore", "loadSystemStore", "loadGraphStore", "closeTx", - "analyzer", "serverInfoManager", "reloadRamtable", + "closeCurrentThreadTransaction", "analyzer", + "serverInfoManager", "reloadRamtable", "reloadRamtable", "access$19", "access$20", "access$21"); Reflection.registerFieldsToFilter( loadClass("org.apache.hugegraph.StandardHugeGraph$StandardHugeGraphParams"), @@ -298,8 +299,10 @@ private static void registerPrivateActions() { "autoCommit", "beforeRead", "afterWrite", "afterRead", "commitMutation2Backend", "checkOwnerThread", "doAction", "store", "reset"); - Reflection.registerFieldsToFilter(HugeFactory.class, "LOG", "NAME_REGEX", "graphs"); - Reflection.registerMethodsToFilter(HugeFactory.class, "lambda$0"); + Reflection.registerFieldsToFilter(HugeFactory.class, "LOG", "NAME_REGEX", "graphs", + "GRAPHS"); + Reflection.registerMethodsToFilter(HugeFactory.class, "lambda$0", + "closeCurrentThreadTransactions"); Reflection.registerFieldsToFilter(SchemaElement.class, "graph", "id", "name", "userdata", "status"); Reflection.registerFieldsToFilter(HugeVertex.class, "EMPTY_SET", "id", "label", "edges", @@ -400,11 +403,10 @@ private static void registerPrivateActions() { "oneNumericField", "hasSameProperties"); Reflection.registerFieldsToFilter(TaskManager.class, "LOG", "SCHEDULE_PERIOD", "THREADS", "MANAGER", "schedulers", "taskExecutor", "taskDbExecutor", - "serverInfoDbExecutor", "schedulerExecutor", "contexts", - "$assertionsDisabled"); + "contexts", "$assertionsDisabled"); Reflection.registerMethodsToFilter(TaskManager.class, "lambda$0", "resetContext", "closeTaskTx", "setContext", "instance", - "closeSchedulerTx", "notifyNewTask", + "notifyNewTask", "scheduleOrExecuteJob", "scheduleOrExecuteJobForGraph"); Reflection.registerFieldsToFilter(StandardTaskScheduler.class, "LOG", "graph", "serverManager", "taskExecutor", "taskDbExecutor", diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java index 510b838437..7bd04a2bea 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java @@ -35,20 +35,23 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Predicate; import java.util.function.Supplier; import javax.security.sasl.AuthenticationException; import org.apache.commons.configuration2.Configuration; +import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.HugeGraphSupplier; import org.apache.hugegraph.auth.HugeAuthenticator.RolePerm; import org.apache.hugegraph.auth.HugeAuthenticator.User; import org.apache.hugegraph.auth.SchemaDefine.AuthElement; import org.apache.hugegraph.backend.cache.Cache; import org.apache.hugegraph.backend.cache.CacheManager; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.BackendFeatures; import org.apache.hugegraph.backend.store.BackendStoreInfo; import org.apache.hugegraph.backend.store.BackendStoreProvider; @@ -61,16 +64,15 @@ import org.apache.hugegraph.iterator.MapperIterator; import org.apache.hugegraph.kvstore.KvStore; import org.apache.hugegraph.masterelection.GlobalMasterInfo; -import org.apache.hugegraph.masterelection.RoleElectionStateMachine; import org.apache.hugegraph.rpc.RpcServiceConfig4Client; import org.apache.hugegraph.rpc.RpcServiceConfig4Server; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaLabel; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeElement; import org.apache.hugegraph.structure.HugeFeatures; @@ -82,7 +84,7 @@ import org.apache.hugegraph.task.TaskStatus; import org.apache.hugegraph.traversal.optimize.HugeScriptTraversal; import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.Nameable; +import org.apache.hugegraph.type.Namifiable; import org.apache.hugegraph.type.define.GraphMode; import org.apache.hugegraph.type.define.GraphReadMode; import org.apache.hugegraph.util.E; @@ -744,7 +746,7 @@ public void proxy(HugeGraph graph) { } @Override - public boolean sameAs(HugeGraph graph) { + public boolean sameAs(HugeGraphSupplier graph) { if (graph instanceof HugeGraphAuthProxy) { graph = ((HugeGraphAuthProxy) graph).hugegraph; } @@ -763,6 +765,12 @@ public V option(TypedOption option) { return this.hugegraph.option(option); } + @Override + public Predicate searchPredicate(String text) { + this.verifyAnyPermission(); + return this.hugegraph.searchPredicate(text); + } + @Override public String name() { this.verifyAnyPermission(); @@ -804,6 +812,27 @@ public void mode(GraphMode mode) { this.hugegraph.mode(mode); } + @Override + public Map loadingMode() { + this.verifyPermission(HugePermission.WRITE, ResourceType.STATUS); + return this.hugegraph.loadingMode(); + } + + @Override + public Map loadingMode(String action, String instance, long epoch, + String token, String principal) { + this.verifyPermission(HugePermission.WRITE, ResourceType.STATUS); + Context context = getContext(); + E.checkState(context != null, "Missing authenticated loading owner context"); + return this.hugegraph.loadingMode(action, instance, epoch, token, context.user().username()); + } + + @Override + public Map recoverLoadingMode(String instance, long epoch, long revision) { + this.verifyAdminPermission(); + return this.hugegraph.recoverLoadingMode(instance, epoch, revision); + } + @Override public GraphReadMode readMode() { this.verifyStatusPermission(); @@ -871,12 +900,6 @@ public AuthManager authManager() { return this.authManager; } - @Override - public RoleElectionStateMachine roleElectionStateMachine() { - this.verifyAdminPermission(); - return this.hugegraph.roleElectionStateMachine(); - } - @Override public void switchAuthManager(AuthManager authManager) { this.verifyAdminPermission(); @@ -896,6 +919,12 @@ public void registerRpcServices(RpcServiceConfig4Server serverConfig, this.hugegraph.registerRpcServices(serverConfig, clientConfig); } + @Override + public void unregisterRpcServices() { + this.verifyAdminPermission(); + this.hugegraph.unregisterRpcServices(); + } + @Override public void initBackend() { this.verifyAdminPermission(); @@ -1060,7 +1089,7 @@ private void verifyPermission(HugePermission actionPerm, LOG.debug("Using requestGraphSpace: {}", graphSpace); } - Nameable elem = HugeResource.NameObject.ANY; + Namifiable elem = HugeResource.NameObject.ANY; return ResourceObject.of(graphSpace, graph, resType, elem); }); } @@ -1154,7 +1183,7 @@ private void verifyNamePermission(HugePermission actionPerm, ResourceType resType, String name) { verifyResPermission(actionPerm, true, () -> { String graph = this.hugegraph.name(); - Nameable elem = HugeResource.NameObject.of(name); + Namifiable elem = HugeResource.NameObject.of(name); // For global resources like USER_GROUP, use request graph space from HugeGraphAuthProxy // instead of the graph space where authManager is located @@ -1296,11 +1325,17 @@ public User user() { static class ContextTask implements Runnable { private final Runnable runner; + private final Runnable cleanup; private final Context context; public ContextTask(Runnable runner) { + this(runner, HugeFactory::closeCurrentThreadTransactions); + } + + ContextTask(Runnable runner, Runnable cleanup) { this.context = getContext(); this.runner = runner; + this.cleanup = cleanup; } @Override @@ -1309,7 +1344,13 @@ public void run() { try { this.runner.run(); } finally { - resetContext(); + try { + this.cleanup.run(); + } catch (Throwable e) { + LOG.error("Failed to close Gremlin worker transactions", e); + } finally { + resetContext(); + } } } } @@ -1520,7 +1561,7 @@ private HugeTask verifyTaskPermission(HugePermission actionPerm, Object r = verifyResPermission(actionPerm, throwIfNoPerm, () -> { String graph = HugeGraphAuthProxy.this.hugegraph.name(); String name = task.id().toString(); - Nameable elem = HugeResource.NameObject.of(name); + Namifiable elem = HugeResource.NameObject.of(name); return ResourceObject.of(graphSpace(), graph, ResourceType.TASK, elem); }, () -> { return hasTaskPermission(task); diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/StandardAuthenticator.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/StandardAuthenticator.java index aecc8af282..f64ead4fb2 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/StandardAuthenticator.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/StandardAuthenticator.java @@ -30,7 +30,6 @@ import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.config.ServerOptions; -import org.apache.hugegraph.masterelection.RoleElectionOptions; import org.apache.hugegraph.rpc.RpcClientProviderWithAuth; import org.apache.hugegraph.util.ConfigUtil; import org.apache.hugegraph.util.E; @@ -132,7 +131,6 @@ public void setup(HugeConfig config) { String raftGroupPeers = config.get(ServerOptions.RAFT_GROUP_PEERS); graphConfig.addProperty(ServerOptions.RAFT_GROUP_PEERS.name(), raftGroupPeers); - this.transferRoleWorkerConfig(graphConfig, config); this.graph = (HugeGraph) GraphFactory.open(graphConfig); @@ -144,21 +142,6 @@ public void setup(HugeConfig config) { } } - private void transferRoleWorkerConfig(HugeConfig graphConfig, HugeConfig config) { - graphConfig.addProperty(RoleElectionOptions.NODE_EXTERNAL_URL.name(), - config.get(ServerOptions.REST_SERVER_URL)); - graphConfig.addProperty(RoleElectionOptions.BASE_TIMEOUT_MILLISECOND.name(), - config.get(RoleElectionOptions.BASE_TIMEOUT_MILLISECOND)); - graphConfig.addProperty(RoleElectionOptions.EXCEEDS_FAIL_COUNT.name(), - config.get(RoleElectionOptions.EXCEEDS_FAIL_COUNT)); - graphConfig.addProperty(RoleElectionOptions.RANDOM_TIMEOUT_MILLISECOND.name(), - config.get(RoleElectionOptions.RANDOM_TIMEOUT_MILLISECOND)); - graphConfig.addProperty(RoleElectionOptions.HEARTBEAT_INTERVAL_SECOND.name(), - config.get(RoleElectionOptions.HEARTBEAT_INTERVAL_SECOND)); - graphConfig.addProperty(RoleElectionOptions.MASTER_DEAD_TIMES.name(), - config.get(RoleElectionOptions.MASTER_DEAD_TIMES)); - } - /** * Verify if a user is legal * diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java index d43b095b76..3441291e4d 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java @@ -42,15 +42,6 @@ public class ServerOptions extends OptionHolder { 1 ); - public static final ConfigOption ENABLE_SERVER_ROLE_ELECTION = - new ConfigOption<>( - "server.role_election", - "Whether to enable role election, if enabled, the server " + - "will elect a master node in the cluster.", - disallowEmpty(), - false - ); - public static final ConfigOption MAX_WORKER_THREADS = new ConfigOption<>( "restserver.max_worker_threads", @@ -442,34 +433,58 @@ public class ServerOptions extends OptionHolder { public static final ConfigOption ARTHAS_TELNET_PORT = new ConfigOption<>( - "arthas.telnetPort", + "arthas.telnet_port", "arthas provides telnet ports to the outside", disallowEmpty(), "8562" ); + public static final ConfigOption ARTHAS_TELNET_PORT_LEGACY = + new ConfigOption<>( + "arthas.telnetPort", + "Legacy alias: arthas provides telnet ports to the outside", + disallowEmpty(), + "8562" + ); + public static final ConfigOption ARTHAS_HTTP_PORT = new ConfigOption<>( - "arthas.httpPort", + "arthas.http_port", "arthas provides http ports to the outside", disallowEmpty(), "8561" ); + public static final ConfigOption ARTHAS_HTTP_PORT_LEGACY = + new ConfigOption<>( + "arthas.httpPort", + "Legacy alias: arthas provides http ports to the outside", + disallowEmpty(), + "8561" + ); + public static final ConfigOption ARTHAS_IP = new ConfigOption<>( "arthas.ip", - "arthas bound ip", + "Arthas bind address; use loopback unless external access is intended", disallowEmpty(), - "0.0.0.0" + "127.0.0.1" ); public static final ConfigOption ARTHAS_DISABLED_COMMANDS = + new ConfigOption<>( + "arthas.disabled_commands", + "Optional comma-separated Arthas disabled commands, empty by default", + null, + "" + ); + + public static final ConfigOption ARTHAS_DISABLED_COMMANDS_LEGACY = new ConfigOption<>( "arthas.disabledCommands", - "arthas disabled commands", - disallowEmpty(), - "jad" + "Legacy alias: optional comma-separated Arthas disabled commands", + null, + "" ); public static final ConfigOption ALLOW_TRACE = @@ -489,6 +504,11 @@ public class ServerOptions extends OptionHolder { "" ); + // TODO: accept the initial admin password from the environment (or document the properties + // contract): read through PropertiesConfiguration, the value is trimmed, backslash-unescaped + // and decoded as ISO-8859-1, so the stored password can differ from what the operator set. + // The Helm chart (helm/hugegraph) refuses padded, backslash or non-ASCII admin passwords in + // its schema and Server wrapper; relax that guard once the credential bypasses the file. public static final ConfigOption ADMIN_PA = new ConfigOption<>( "auth.admin_pa", diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java index a386a0ca67..2a8c608167 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/core/GraphManager.java @@ -30,12 +30,14 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; 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.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -48,7 +50,7 @@ import org.apache.commons.configuration2.MapConfiguration; import org.apache.commons.configuration2.PropertiesConfiguration; import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.StandardHugeGraph; @@ -59,7 +61,7 @@ import org.apache.hugegraph.auth.HugeGraphAuthProxy; import org.apache.hugegraph.auth.HugeUser; import org.apache.hugegraph.auth.StandardAuthenticator; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.cache.Cache; import org.apache.hugegraph.backend.cache.CacheManager; import org.apache.hugegraph.backend.store.AbstractBackendStoreProvider; @@ -82,9 +84,6 @@ import org.apache.hugegraph.kvstore.KvStore; import org.apache.hugegraph.kvstore.KvStoreImpl; import org.apache.hugegraph.masterelection.GlobalMasterInfo; -import org.apache.hugegraph.masterelection.RoleElectionOptions; -import org.apache.hugegraph.masterelection.RoleElectionStateMachine; -import org.apache.hugegraph.masterelection.StandardRoleListener; import org.apache.hugegraph.meta.MetaDriver; import org.apache.hugegraph.meta.MetaManager; import org.apache.hugegraph.meta.PdMetaDriver; @@ -117,6 +116,7 @@ import org.apache.hugegraph.space.register.dto.ServiceDTO; import org.apache.hugegraph.space.register.registerImpl.PdRegister; import org.apache.hugegraph.task.TaskManager; +import org.apache.hugegraph.task.TaskScheduler; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.traversal.optimize.HugeScriptTraversal; import org.apache.hugegraph.type.define.CollectionType; @@ -137,6 +137,8 @@ import org.apache.tinkerpop.gremlin.structure.util.GraphFactory; import org.slf4j.Logger; +import com.alipay.sofa.jraft.NodeManager; +import com.alipay.sofa.jraft.util.Endpoint; import com.alipay.sofa.rpc.config.ServerConfig; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; @@ -171,6 +173,11 @@ public final class GraphManager { private final Map graphs; private final Set localGraphs; private final Set removingGraphs; + // Guarded by this manager monitor; placement-event tokens are consumed independently. + private final Set rpcRemovingOwners; + private final Set pendingGraphAdds; + private final Set failedPeerRemovals; + private final Map clearedGraphs; private final Set creatingGraphs; private final HugeAuthenticator authenticator; private final AuthManager authManager; @@ -182,6 +189,8 @@ public final class GraphManager { private final RpcServer rpcServer; private final RpcClientProvider rpcClient; + private boolean rpcInitialized; + private volatile Endpoint raftRpcEndpoint; private final GlobalMasterInfo globalNodeRoleInfo; private final HugeConfig conf; private final EventHub eventHub; @@ -189,7 +198,6 @@ public final class GraphManager { private final Set serverUrlsToPd; private final Boolean serverDeployInK8s; private final HugeConfig config; - private RoleElectionStateMachine roleStateMachine; private K8sDriver.CA ca; private final boolean PDExist; @@ -218,6 +226,10 @@ public GraphManager(HugeConfig conf, EventHub hub) { // key is graphSpaceName + "-" + graphName this.graphs = new ConcurrentHashMap<>(); this.removingGraphs = ConcurrentHashMap.newKeySet(); + this.rpcRemovingOwners = Collections.newSetFromMap(new IdentityHashMap<>()); + this.pendingGraphAdds = new HashSet<>(); + this.failedPeerRemovals = Collections.newSetFromMap(new IdentityHashMap<>()); + this.clearedGraphs = new ConcurrentHashMap<>(); this.creatingGraphs = ConcurrentHashMap.newKeySet(); this.authenticator = HugeAuthenticator.loadAuthenticator(conf); this.serviceGraphSpace = conf.get(ServerOptions.SERVICE_GRAPH_SPACE); @@ -226,7 +238,6 @@ public GraphManager(HugeConfig conf, EventHub hub) { this.rpcClient = new RpcClientProvider(conf); this.pdPeers = conf.get(ServerOptions.PD_PEERS); - this.roleStateMachine = null; this.globalNodeRoleInfo = new GlobalMasterInfo(); this.eventHub = hub; @@ -696,7 +707,7 @@ public void init() { this.waitGraphsReady(); this.checkBackendVersionOrExit(this.conf); - this.serverStarted(this.conf); + this.serverStarted(); this.addMetrics(this.conf); } @@ -705,7 +716,7 @@ public void reload() { // Remove graphs from GraphManager for (String graph : this.graphs.keySet()) { String[] parts = graph.split(DELIMITER); - this.dropGraph(parts[0], parts[1], false); + this.dropGraph(parts[0], parts[1], false, false); } int count = 0; while (!this.graphs.isEmpty() && count++ < 10) { @@ -1112,24 +1123,22 @@ public void getSpaceStorage(String graphSpace) { } public void clearGraphSpace(String name) { - // Clear all roles - this.metaManager.clearGraphAuth(name); - - // Clear all schemaTemplate - this.metaManager.clearSchemaTemplate(name); - // Clear all graphs for (String key : this.graphs.keySet()) { - if (key.startsWith(name)) { + if (key.startsWith(name + DELIMITER)) { String[] parts = key.split(DELIMITER); this.dropGraph(parts[0], parts[1], true); } } + // Shared metadata remains available if a graph refuses deletion. + this.metaManager.clearGraphAuth(name); + this.metaManager.clearSchemaTemplate(name); + // Clear all services for (String key : this.services.keySet()) { - if (key.startsWith(name)) { - String[] parts = key.split(DELIMITER); + if (key.startsWith(name + DELIMITER)) { + String[] parts = key.split(DELIMITER, 2); this.dropService(parts[0], parts[1]); } } @@ -1456,18 +1465,35 @@ private HugeGraph createGraphLocal(HugeConfig config, String name) { // Init graph and start it graph.create(this.graphsDir, this.globalNodeRoleInfo); - // Let gremlin server and rest server add graph to context + // Complete RPC registration before Gremlin listeners expose the graph. + this.publishGraph(graph); this.notifyEvent(Events.GRAPH_CREATE, graph); } catch (Throwable e) { LOG.error("Failed to create graph '{}' due to: {}", name, e.getMessage(), e); if (graph != null) { - this.graphs.remove(graph.spaceGraphName(), graph); + synchronized (this) { + if (this.graphs.get(graph.spaceGraphName()) == graph) { + this.rpcRemovingOwners.add(graph); + } + } try { - this.dropGraphLocal(graph); - } finally { - // The create event may have partially registered the graph + this.unregisterRpcServices(graph); + HugeGraph candidate = graph; + HugeGraphAuthProxy.runAsAdmin(() -> this.dropGraphLocal(candidate)); + synchronized (this) { + this.graphs.remove(graph.spaceGraphName(), graph); + this.rpcRemovingOwners.remove(graph); + } this.notifyEventLenient(Events.GRAPH_DROP, graph); + } catch (Throwable cleanup) { + e.addSuppressed(cleanup); + synchronized (this) { + this.graphs.putIfAbsent(graph.spaceGraphName(), graph); + if (this.graphs.get(graph.spaceGraphName()) == graph) { + this.rpcRemovingOwners.add(graph); + } + } } } throw e; @@ -1477,16 +1503,8 @@ private HugeGraph createGraphLocal(HugeConfig config, String name) { } private void dropGraphLocal(HugeGraph graph) { - try { - // Clear data and config files - graph.drop(); - } finally { - /* - * Will fill graph instance into HugeFactory.graphs after - * GraphFactory.open() succeed, remove it when the graph drops - */ - HugeFactory.remove(graph); - } + graph.drop(); + HugeFactory.remove(graph); } public HugeGraph createGraph(String graphSpace, String name, String creator, @@ -1609,7 +1627,6 @@ public HugeGraph createGraph(String graphSpace, String name, String creator, graph.updateTime(timeStamp); String graphName = spaceGraphName(graphSpace, name); - this.graphs.put(graphName, graph); /* * Let gremlin server and rest server context add graph before the @@ -1617,18 +1634,10 @@ public HugeGraph createGraph(String graphSpace, String name, String creator, * graph behind in meta for the other servers to converge on */ try { + this.publishGraph(graph); this.notifyEvent(Events.GRAPH_CREATE, graph); } catch (Throwable e) { - this.notifyEventLenient(Events.GRAPH_DROP, graph); - this.graphs.remove(graphName, graph); - try { - graph.close(); - } catch (Exception e1) { - if (graph instanceof StandardHugeGraph) { - ((StandardHugeGraph) graph).clearSchedulerAndLock(); - } - } - HugeFactory.remove(graph); + this.rollbackGraph(graphName, graph, e); throw e; } @@ -1726,21 +1735,24 @@ public GlobalMasterInfo globalNodeRoleInfo() { } public void close() { - for (Graph graph : this.graphs.values()) { + for (Map.Entry entry : this.graphs.entrySet()) { + Graph graph = entry.getValue(); try { graph.close(); + synchronized (this) { + if (this.clearedGraphs.get(entry.getKey()) == graph) { + this.clearedGraphs.remove(entry.getKey()); + } + } } catch (Throwable e) { LOG.warn("Failed to close graph '{}'", graph, e); } } this.destroyRpcServer(); this.unlistenChanges(); - if (this.roleStateMachine != null) { - this.roleStateMachine.shutdown(); - } } - private void startRpcServer() { + private synchronized void startRpcServer() { if (!this.rpcServer.enabled()) { LOG.info("RpcServer is not enabled, skip starting rpc service"); return; @@ -1754,20 +1766,50 @@ private void startRpcServer() { this.authenticator.authManager()); } - // Start graph rpc service if RPC_REMOTE_URL enabled - if (this.rpcClient.enabled()) { - RpcConsumerConfig clientConfig = this.rpcClient.config(); - - for (Graph graph : this.graphs.values()) { - HugeGraph hugegraph = (HugeGraph) graph; - hugegraph.registerRpcServices(serverConfig, clientConfig); + try { + // Start graph rpc service if RPC_REMOTE_URL enabled + if (this.rpcClient.enabled()) { + RpcConsumerConfig clientConfig = this.rpcClient.config(); + + for (Graph graph : this.graphs.values()) { + HugeGraph hugegraph = (HugeGraph) graph; + HugeGraphAuthProxy.runAsAdmin(() -> { + // Auth wrappers and the owner must use the same existing lifecycle monitor. + synchronized (hugegraph.hugegraph()) { + if (hugegraph.closed() || this.rpcRemovingOwners.contains(hugegraph)) { + // Retained provider configs must be removed before exportAll. + this.unregisterRpcServices(hugegraph); + return; + } + hugegraph.registerRpcServices(serverConfig, clientConfig); + } + }); + } } - } - try { this.rpcServer.exportAll(); + this.rpcInitialized = true; } catch (Throwable e) { - this.rpcServer.destroy(); + this.rpcInitialized = false; + for (Graph graph : this.graphs.values()) { + if (this.rpcRemovingOwners.contains(graph)) { + continue; + } + try { + this.unregisterRpcServices((HugeGraph) graph); + } catch (Throwable cleanup) { + if (e != cleanup) { + e.addSuppressed(cleanup); + } + } + } + try { + this.rpcServer.destroy(); + } catch (Throwable cleanup) { + if (e != cleanup) { + e.addSuppressed(cleanup); + } + } throw e; } } @@ -1782,8 +1824,10 @@ private com.alipay.remoting.rpc.RpcServer remotingRpcServer() { if (!serverConfig.getServer().isStarted()) { serverConfig.getServer().start(); } - return Whitebox.getInternalState(serverConfig.getServer(), - "remotingServer"); + com.alipay.remoting.rpc.RpcServer server = Whitebox.getInternalState(serverConfig.getServer(), + "remotingServer"); + this.raftRpcEndpoint = new Endpoint(server.ip(), server.port()); + return server; } private void destroyRpcServer() { @@ -1791,6 +1835,11 @@ private void destroyRpcServer() { this.rpcClient.destroy(); } finally { this.rpcServer.destroy(); + Endpoint endpoint = this.raftRpcEndpoint; + if (endpoint != null) { + NodeManager.getInstance().removeAddress(endpoint); + this.raftRpcEndpoint = null; + } } } @@ -1837,17 +1886,24 @@ private void loadGraph(String name, String graphConfPath) { this.transferPdPeersConfig(config); - this.transferRoleWorkerConfig(config); - Graph graph = GraphFactory.open(config); - this.graphs.put(defaultSpaceGraphName(name), graph); + try { + this.publishGraph(defaultSpaceGraphName(name), (HugeGraph) graph); + } catch (RuntimeException | Error failure) { + this.rollbackGraph(defaultSpaceGraphName(name), (HugeGraph) graph, failure); + throw failure; + } HugeConfig graphConfig = (HugeConfig) graph.configuration(); - assert graphConfPath.equals(Objects.requireNonNull(graphConfig.file()).getPath()); + // A graph Gremlin Server opened first from this path is cached without a file + assert graphConfig.file() == null || + graphConfPath.equals(graphConfig.file().getPath()); LOG.info("Graph '{}' was successfully configured via '{}'", name, graphConfPath); + this.bindLocalGraph(name, (HugeGraph) graph); + if (this.requireAuthentication() && !(graph instanceof HugeGraphAuthProxy)) { LOG.warn("You may need to support access control for '{}' with {}", @@ -1855,6 +1911,65 @@ private void loadGraph(String name, String graphConfPath) { } } + private void rollbackGraph(String graphName, HugeGraph graph, Throwable failure) { + synchronized (this) { + if (this.graphs.get(graphName) == graph) { + this.rpcRemovingOwners.add(graph); + } + } + try { + HugeGraphAuthProxy.runAsAdmin(() -> { + try { + graph.close(); + } catch (Exception cleanup) { + throw new HugeException("Failed to close unpublished graph '%s'", cleanup, graphName); + } + }); + this.notifyEventLenient(Events.GRAPH_DROP, graph); + synchronized (this) { + HugeFactory.remove(graph); + this.graphs.remove(graphName, graph); + this.rpcRemovingOwners.remove(graph); + } + } catch (Throwable cleanup) { + if (cleanup != failure) { + failure.addSuppressed(cleanup); + } + // Pending owners must keep their graph/factory identity for cleanup retry. + synchronized (this) { + this.graphs.putIfAbsent(graphName, graph); + if (this.graphs.get(graphName) == graph) { + this.rpcRemovingOwners.add(graph); + } + } + } + } + + /* + * Gremlin Server opens the graphs in conf/graphs once, while it is being + * prepared, and drops any graph whose open fails there (for example when + * PD is briefly unreachable). Offer each locally loaded graph to it again, + * as the create paths do, so such a graph still gets its Gremlin binding. + * Rebinding the graph Gremlin already holds is harmless: both opens return + * the same cached instance. + */ + private void bindLocalGraph(String name, HugeGraph graph) { + String graphName = defaultSpaceGraphName(name); + if (!graphName.equals(graph.spaceGraphName())) { + LOG.warn("Graph '{}' is named '{}' by its config, so it can't be " + + "rebound in Gremlin Server if Gremlin failed to open it", + graphName, graph.spaceGraphName()); + return; + } + try { + this.notifyEvent(Events.GRAPH_CREATE, graph); + } catch (Throwable e) { + LOG.error("Graph '{}' is served by REST but could not be bound " + + "in Gremlin Server, restart the Server to retry", + graphName, e); + } + } + private void transferPdPeersConfig(HugeConfig config) { if (config.containsKey(CoreOptions.PD_PEERS.name())) { return; @@ -1868,21 +1983,6 @@ private void transferPdPeersConfig(HugeConfig config) { } } - private void transferRoleWorkerConfig(HugeConfig config) { - config.setProperty(RoleElectionOptions.NODE_EXTERNAL_URL.name(), - this.conf.get(ServerOptions.REST_SERVER_URL)); - config.setProperty(RoleElectionOptions.BASE_TIMEOUT_MILLISECOND.name(), - this.conf.get(RoleElectionOptions.BASE_TIMEOUT_MILLISECOND)); - config.setProperty(RoleElectionOptions.EXCEEDS_FAIL_COUNT.name(), - this.conf.get(RoleElectionOptions.EXCEEDS_FAIL_COUNT)); - config.setProperty(RoleElectionOptions.RANDOM_TIMEOUT_MILLISECOND.name(), - this.conf.get(RoleElectionOptions.RANDOM_TIMEOUT_MILLISECOND)); - config.setProperty(RoleElectionOptions.HEARTBEAT_INTERVAL_SECOND.name(), - this.conf.get(RoleElectionOptions.HEARTBEAT_INTERVAL_SECOND)); - config.setProperty(RoleElectionOptions.MASTER_DEAD_TIMES.name(), - this.conf.get(RoleElectionOptions.MASTER_DEAD_TIMES)); - } - private void waitGraphsReady() { if (!this.rpcServer.enabled()) { LOG.info("RpcServer is not enabled, skip wait graphs ready"); @@ -1928,16 +2028,6 @@ private void checkBackendVersionOrExit(HugeConfig config) { } private void initNodeRole() { - boolean enableRoleElection = config.get( - ServerOptions.ENABLE_SERVER_ROLE_ELECTION); - if (enableRoleElection) { - LOG.warn("The server.role_election option is deprecated and no " + - "longer supported (removed with server_info persistence). " + - "The configured server.role is still used for local node " + - "role initialization. Set server.role_election=false to " + - "suppress this warning."); - } - String role = config.get(ServerOptions.SERVER_ROLE); E.checkArgument(StringUtils.isNotEmpty(role), "The server role can't be null or empty"); @@ -1946,16 +2036,12 @@ private void initNodeRole() { this.globalNodeRoleInfo.initNodeRole(nodeRole); } - private void serverStarted(HugeConfig conf) { + private void serverStarted() { for (String graph : this.graphs()) { HugeGraph hugegraph = this.graph(graph); assert hugegraph != null; hugegraph.serverStarted(this.globalNodeRoleInfo); } - if (!this.globalNodeRoleInfo.nodeRole().computer() && this.supportRoleElection() && - config.get(ServerOptions.ENABLE_SERVER_ROLE_ELECTION)) { - LOG.info("Skip role state machine init (deprecated with server_info)"); - } } public SchemaTemplate schemaTemplate(String graphSpace, @@ -1964,30 +2050,6 @@ public SchemaTemplate schemaTemplate(String graphSpace, return this.metaManager.schemaTemplate(graphSpace, schemaTemplate); } - private void initRoleStateMachine() { - E.checkArgument(this.roleStateMachine == null, - "Repeated initialization of role state worker"); - this.globalNodeRoleInfo.supportElection(true); - this.roleStateMachine = this.authenticator().graph().roleElectionStateMachine(); - StandardRoleListener listener = new StandardRoleListener(TaskManager.instance(), - this.globalNodeRoleInfo); - this.roleStateMachine.start(listener); - } - - private boolean supportRoleElection() { - try { - if (!(this.authenticator() instanceof StandardAuthenticator)) { - LOG.info("{} authenticator does not support role election currently", - this.authenticator().getClass().getSimpleName()); - return false; - } - return true; - } catch (IllegalStateException e) { - LOG.info("{}, does not support role election currently", e.getMessage()); - return false; - } - } - private void addMetrics(HugeConfig config) { final MetricManager metric = MetricManager.INSTANCE; // Force to add a server reporter @@ -2030,18 +2092,65 @@ private void listenChanges() { LOG.debug("RestServer accepts event '{}'", event.name()); event.checkArgs(HugeGraph.class); HugeGraph graph = (HugeGraph) event.args()[0]; - this.graphs.put(graph.spaceGraphName(), graph); + this.publishGraph(graph); return null; }); this.eventHub.listen(Events.GRAPH_DROP, event -> { LOG.debug("RestServer accepts event '{}'", event.name()); event.checkArgs(HugeGraph.class); HugeGraph graph = (HugeGraph) event.args()[0]; - this.graphs.remove(graph.spaceGraphName(), graph); + this.unpublishGraph(graph); return null; }); } + private void publishGraph(HugeGraph graph) { + this.publishGraph(graph.spaceGraphName(), graph); + } + + private synchronized void publishGraph(String name, HugeGraph graph) { + if (this.graphs.get(name) == graph) { + return; + } + this.registerRpcServices(graph); + this.graphs.put(name, graph); + } + + private void registerRpcServices(HugeGraph graph) { + if (this.rpcInitialized && this.rpcServer.enabled() && this.rpcClient.enabled()) { + try { + HugeGraphAuthProxy.runAsAdmin(() -> graph.registerRpcServices(this.rpcServer.config(), + this.rpcClient.config())); + this.rpcServer.exportAll(); + } catch (RuntimeException | Error failure) { + try { + this.unregisterRpcServices(graph); + } catch (RuntimeException | Error cleanup) { + failure.addSuppressed(cleanup); + } + throw failure; + } + } + } + + private void unregisterRpcServices(HugeGraph graph) { + HugeGraphAuthProxy.runAsAdmin(graph::unregisterRpcServices); + } + + private synchronized boolean unpublishGraph(HugeGraph graph) { + String name = graph.spaceGraphName(); + if (this.graphs.get(name) != graph) { + return false; + } + // A failed cleanup keeps the owner published so that drop can retry. + this.unregisterRpcServices(graph); + boolean removed = this.graphs.remove(name, graph); + if (removed) { + this.rpcRemovingOwners.remove(graph); + } + return removed; + } + private void unlistenChanges() { this.eventHub.unlisten(Events.GRAPH_CREATE); this.eventHub.unlisten(Events.GRAPH_DROP); @@ -2142,12 +2251,11 @@ private HugeGraph createGraph(String graphSpace, HugeConfig config, } catch (BackendException e) { try { graph.close(); + HugeFactory.remove(graph); } catch (Exception e1) { - if (graph instanceof StandardHugeGraph) { - ((StandardHugeGraph) graph).clearSchedulerAndLock(); - } + e.addSuppressed(e1); + this.graphs.putIfAbsent(graph.spaceGraphName(), graph); } - HugeFactory.remove(graph); throw e; } } @@ -2239,7 +2347,7 @@ public Set getServiceUrls(String graphSpace, String service, PdRegisterType registerType) { Map configs = new HashMap<>(); if (StringUtils.isNotEmpty(graphSpace)) { - configs.put(PdRegisterLabel.REGISTER_TYPE.name(), graphSpace); + configs.put(PdRegisterLabel.GRAPHSPACE.name(), graphSpace); } if (StringUtils.isNotEmpty(service)) { configs.put(PdRegisterLabel.SERVICE_NAME.name(), service); @@ -2280,7 +2388,6 @@ public HugeGraph graph(String graphSpace, String name) { creator, config, false); graph1.createTime(createTime); graph1.updateTime(updateTime); - this.graphs.put(key, graph1); return graph1; } throw new NotFoundException(String.format("Graph '%s' does not exist", name)); @@ -2301,21 +2408,44 @@ public void dropGraphLocal(String name) { "The graph '%s' is the only one, not allowed to delete", name); - this.dropGraphLocal(graph); + try { + this.dropGraphLocal(graph); + } catch (RuntimeException | Error failure) { + synchronized (this) { + if (graph.closed()) { + this.rpcRemovingOwners.add(graph); + } + } + // Local clear failure leaves the still-open owner's existing RPC services untouched. + throw failure; + } // Let gremlin server and rest server context remove graph this.notifyEventLenient(Events.GRAPH_DROP, graph); } public void dropGraph(String graphSpace, String name, boolean clear) { + this.dropGraph(graphSpace, name, clear, true); + } + + private void dropGraph(String graphSpace, String name, boolean clear, boolean replayPendingAdd) { if (!isPDEnabled()) { dropGraphLocal(name); return; } + HugeGraph graph = this.graph(graphSpace, name); + E.checkArgumentNotNull(graph, "The graph '%s' doesn't exist", name); + this.dropGraph(graphSpace, name, clear, graph, replayPendingAdd); + } + + private void dropGraph(String graphSpace, String name, boolean clear, HugeGraph g, boolean replayPendingAdd) { + synchronized (this) { + if (g == null || this.graphs.get(spaceGraphName(graphSpace, name)) != g) { + return; + } + } boolean grpcThread = Thread.currentThread().getName().contains("grpc"); - HugeGraph g = this.graph(graphSpace, name); - E.checkArgumentNotNull(g, "The graph '%s' doesn't exist", name); if (this.localGraphs.contains(name)) { throw new HugeException("Can't delete graph '%s' loaded from " + "local config. Please delete config file " + @@ -2324,62 +2454,120 @@ public void dropGraph(String graphSpace, String name, boolean clear) { } String graphName = spaceGraphName(graphSpace, name); - if (clear) { - this.removingGraphs.add(graphName); + if (clear && g.closed() && this.clearedGraphs.get(graphName) != g) { + // A non-clearing close cannot certify that destructive deletion completed. + throw new HugeException("Graph '%s' is closed without a successful backend clear; " + + "retain its owner and complete backend cleanup before deletion", graphName); + } + // The same owner may await close after clear succeeded, even while a foreign tx remains open. + if (clear && !g.closed() && this.clearedGraphs.get(graphName) != g) { + // The auth proxy checks ADMIN before the local deletion fence. + TaskScheduler scheduler = g.taskScheduler(); + if (!scheduler.close()) { + try { + scheduler.waitUntilAllTasksCompleted(g.configuration().get(CoreOptions.TASK_WAIT_TIMEOUT)); + } catch (TimeoutException e) { + throw new HugeException("Tasks are still active in graph '%s'; task admission remains closed. " + + "Retry deletion after owners exit, or restart to resume task admission", + e, graphName); + } + E.checkState(scheduler.close(), + "Tasks are still active in graph '%s'; task admission remains closed. " + + "Retry deletion after owners exit, or restart to resume task admission", + graphName); + } + // Cleanup failure must precede metadata removal and destructive clear. + synchronized (this) { + this.rpcRemovingOwners.add(g); + this.unregisterRpcServices(g); + this.removingGraphs.add(graphName); + } + boolean metadataRemoved = false; try { this.metaManager.removeGraphConfig(graphSpace, name); + metadataRemoved = true; this.metaManager.notifyGraphRemove(graphSpace, name); } catch (Exception e) { + // No backend clear has started; retain RPC for the same open local owner. + synchronized (this) { + if (!metadataRemoved) { + // No local REMOVE was sent; a later peer REMOVE must still be handled. + this.removingGraphs.remove(graphName); + } + if (this.graphs.get(graphName) == g && !g.closed()) { + try { + this.registerRpcServices(g); + this.rpcRemovingOwners.remove(g); + } catch (RuntimeException | Error restoration) { + if (e != restoration) { + e.addSuppressed(restoration); + } + } + } + } throw new HugeException( - "Failed to remove graph config of '%s'", name, e); - } - - /** - * close task scheduler before clear data, - * because taskinfo stored in backend in - * {@link org.apache.hugegraph.task.DistributedTaskScheduler} - */ - try { - g.taskScheduler().close(); - } catch (Throwable t) { - LOG.warn(String.format( - "Error when close TaskScheduler of %s", - graphName), - t); + "Failed to remove graph config of '%s'", e, name); } g.clearBackend(); - try { - g.close(); - } catch (Exception e) { - LOG.warn("Failed to close graph", e); + synchronized (this) { + if (this.graphs.get(graphName) == g) { + this.clearedGraphs.put(graphName, g); + } } } - GraphSpace gs = this.graphSpace(graphSpace); - if (!grpcThread) { - gs.recycleGraph(); - LOG.info("The graph_number_used successfully decreased to {} " + - "of graph space: {} for graph: {}", - gs.graphNumberUsed(), gs.name(), name); - this.metaManager.updateGraphSpaceConfig(graphSpace, gs); + try { + g.close(); + } catch (Exception failure) { + // Logical closed does not certify native release; keep this owner until close succeeds. + throw new HugeException("Failed to close graph '%s', please retry later", failure, graphName); + } + // Serialize with publication, but leave graph close outside the manager gate. + boolean replayAdd = false; + synchronized (this) { + if (this.graphs.get(graphName) == g) { + this.unregisterRpcServices(g); + GraphSpace gs = this.graphSpace(graphSpace); + if (!grpcThread) { + int used = gs.graphNumberUsed(); + gs.recycleGraph(); + LOG.info("The graph_number_used successfully decreased to {} " + + "of graph space: {} for graph: {}", + gs.graphNumberUsed(), gs.name(), name); + try { + this.metaManager.updateGraphSpaceConfig(graphSpace, gs); + } catch (RuntimeException | Error failure) { + gs.graphNumberUsed(used); + throw failure; + } + } + // Remove the factory binding before permitting same-name creation. + HugeFactory.remove(g); + this.graphs.remove(graphName, g); + this.rpcRemovingOwners.remove(g); + this.failedPeerRemovals.remove(g); + if (replayPendingAdd) { + replayAdd = this.pendingGraphAdds.contains(graphName); + } else { + // Reload loads all current metadata after every old owner has been removed. + this.pendingGraphAdds.remove(graphName); + } + if (this.clearedGraphs.get(graphName) == g) { + this.clearedGraphs.remove(graphName); + } + } } // Let gremlin server and rest server context remove graph LOG.info("Notify remove graph {} by GRAPH_DROP event", name); - Graph graph = this.graphs.remove(graphName); - if (graph != null) { - try { - graph.close(); - } catch (Exception e) { - LOG.warn("Failed to close graph", e); - } + this.eventHub.notify(Events.GRAPH_DROP, g); + if (replayAdd) { try { - // Delete alias graph in HugeFactory - HugeFactory.remove((HugeGraph) graph); - } catch (Exception e) { - LOG.warn("Failed to remove hugeFactory graph", e); + this.loadAddedGraph(graphName); + } catch (RuntimeException | Error failure) { + // The old DELETE succeeded; a replacement load failure must not invite deleting it again. + LOG.error("Failed to load received graph add after cleanup of {}", graphName, failure); } } - this.eventHub.notify(Events.GRAPH_DROP, g); } private void checkOptions(String graphSpace, HugeConfig config) { @@ -2685,41 +2873,66 @@ private void graphAddHandler(T response) { continue; } LOG.info("Accept graph add signal from etcd for {}", graphName); - if (this.graphs.containsKey(graphName) || - this.creatingGraphs.contains(graphName)) { - this.creatingGraphs.remove(graphName); - continue; + HugeGraph retryOwner = null; + synchronized (this) { + HugeGraph owner = (HugeGraph) this.graphs.get(graphName); + boolean retryPeerCleanup = owner != null && this.failedPeerRemovals.contains(owner); + if (this.creatingGraphs.remove(graphName) && !retryPeerCleanup) { + // The acknowledgement of local creation is not a replacement ADD. + continue; + } + if (owner != null) { + // Independent key watches can deliver a replacement ADD before the old REMOVE. + this.pendingGraphAdds.add(graphName); + if (!retryPeerCleanup) { + continue; + } + retryOwner = owner; + } } - - LOG.info("Not exist in cache, Starting construct graph {}", - graphName); - Map config = - this.metaManager.getGraphConfig(parts[0], parts[1]); - if (config == null) { - LOG.error("The graph config not exist: {}", graphName); + if (retryOwner != null) { + try { + // Retry the exact peer owner; a concurrent replacement must never be closed here. + this.dropGraph(parts[0], parts[1], false, retryOwner, true); + } catch (HugeException failure) { + LOG.error("Failed to retry peer cleanup for graph '{}'", graphName, failure); + } continue; } - Object objc = config.get("creator"); - String creator = null == objc ? - GraphSpace.DEFAULT_CREATOR_NAME : - String.valueOf(objc); - - // Create graph without init try { - HugeGraph graph; - // TODO: add alias graph - graph = this.createGraph(parts[0], parts[1], creator, config, false); - LOG.info("Add graph space:{} graph:{}", parts[0], parts[1]); - graph.started(true); - if (graph.tx().isOpen()) { - graph.tx().close(); - } + this.loadAddedGraph(graphName); } catch (HugeException e) { if (!this.startIgnoreSingleGraphError) { throw e; } - LOG.error(String.format( - "Failed to create graph '%s'", graphName), e); + LOG.error(String.format("Failed to create graph '%s'", graphName), e); + } + } + } + + private void loadAddedGraph(String graphName) { + String[] parts = graphName.split(DELIMITER); + LOG.info("Not exist in cache, Starting construct graph {}", graphName); + Map config = this.metaManager.getGraphConfig(parts[0], parts[1]); + HugeGraph graph = null; + if (config != null) { + Object objc = config.get("creator"); + String creator = null == objc ? GraphSpace.DEFAULT_CREATOR_NAME : String.valueOf(objc); + // Create graph without init; reread metadata instead of keeping an old event's configuration. + graph = this.createGraph(parts[0], parts[1], creator, config, false); + LOG.info("Add graph space:{} graph:{}", parts[0], parts[1]); + graph.started(true); + if (graph.tx().isOpen()) { + graph.tx().close(); + } + } else { + LOG.error("The graph config not exist: {}", graphName); + } + synchronized (this) { + if (this.graphs.get(graphName) == graph && + (graph == null || (!graph.closed() && !this.rpcRemovingOwners.contains(graph)))) { + // Do not consume a notification newly retained by another cleanup owner. + this.pendingGraphAdds.remove(graphName); } } } @@ -2740,9 +2953,15 @@ private void graphRemoveHandler(T response) { LOG.error("The graph name format is incorrect: {}", graphName); continue; } + HugeGraph owner = (HugeGraph) this.graphs.get(graphName); try { - this.dropGraph(parts[0], parts[1], false); + this.dropGraph(parts[0], parts[1], false, owner, true); } catch (HugeException e) { + synchronized (this) { + if (this.graphs.get(graphName) == owner) { + this.failedPeerRemovals.add(owner); + } + } LOG.error(String.format( "Failed to drop graph '%s'", graphName), e); } diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/kvstore/KvStoreImpl.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/kvstore/KvStoreImpl.java index 01f217d63b..c6c7f6eae2 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/kvstore/KvStoreImpl.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/kvstore/KvStoreImpl.java @@ -25,9 +25,9 @@ import java.util.Map; import java.util.Optional; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.store.Shard; +import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.variables.HugeVariables; import org.apache.tinkerpop.gremlin.structure.Vertex; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/metrics/MetricsUtil.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/metrics/MetricsUtil.java index 3721cb1e18..da36ae5e1a 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/metrics/MetricsUtil.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/metrics/MetricsUtil.java @@ -17,6 +17,9 @@ package org.apache.hugegraph.metrics; +import java.util.Map; +import java.util.regex.Pattern; + import org.apache.tinkerpop.gremlin.server.util.MetricManager; import com.codahale.metrics.Counter; @@ -59,10 +62,13 @@ public class MetricsUtil { public static final String STDDEV_ATTR = "{name=\"stddev\",} "; public static final String P50_ATTR = "{name=\"p50\",} "; - public static final String LEFT_NAME_STR = "{name="; - public static final String RIGHT_NAME_STR = ",} "; + public static final String LEFT_NAME_STR = "{name=\""; + public static final String RIGHT_NAME_STR = "\",} "; public static final String PROM_HELP_NAME = "hugegraph_info"; + private static final Pattern INVALID_PROMETHEUS_NAME_CHAR = + Pattern.compile("[^a-zA-Z0-9_:]"); + public static Gauge registerGauge(Class clazz, String name, Gauge gauge) { return REGISTRY.register(MetricRegistry.name(clazz, name), gauge); @@ -92,19 +98,29 @@ public static Timer registerTimer(Class clazz, String name) { return REGISTRY.timer(MetricRegistry.name(clazz, name)); } - public static String replaceDotDashInKey(String orgKey) { - return orgKey.replace(".", "_").replace("-", "_").replace("/", "_").replace("$", "_"); - } - - public static String replaceSlashInKey(String orgKey) { - return orgKey.replace("/", "_"); + /** + * Convert a metric key into a valid Prometheus metric name, which must + * match [a-zA-Z_:][a-zA-Z0-9_:]*. Every other character becomes '_', + * and a leading digit gets a '_' prefix. Only call this when writing + * Prometheus output; registry keys stay unchanged. + */ + public static String sanitizePrometheusMetricName(String originalName) { + String sanitized = INVALID_PROMETHEUS_NAME_CHAR.matcher(originalName) + .replaceAll("_"); + if (sanitized.isEmpty()) { + return "_"; + } + if (Character.isDigit(sanitized.charAt(0))) { + return "_" + sanitized; + } + return sanitized; } public static void writePrometheusFormat(StringBuilder promeMetrics, MetricRegistry registry) { // gauges registry.getGauges().forEach((key, gauge) -> { if (gauge != null) { - String helpName = replaceDotDashInKey(key); + String helpName = sanitizePrometheusMetricName(key); promeMetrics.append(STR_HELP) .append(helpName).append(END_LSTR); promeMetrics.append(STR_TYPE) @@ -117,7 +133,7 @@ public static void writePrometheusFormat(StringBuilder promeMetrics, MetricRegis // histograms registry.getHistograms().forEach((key, histogram) -> { if (histogram != null) { - String helpName = replaceDotDashInKey(key); + String helpName = sanitizePrometheusMetricName(key); promeMetrics.append(STR_HELP) .append(helpName).append(END_LSTR); promeMetrics.append(STR_TYPE) @@ -134,7 +150,7 @@ public static void writePrometheusFormat(StringBuilder promeMetrics, MetricRegis // meters registry.getMeters().forEach((key, metric) -> { if (metric != null) { - String helpName = replaceDotDashInKey(key); + String helpName = sanitizePrometheusMetricName(key); promeMetrics.append(STR_HELP) .append(helpName).append(END_LSTR); promeMetrics.append(STR_TYPE) @@ -160,7 +176,7 @@ public static void writePrometheusFormat(StringBuilder promeMetrics, MetricRegis // timer registry.getTimers().forEach((key, timer) -> { if (timer != null) { - String helpName = replaceDotDashInKey(key); + String helpName = sanitizePrometheusMetricName(key); promeMetrics.append(STR_HELP) .append(helpName).append(END_LSTR); promeMetrics.append(STR_TYPE) @@ -184,6 +200,25 @@ public static void writePrometheusFormat(StringBuilder promeMetrics, MetricRegis }); } + /** + * Write the per-path request statistics, one gauge family per path with + * a quoted "name" label for each statistic. + */ + public static void writeStatisticsPrometheusFormat( + StringBuilder promeMetrics, Map> metricMap) { + metricMap.forEach((methodKey, itemMetricMap) -> { + String metricName = sanitizePrometheusMetricName(methodKey); + promeMetrics.append(STR_HELP) + .append(metricName).append(END_LSTR); + promeMetrics.append(STR_TYPE) + .append(metricName).append(SPACE_STR + GAUGE_TYPE + END_LSTR); + itemMetricMap.forEach((labelName, value) -> { + promeMetrics.append(metricName).append(LEFT_NAME_STR).append(labelName) + .append(RIGHT_NAME_STR).append(value).append(END_LSTR); + }); + }); + } + public static String exportSnapshot(final String helpName, final Snapshot snapshot) { if (snapshot == null) { return ""; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherGremlinPredicates.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherGremlinPredicates.java new file mode 100644 index 0000000000..53498b720f --- /dev/null +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherGremlinPredicates.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.hugegraph.opencypher; + +import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate; +import org.opencypher.gremlin.translation.traversal.TraversalGremlinPredicates; +import org.opencypher.gremlin.traversal.CustomPredicate; + +/** + * Adapt the extension factories compiled against the pre-3.8 P constructor. + * The original predicates retain Cypher type checks and whole-string regex matching. + */ +public final class CypherGremlinPredicates extends TraversalGremlinPredicates { + + @Override + public P regexMatch(Object value) { + return cypherRegex(value); + } + + @Override + public P isNode() { + return cypherIsNode(); + } + + @Override + public P isRelationship() { + return cypherIsRelationship(); + } + + @Override + public P isString() { + return cypherIsString(); + } + + public static P cypherRegex(Object value) { + return predicate(CustomPredicate.cypherRegex, value); + } + + public static P cypherIsNode() { + return predicate(CustomPredicate.cypherIsNode, null); + } + + public static P cypherIsRelationship() { + return predicate(CustomPredicate.cypherIsRelationship, null); + } + + public static P cypherIsString() { + return predicate(CustomPredicate.cypherIsString, null); + } + + private static P predicate(CustomPredicate predicate, Object value) { + PBiPredicate adapter = predicate::test; + return new P<>(adapter, value); + } +} diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java index dfad9b9594..8125b5566e 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java @@ -19,24 +19,22 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Optional.empty; -import static org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode.SERVER_ERROR; +import static org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode.SERVER_ERROR; import static org.opencypher.gremlin.translation.StatementOption.EXPLAIN; import static org.slf4j.LoggerFactory.getLogger; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; -import org.apache.tinkerpop.gremlin.driver.Tokens; -import org.apache.tinkerpop.gremlin.driver.message.RequestMessage; -import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage; -import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode; import org.apache.tinkerpop.gremlin.process.traversal.P; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal; @@ -49,24 +47,32 @@ import org.apache.tinkerpop.gremlin.server.op.AbstractEvalOpProcessor; import org.apache.tinkerpop.gremlin.server.op.OpProcessorException; import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.util.Tokens; import org.apache.tinkerpop.gremlin.util.function.ThrowingConsumer; +import org.apache.tinkerpop.gremlin.util.message.RequestMessage; +import org.apache.tinkerpop.gremlin.util.message.ResponseMessage; +import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode; import org.opencypher.gremlin.translation.CypherAst; import org.opencypher.gremlin.translation.groovy.GroovyPredicate; import org.opencypher.gremlin.translation.ir.TranslationWriter; import org.opencypher.gremlin.translation.ir.model.GremlinStep; import org.opencypher.gremlin.translation.translator.Translator; +import org.opencypher.gremlin.translation.traversal.TraversalGremlinBindings; +import org.opencypher.gremlin.translation.traversal.TraversalGremlinSteps; import org.opencypher.gremlin.traversal.ParameterNormalizer; import org.opencypher.gremlin.traversal.ProcedureContext; import org.opencypher.gremlin.traversal.ReturnNormalizer; +import org.opencypher.v9_0.expressions.Parameter; import org.slf4j.Logger; import io.netty.channel.ChannelHandlerContext; +import scala.Product; import scala.collection.Seq; /** * Description of the modifications: *

- * 1) Changed the method signature to adopt the gremlin-server 3.5.1. + * 1) Changed the method signature to adopt the gremlin-server Context API. *

  * public Optional> selectOther(RequestMessage requestMessage)
  * -->
@@ -100,12 +106,74 @@ public class CypherOpProcessor extends AbstractEvalOpProcessor {
     private static final String DEFAULT_TRANSLATOR_DEFINITION =
             "gremlin+cfog_server_extensions+inline_parameters";
 
+    // translation 1.0.4 uses this string as null and cannot preserve it as bound data.
+    private static final String RESERVED_NULL_VALUE = "  cypher.null";
+
     private static final Logger logger = getLogger(CypherOpProcessor.class);
 
     public CypherOpProcessor() {
         super(true);
     }
 
+    @Override
+    protected Optional> validateEvalMessage(RequestMessage message)
+            throws OpProcessorException {
+        Object query = message.getArgs().get(Tokens.ARGS_GREMLIN);
+        String error = null;
+        if (!(query instanceof String) || ((String) query).isBlank()) {
+            error = "The Cypher query must be a nonblank string";
+        } else if (message.getArgs().containsKey(Tokens.ARGS_BINDINGS)) {
+            Object supplied = message.getArgs().get(Tokens.ARGS_BINDINGS);
+            if (!(supplied instanceof Map)) {
+                error = "Cypher bindings must be a map";
+            } else {
+                Map bindings = (Map) supplied;
+                if (bindings.size() > this.maxParameters) {
+                    error = "Cypher bindings exceed the maximum parameter count " + this.maxParameters;
+                } else {
+                    // Cypher parameters do not enter the Gremlin script namespace.
+                    for (Object key : bindings.keySet()) {
+                        if (!(key instanceof String)) {
+                            error = "Cypher binding keys must be nonnull strings";
+                            break;
+                        }
+                    }
+                    if (error == null && containsReservedNullValue(bindings)) {
+                        error = "Cypher bindings contain a string reserved for null by the translator";
+                    }
+                }
+            }
+        }
+        if (error != null) {
+            throw new OpProcessorException(error, ResponseMessage.build(message)
+                    .code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS)
+                    .statusMessage(error).create());
+        }
+        return empty();
+    }
+
+    private static boolean containsReservedNullValue(Map bindings) {
+        Deque> pending = new ArrayDeque<>();
+        pending.push(bindings.values().iterator());
+        while (!pending.isEmpty()) {
+            Iterator values = pending.peek();
+            if (!values.hasNext()) {
+                pending.pop();
+                continue;
+            }
+            Object value = values.next();
+            if (RESERVED_NULL_VALUE.equals(value)) {
+                return true;
+            }
+            if (value instanceof Map) {
+                pending.push(((Map) value).values().iterator());
+            } else if (value instanceof Collection) {
+                pending.push(((Collection) value).iterator());
+            }
+        }
+        return false;
+    }
+
     @Override
     public String getName() {
         return "cypher";
@@ -131,6 +199,10 @@ private void evalCypher(Context context) throws OpProcessorException {
         Map parameters = ParameterNormalizer.normalize(getParameters(args));
         ProcedureContext procedureContext = ProcedureContext.global();
         CypherAst ast = CypherAst.parse(cypher, parameters, procedureContext.getSignatures());
+        validateParameters(ast, parameters);
+        // The translator has no temporal functions; fold the supported
+        // constructors into bound values before it rejects them.
+        parameters = CypherTemporalFunctions.fold(ast, parameters);
 
         String translatorDefinition = getTranslatorDefinition(context);
 
@@ -139,13 +211,17 @@ private void evalCypher(Context context) throws OpProcessorException {
                                                                       .build(translatorDefinition);
 
         Translator traversalTranslator = Translator.builder()
-                                                                      .traversal(g)
+                                                                      .custom(new TraversalGremlinSteps(g),
+                                                                              new CypherGremlinPredicates(),
+                                                                              new TraversalGremlinBindings())
                                                                       .build(translatorDefinition);
 
         Seq ir = ast.translate(strTranslator.flavor(),
                                             strTranslator.features(), procedureContext);
 
-        String gremlin = TranslationWriter.write(ir, strTranslator, parameters);
+        Map explainParameters =
+                CypherTemporalFunctions.explainParameters(parameters);
+        String gremlin = TranslationWriter.write(ir, strTranslator, explainParameters);
         logger.trace("Gremlin: {}", gremlin);
 
         if (ast.getOptions().contains(EXPLAIN)) {
@@ -157,24 +233,36 @@ private void evalCypher(Context context) throws OpProcessorException {
                                                                  parameters);
         ReturnNormalizer returnNormalizer = ReturnNormalizer.create(ast.getReturnTypes());
         Iterator normalizedTraversal = returnNormalizer.normalize(traversal);
-        inTransaction(gts, () -> handleIterator(context, normalizedTraversal));
+        handleTraversal(context, normalizedTraversal, traversal);
     }
 
-    private void inTransaction(GraphTraversalSource gts, Runnable runnable) {
-        Graph graph = gts.getGraph();
-        boolean supportsTransactions = graph.features().graph().supportsTransactions();
-        if (!supportsTransactions) {
-            runnable.run();
-            return;
-        }
-
-        try {
-            graph.tx().open();
-            runnable.run();
-            graph.tx().commit();
-        } catch (Exception e) {
-            if (graph.tx().isOpen()) {
-                graph.tx().rollback();
+    private static void validateParameters(CypherAst ast, Map parameters) {
+        // Inspect parsed parameter nodes: dollar signs in literals/comments are not bindings.
+        // The upstream translator otherwise silently turns an absent binding into null.
+        Deque pending = new ArrayDeque<>();
+        pending.push(ast.statement());
+        while (!pending.isEmpty()) {
+            Object node = pending.pop();
+            if (node instanceof Parameter) {
+                String name = ((Parameter) node).name();
+                if (!parameters.containsKey(name)) {
+                    throw new IllegalArgumentException("Missing Cypher parameter: " + name);
+                }
+                continue;
+            }
+            scala.collection.Iterator children;
+            if (node instanceof scala.collection.Iterable) {
+                children = ((scala.collection.Iterable) node).iterator();
+            } else if (node instanceof Product) {
+                children = ((Product) node).productIterator();
+            } else {
+                continue;
+            }
+            while (children.hasNext()) {
+                Object child = children.next();
+                if (child != null) {
+                    pending.push(child);
+                }
             }
         }
     }
@@ -217,22 +305,45 @@ private OpProcessorException opProcessorException(RequestMessage msg, String err
                                                                      .create());
     }
 
-    @Override
-    protected void handleIterator(Context context, Iterator traversal) {
+    private void handleTraversal(Context context, Iterator traversal,
+                                 GraphTraversal source) {
         RequestMessage msg = context.getRequestMessage();
         final long timeout = msg.getArgs().containsKey(Tokens.ARGS_EVAL_TIMEOUT)
                              ? ((Number) msg.getArgs().get(Tokens.ARGS_EVAL_TIMEOUT)).longValue()
                              : context.getSettings().evaluationTimeout;
 
         FutureTask evalFuture = new FutureTask<>(() -> {
-            try {
+            try (GraphTraversal ignored = source) {
+                // The parent commits before sending the terminal success response. Both
+                // iteration and failure rollback must run on this transaction-owning thread.
                 super.handleIterator(context, traversal);
+            } catch (Error error) {
+                try {
+                    attemptRollback(msg, context.getGraphManager(),
+                                    context.getSettings().strictTransactionManagement);
+                } catch (Exception rollbackFailure) {
+                    error.addSuppressed(rollbackFailure);
+                }
+                // FutureTask retains the error, so send a terminal response here as well.
+                // This also covers errors raised after a timeout has cancelled the task.
+                logger.error("Fatal error during traversal iteration", error);
+                context.writeAndFlush(ResponseMessage.build(msg)
+                                                     .code(SERVER_ERROR)
+                                                     .statusMessage(error.getMessage())
+                                                     .statusAttributeException(error)
+                                                     .create());
+                throw error;
             } catch (Exception ex) {
+                try {
+                    attemptRollback(msg, context.getGraphManager(),
+                                    context.getSettings().strictTransactionManagement);
+                } catch (Exception rollbackFailure) {
+                    ex.addSuppressed(rollbackFailure);
+                }
                 String errorMessage = getErrorMessage(msg, ex);
 
                 logger.error("Error during traversal iteration", ex);
-                ChannelHandlerContext ctx = context.getChannelHandlerContext();
-                ctx.writeAndFlush(ResponseMessage.build(msg)
+                context.writeAndFlush(ResponseMessage.build(msg)
                                                  .code(SERVER_ERROR)
                                                  .statusMessage(errorMessage)
                                                  .statusAttributeException(ex)
@@ -242,11 +353,10 @@ protected void handleIterator(Context context, Iterator traversal) {
         }
         );
 
-        final Future executionFuture = context.getGremlinExecutor()
-                                                 .getExecutorService().submit(evalFuture);
+        context.getGremlinExecutor().getExecutorService().execute(evalFuture);
         if (timeout > 0) {
             context.getScheduledExecutorService().schedule(
-                    () -> executionFuture.cancel(true)
+                    () -> evalFuture.cancel(true)
                     , timeout, TimeUnit.MILLISECONDS);
         }
 
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherPlugin.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherPlugin.java
index 6d7899dd19..fef7c73bdc 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherPlugin.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherPlugin.java
@@ -40,15 +40,14 @@
 import org.apache.tinkerpop.gremlin.jsr223.GremlinPlugin;
 import org.apache.tinkerpop.gremlin.jsr223.ImportCustomizer;
 import org.opencypher.gremlin.traversal.CustomFunctions;
-import org.opencypher.gremlin.traversal.CustomPredicate;
 
 public class CypherPlugin implements GremlinPlugin {
 
     private static final ImportCustomizer IMPORTS =
             DefaultImportCustomizer.build()
-                                   .addClassImports(CustomPredicate.class)
+                                   .addClassImports(CypherGremlinPredicates.class)
                                    .addMethodImports(
-                                           getDeclaredPublicMethods(CustomPredicate.class))
+                                           getDeclaredPublicMethods(CypherGremlinPredicates.class))
                                    .addClassImports(CustomFunctions.class)
                                    .addMethodImports(
                                            getDeclaredPublicMethods(CustomFunctions.class))
@@ -57,7 +56,8 @@ public class CypherPlugin implements GremlinPlugin {
     private static List getDeclaredPublicMethods(Class klass) {
         Method[] declaredMethods = klass.getDeclaredMethods();
         return Stream.of(declaredMethods)
-                     .filter(method -> Modifier.isPublic(method.getModifiers()))
+                     .filter(method -> Modifier.isPublic(method.getModifiers()) &&
+                                       Modifier.isStatic(method.getModifiers()))
                      .collect(Collectors.toList());
     }
 
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherTemporalFunctions.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherTemporalFunctions.java
new file mode 100644
index 0000000000..60c2132872
--- /dev/null
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherTemporalFunctions.java
@@ -0,0 +1,435 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.opencypher;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.opencypher.gremlin.translation.CypherAst;
+import org.opencypher.v9_0.expressions.Expression;
+import org.opencypher.v9_0.expressions.FunctionInvocation;
+import org.opencypher.v9_0.expressions.Parameter;
+import org.opencypher.v9_0.expressions.StringLiteral;
+import org.opencypher.v9_0.util.InputPosition;
+import org.opencypher.v9_0.util.symbols.AnyType;
+import org.opencypher.v9_0.util.symbols.CypherType;
+import org.slf4j.Logger;
+
+import scala.Option;
+import scala.Product;
+import scala.Some;
+import scala.Tuple2;
+import scala.collection.JavaConverters;
+import scala.collection.Seq;
+
+/**
+ * Rewrites the Cypher temporal constructor functions that the cypher-for-gremlin
+ * translator does not implement into parameters bound to native date values.
+ * 

+ * The translator's function list is fixed inside its own dependency, so the + * supported constructors (date, datetime, localdatetime) are folded into the + * parsed statement right before translation: each call with a foldable argument + * becomes a synthetic bound parameter and the date value is added to the + * bindings. Calls that cannot be folded are left in place and keep failing + * with the translator's original error, as do all other temporal functions. + */ +public final class CypherTemporalFunctions { + + private static final Logger logger = getLogger(CypherTemporalFunctions.class); + + private static final String DATE = "date"; + private static final String DATETIME = "datetime"; + private static final String LOCALDATETIME = "localdatetime"; + + // Two leading spaces keep the prefix away from ordinary parameter names; + // backtick-escaped Cypher parameters can still carry it, so generated + // names skip every one the request already occupies. + private static final String SYNTHETIC_PREFIX = " cypher.fold."; + + private CypherTemporalFunctions() { + } + + /** + * Folds the supported temporal constructors in the parsed statement and + * returns the effective request parameters for translation: the same + * instance when nothing was folded, a copy carrying the synthetic + * bindings otherwise. The parsed statement is only replaced when the + * rewrite completed without errors, so a fold failure leaves the query + * exactly as it was parsed. The inferred types of the rebuilt expressions + * are carried over with the statement. + */ + public static Map fold(CypherAst ast, Map parameters) { + TemporalFold fold = new TemporalFold(parameters); + try { + Field statementField = CypherAst.class.getDeclaredField("statement"); + Field parametersField = CypherAst.class.getDeclaredField("parameters"); + Field typesField = CypherAst.class.getDeclaredField("expressionTypes"); + statementField.setAccessible(true); + parametersField.setAccessible(true); + typesField.setAccessible(true); + + Object folded = fold.copy(ast.statement()); + if (fold.bindings.isEmpty()) { + return parameters; + } + Object merged = mergeParameters(parametersField.get(ast), fold.bindings); + Object types = carryTypes(typesField.get(ast), fold.copies); + statementField.set(ast, folded); + parametersField.set(ast, merged); + typesField.set(ast, types); + } catch (Exception | LinkageError e) { + logger.warn("Failed to fold temporal functions, keeping the parsed statement", e); + return parameters; + } + Map effective = new LinkedHashMap<>(parameters); + effective.putAll(fold.bindings); + return effective; + } + + @SuppressWarnings("unchecked") + private static Object mergeParameters(Object parameters, Map bindings) { + scala.collection.immutable.Map merged = + (scala.collection.immutable.Map) parameters; + for (Map.Entry binding : bindings.entrySet()) { + merged = merged.$plus(new Tuple2<>(binding.getKey(), binding.getValue())); + } + return merged; + } + + /** + * Returns the expression type map extended with the types of the rebuilt + * expressions, so a statement the fold touched still finds the type of + * every expression, for example the valueMap() decision for a RETURN + * item that holds a node or relationship. + */ + @SuppressWarnings("unchecked") + private static Object carryTypes(Object types, Map copies) { + scala.collection.immutable.Map expressionTypes = + (scala.collection.immutable.Map) types; + scala.collection.immutable.Map carried = expressionTypes; + for (Map.Entry pair : copies.entrySet()) { + if (pair.getKey() == pair.getValue()) { + continue; + } + Option type = expressionTypes.get((Expression) pair.getKey()); + if (type.isDefined()) { + carried = carried.$plus(new Tuple2<>((Expression) pair.getValue(), type.get())); + } + } + return carried; + } + + /** + * Returns a copy of the given parameters in which folded temporal values + * are replaced by a printable constructor form, so the string translation + * stays executable Groovy instead of rendering a date through toString. + */ + public static Map explainParameters(Map parameters) { + boolean folded = false; + for (String name : parameters.keySet()) { + if (name.startsWith(SYNTHETIC_PREFIX)) { + folded = true; + break; + } + } + if (!folded) { + return parameters; + } + Map explainable = new LinkedHashMap<>(parameters); + for (Map.Entry entry : explainable.entrySet()) { + if (entry.getKey().startsWith(SYNTHETIC_PREFIX) && + entry.getValue() instanceof Date) { + entry.setValue(new GroovyDate((Date) entry.getValue())); + } + } + return explainable; + } + + /** + * Renders a folded date as an explicit constructor so that the EXPLAIN + * translation of the string writer stays valid Groovy. + */ + private static final class GroovyDate { + + private final Date date; + + private GroovyDate(Date date) { + this.date = date; + } + + @Override + public String toString() { + return "new Date(" + this.date.getTime() + "L)"; + } + } + + private static final class TemporalFold { + + private final Map parameters; + private final Map bindings = new LinkedHashMap<>(); + // Identity pairs of every visited expression and its rebuilt copy; + // the inferred types travel to the copies after the fold. + private final Map copies = new IdentityHashMap<>(); + // One clock read per statement: the bare constructors of a query + // derive from a single instant. + private final Instant now = Instant.now(); + private int counter = 0; + + private TemporalFold(Map parameters) { + this.parameters = parameters; + } + + /** + * Returns a copy of the node with temporal constructors folded in, or + * the same instance when the subtree does not change. Subtrees that + * cannot be rebuilt are shared with the original statement, so the + * copy stays minimal. + */ + private Object copy(Object node) throws Exception { + Object copied = copyNode(node); + if (node instanceof Expression) { + this.copies.put(node, copied); + } + return copied; + } + + private Object copyNode(Object node) throws Exception { + if (node == null || node instanceof String || node instanceof Number || + node instanceof Boolean || node instanceof Character) { + return node; + } + if (node instanceof FunctionInvocation) { + Object folded = foldCall((FunctionInvocation) node); + if (folded != null) { + return folded; + } + } + if (node instanceof Option) { + Option option = (Option) node; + if (!option.isEmpty()) { + Object value = option.get(); + Object copied = copy(value); + if (copied != value) { + return new Some<>(copied); + } + } + return node; + } + if (node instanceof scala.collection.IndexedSeq) { + return copyIndexedSeq((scala.collection.IndexedSeq) node); + } + if (node instanceof Seq) { + return copySeq((Seq) node); + } + if (node instanceof scala.collection.immutable.Set) { + return copySet((scala.collection.immutable.Set) node); + } + if (node instanceof Tuple2) { + Tuple2 tuple = (Tuple2) node; + Object first = copy(tuple._1()); + Object second = copy(tuple._2()); + return first == tuple._1() && second == tuple._2() ? node : new Tuple2<>(first, second); + } + if (node instanceof Product) { + return rebuild((Product) node); + } + return node; + } + + private Object foldCall(FunctionInvocation call) { + // Function names resolve case-insensitively, like the parser and + // the translator do, so match the lower-case spelling. + String name = call.name().toLowerCase(Locale.ROOT); + if (!DATE.equals(name) && !DATETIME.equals(name) && !LOCALDATETIME.equals(name)) { + return null; + } + if (call.distinct() || !call.namespace().parts().isEmpty()) { + return null; + } + Seq arguments = call.args(); + Date value; + if (arguments.isEmpty()) { + value = currentValue(name); + } else if (arguments.size() == 1) { + String literal = literalOf(arguments.head()); + if (literal == null) { + return null; + } + value = parse(name, literal); + if (value == null) { + return null; + } + } else { + return null; + } + String binding = nextBindingName(name); + this.bindings.put(binding, value); + return new Parameter(binding, AnyType.instance(), call.position()); + } + + private String nextBindingName(String name) { + String binding = SYNTHETIC_PREFIX + name + '.' + this.counter++; + while (this.parameters.containsKey(binding) || + this.bindings.containsKey(binding)) { + binding = SYNTHETIC_PREFIX + name + '.' + this.counter++; + } + return binding; + } + + private String literalOf(Object argument) { + if (argument instanceof StringLiteral) { + return ((StringLiteral) argument).value(); + } + if (argument instanceof Parameter) { + Object bound = this.parameters.get(((Parameter) argument).name()); + if (bound instanceof String) { + return (String) bound; + } + } + return null; + } + + private Date currentValue(String name) { + if (DATE.equals(name)) { + return Date.from(LocalDate.ofInstant(this.now, ZoneId.systemDefault()) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()); + } + return Date.from(this.now); + } + + private static Date parse(String name, String literal) { + // ISO parsing reads the fractional part as a fraction of a + // second, like Cypher and the server's own date parsing do. + try { + if (DATE.equals(name)) { + return Date.from(LocalDate.parse(literal) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()); + } + return Date.from(LocalDateTime.parse(literal) + .atZone(ZoneId.systemDefault()) + .toInstant()); + } catch (DateTimeParseException e) { + return null; + } + } + + private Object copyIndexedSeq(scala.collection.IndexedSeq seq) throws Exception { + List copies = new ArrayList<>(seq.size()); + boolean changed = false; + scala.collection.Iterator iterator = seq.iterator(); + while (iterator.hasNext()) { + Object element = iterator.next(); + Object copied = copy(element); + changed |= copied != element; + copies.add(copied); + } + if (!changed) { + return seq; + } + return scala.collection.immutable.IndexedSeq$.MODULE$.apply(JavaConverters.asScalaBuffer(copies)); + } + + private Object copySeq(Seq seq) throws Exception { + List copies = new ArrayList<>(seq.size()); + boolean changed = false; + scala.collection.Iterator iterator = seq.iterator(); + while (iterator.hasNext()) { + Object element = iterator.next(); + Object copied = copy(element); + changed |= copied != element; + copies.add(copied); + } + if (!changed) { + return seq; + } + return JavaConverters.asScalaBuffer(copies).toList(); + } + + private Object copySet(scala.collection.immutable.Set set) throws Exception { + List copies = new ArrayList<>(set.size()); + boolean changed = false; + scala.collection.Iterator iterator = set.iterator(); + while (iterator.hasNext()) { + Object element = iterator.next(); + Object copied = copy(element); + changed |= copied != element; + copies.add(copied); + } + if (!changed) { + return set; + } + return JavaConverters.asScalaBuffer(copies).toSet(); + } + + private Object rebuild(Product product) throws Exception { + int arity = product.productArity(); + Object[] elements = new Object[arity]; + boolean changed = false; + for (int i = 0; i < arity; i++) { + Object element = product.productElement(i); + Object copied = copy(element); + changed |= copied != element; + elements[i] = copied; + } + if (!changed) { + return product; + } + Constructor constructor = constructorOf(product.getClass(), arity); + constructor.setAccessible(true); + Object[] arguments = new Object[constructor.getParameterCount()]; + System.arraycopy(elements, 0, arguments, 0, arity); + if (constructor.getParameterCount() == arity + 1) { + arguments[arity] = product.getClass().getMethod("position").invoke(product); + } + return constructor.newInstance(arguments); + } + + private static Constructor constructorOf(Class clazz, int arity) { + for (Constructor constructor : clazz.getDeclaredConstructors()) { + if (constructor.getParameterCount() == arity + 1) { + Class[] types = constructor.getParameterTypes(); + if (InputPosition.class.isAssignableFrom(types[arity])) { + return constructor; + } + } + } + for (Constructor constructor : clazz.getDeclaredConstructors()) { + if (constructor.getParameterCount() == arity) { + return constructor; + } + } + throw new IllegalStateException("No matching constructor in " + clazz.getName()); + } + } +} diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/serializer/JsonSerializer.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/serializer/JsonSerializer.java index 8d02dd6c8c..e7b5952d2d 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/serializer/JsonSerializer.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/serializer/JsonSerializer.java @@ -24,17 +24,17 @@ import java.util.List; import java.util.Map; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.api.API; import org.apache.hugegraph.auth.SchemaDefine.AuthElement; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.page.PageInfo; import org.apache.hugegraph.iterator.Metadatable; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.space.GraphSpace; import org.apache.hugegraph.space.SchemaTemplate; import org.apache.hugegraph.space.Service; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/serializer/Serializer.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/serializer/Serializer.java index 1585725b09..ddc2102f60 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/serializer/Serializer.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/serializer/Serializer.java @@ -23,12 +23,12 @@ import java.util.Map; import org.apache.hugegraph.auth.SchemaDefine.AuthElement; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.space.GraphSpace; import org.apache.hugegraph.space.SchemaTemplate; import org.apache.hugegraph.space.Service; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/server/ApplicationConfig.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/server/ApplicationConfig.java index 1930a1146c..5cc3e0eb33 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/server/ApplicationConfig.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/server/ApplicationConfig.java @@ -17,13 +17,14 @@ package org.apache.hugegraph.server; -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.api.filter.RedirectFilterDynamicFeature; +import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.define.WorkLoad; import org.apache.hugegraph.event.EventHub; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.Log; import org.apache.hugegraph.version.CoreVersion; import org.apache.tinkerpop.gremlin.server.util.MetricManager; import org.glassfish.hk2.api.Factory; @@ -36,6 +37,7 @@ import org.glassfish.jersey.server.monitoring.ApplicationEventListener; import org.glassfish.jersey.server.monitoring.RequestEvent; import org.glassfish.jersey.server.monitoring.RequestEventListener; +import org.slf4j.Logger; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.jersey3.InstrumentedResourceMethodApplicationListener; @@ -65,6 +67,9 @@ ) @ApplicationPath("/") public class ApplicationConfig extends ResourceConfig { + + private static final Logger LOG = Log.logger(ApplicationConfig.class); + @Context private ServletConfig servletConfig; @@ -77,8 +82,6 @@ public ApplicationConfig(HugeConfig conf, EventHub hub) { // Register to use the jsr250 annotations @RolesAllowed register(RolesAllowedDynamicFeature.class); - register(RedirectFilterDynamicFeature.class); - // Register HugeConfig to context register(new ConfFactory(conf)); @@ -198,7 +201,15 @@ public void onEvent(ApplicationEvent event) { @Override public RequestEventListener onRequest(RequestEvent event) { - return null; + return request -> { + if (request.getType() == RequestEvent.Type.FINISHED) { + try { + HugeFactory.closeCurrentThreadTransactions(); + } catch (RuntimeException e) { + LOG.error("Failed to release REST request transactions", e); + } + } + }; } }); } diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/server/HttpGremlinRequestHandler.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/server/HttpGremlinRequestHandler.java new file mode 100644 index 0000000000..6eae82e21d --- /dev/null +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/server/HttpGremlinRequestHandler.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.hugegraph.server; + +import java.util.Map; + +import org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutor; +import org.apache.tinkerpop.gremlin.server.GraphManager; +import org.apache.tinkerpop.gremlin.server.Settings; +import org.apache.tinkerpop.gremlin.server.handler.HttpGremlinEndpointHandler; +import org.apache.tinkerpop.gremlin.util.MessageSerializer; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; + +@ChannelHandler.Sharable +public class HttpGremlinRequestHandler extends HttpGremlinEndpointHandler { + + public HttpGremlinRequestHandler(Map> serializers, + GremlinExecutor executor, + GraphManager graphManager, + Settings settings) { + super(serializers, executor, graphManager, settings); + } + + @Override + public void channelRead(ChannelHandlerContext context, Object message) { + if (!(message instanceof FullHttpRequest)) { + super.channelRead(context, message); + return; + } + + FullHttpRequest request = (FullHttpRequest) message; + try { + /* + * TinkerPop 3.8.1 releases the request before resolving aliases, + * then releases it again on an unknown alias. That second release + * writes a spurious 500 after the intended 400 on the same stream. + * Own the original reference here and let the upstream handler + * parse a view whose release is a no-op. It parses synchronously; + * asynchronous evaluation holds only the parsed RequestMessage. + */ + FullHttpRequest view = request.replace( + Unpooled.unreleasableBuffer(request.content())); + super.channelRead(context, view); + } finally { + request.release(); + } + } +} diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/server/HugeGraphWsAndHttpChannelizer.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/server/HugeGraphWsAndHttpChannelizer.java new file mode 100644 index 0000000000..94f358425f --- /dev/null +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/server/HugeGraphWsAndHttpChannelizer.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.hugegraph.server; + +import org.apache.tinkerpop.gremlin.server.AbstractChannelizer; +import org.apache.tinkerpop.gremlin.server.handler.WsAndHttpChannelizerHandler; +import org.apache.tinkerpop.gremlin.server.util.ServerGremlinExecutor; + +import io.netty.channel.ChannelPipeline; + +public class HugeGraphWsAndHttpChannelizer extends AbstractChannelizer { + + private WsAndHttpChannelizerHandler handler; + + @Override + public void init(ServerGremlinExecutor executor) { + super.init(executor); + this.handler = new WsAndHttpChannelizerHandler(); + this.handler.init(executor, new HttpGremlinRequestHandler( + this.serializers, this.gremlinExecutor, + this.graphManager, this.settings)); + } + + @Override + public void configure(ChannelPipeline pipeline) { + this.handler.configure(pipeline); + pipeline.addAfter(PIPELINE_HTTP_REQUEST_DECODER, + "WsAndHttpChannelizerHandler", this.handler); + } + + @Override + public boolean supportsIdleMonitor() { + return true; + } + + @Override + public Object createIdleDetectionMessage() { + return this.handler.getWsChannelizer().createIdleDetectionMessage(); + } +} diff --git a/hugegraph-server/hugegraph-core/pom.xml b/hugegraph-server/hugegraph-core/pom.xml index 4e03478a53..88d89f6898 100644 --- a/hugegraph-server/hugegraph-core/pom.xml +++ b/hugegraph-server/hugegraph-core/pom.xml @@ -32,13 +32,6 @@ 0.7.4 5.12.1 1.8.1 - 1.10.0 - 2.6.2 - portable-1.8.3 - 5.1.6 - 8.11.2 - 1.0.2 - 2012_u6 1.21 11.1.0 8.5.9 @@ -49,17 +42,14 @@ com.fasterxml.jackson.core jackson-databind - 2.13.2 com.fasterxml.jackson.core jackson-core - 2.13.2 com.fasterxml.jackson.core jackson-annotations - 2.15.2 io.fabric8 @@ -209,58 +199,6 @@ ${lz4.version} - - - org.ansj - ansj_seg - ${ansj-seg.version} - - - com.hankcs - hanlp - ${hanlp.version} - - - org.apache.lucene - lucene-analyzers-smartcn - ${lucene.version} - - - org.apache.lucene - lucene-core - ${lucene.version} - - - com.huaban - jieba-analysis - ${jieba-analysis.version} - - - org.lionsoul - jcseg-core - ${jcseg.version} - - - com.chenlb.mmseg4j - mmseg4j-core - ${mmseg4j-core.version} - - - com.janeluo - ikanalyzer - ${ikanalyzer.version} - - - org.apache.lucene - lucene-core - - - org.apache.lucene - lucene-analyzers-common - - - - it.unimi.dsi @@ -298,7 +236,7 @@ org.apache.hugegraph - hg-store-common + hugegraph-struct ${revision} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeException.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeException.java deleted file mode 100644 index fec7bc1449..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeException.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph; - -import java.io.InterruptedIOException; - -import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalInterruptedException; - -public class HugeException extends RuntimeException { - - private static final long serialVersionUID = -8711375282196157058L; - - public HugeException(String message) { - super(message); - } - - public HugeException(String message, Throwable cause) { - super(message, cause); - } - - public HugeException(String message, Object... args) { - super(String.format(message, args)); - } - - public HugeException(String message, Throwable cause, Object... args) { - super(String.format(message, args), cause); - } - - public Throwable rootCause() { - return rootCause(this); - } - - public static Throwable rootCause(Throwable e) { - Throwable cause = e; - while (cause.getCause() != null) { - cause = cause.getCause(); - } - return cause; - } - - public static boolean isInterrupted(Throwable e) { - Throwable rootCause = HugeException.rootCause(e); - return rootCause instanceof InterruptedException || - rootCause instanceof TraversalInterruptedException || - rootCause instanceof InterruptedIOException; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeFactory.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeFactory.java index f258cb5c7b..0e500ed0bc 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeFactory.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeFactory.java @@ -17,9 +17,14 @@ package org.apache.hugegraph; +import org.apache.hugegraph.exception.HugeException; + import java.io.File; import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; @@ -44,7 +49,7 @@ public class HugeFactory { public static final String SYS_GRAPH = Graph.Hidden.hide("sys_graph"); private static final Logger LOG = Log.logger(HugeFactory.class); private static final String NAME_REGEX = "^[A-Za-z][A-Za-z0-9_]{0,47}$"; - private static final Map GRAPHS = new HashMap<>(); + private static final Map GRAPHS = new HashMap<>(); private static final AtomicBoolean SHUT_DOWN = new AtomicBoolean(false); private static final Thread SHUT_DOWN_HOOK = new Thread(() -> { LOG.info("HugeGraph is shutting down"); @@ -83,7 +88,7 @@ public static synchronized HugeGraph open(HugeConfig config) { String graphSpace = config.get(CoreOptions.GRAPH_SPACE); name = name.toLowerCase(); String spaceGraphName = graphSpace + "-" + name; - HugeGraph graph = GRAPHS.get(spaceGraphName); + StandardHugeGraph graph = GRAPHS.get(spaceGraphName); if (graph == null || graph.closed()) { graph = new StandardHugeGraph(config); GRAPHS.put(spaceGraphName, graph); @@ -104,11 +109,38 @@ public static HugeGraph open(URL url) { return open(getRemoteConfig(url)); } - public static void remove(HugeGraph graph) { + public static synchronized void remove(HugeGraph graph) { String spaceGraphName = graph.spaceGraphName(); GRAPHS.remove(spaceGraphName); } + public static void closeCurrentThreadTransactions() { + List graphs; + synchronized (HugeFactory.class) { + graphs = new ArrayList<>(GRAPHS.values()); + } + closeCurrentThreadTransactions(graphs); + } + + static void closeCurrentThreadTransactions( + Collection graphs) { + Throwable failure = null; + for (StandardHugeGraph graph : graphs) { + try { + graph.closeCurrentThreadTransaction(); + } catch (Throwable e) { + if (failure == null) { + failure = e; + } else if (failure != e) { + failure.addSuppressed(e); + } + } + } + if (failure != null) { + throw new HugeException("Failed to close current thread transactions", failure); + } + } + public static void checkGraphName(String name, String configFile) { E.checkArgument(SYS_GRAPH.equals(name) || name.matches(NAME_REGEX), "Invalid graph name '%s' in %s, " + @@ -155,10 +187,10 @@ public static void shutdown(long timeout, boolean ignoreException) { } try { + TaskManager.instance().shutdown(timeout); if (!EventHub.destroy(timeout)) { throw new TimeoutException(timeout + "s"); } - TaskManager.instance().shutdown(timeout); OltpTraverser.destroy(); } catch (Throwable e) { LOG.error("Error while shutdown", e); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java index 8fcd76d39b..745b42c238 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java @@ -23,12 +23,14 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import org.apache.commons.lang3.tuple.Pair; import org.apache.hugegraph.auth.AuthManager; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.BackendFeatures; import org.apache.hugegraph.backend.store.BackendStoreInfo; import org.apache.hugegraph.backend.store.BackendStoreProvider; @@ -37,16 +39,15 @@ import org.apache.hugegraph.config.TypedOption; import org.apache.hugegraph.kvstore.KvStore; import org.apache.hugegraph.masterelection.GlobalMasterInfo; -import org.apache.hugegraph.masterelection.RoleElectionStateMachine; import org.apache.hugegraph.rpc.RpcServiceConfig4Client; import org.apache.hugegraph.rpc.RpcServiceConfig4Server; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaLabel; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeFeatures; import org.apache.hugegraph.task.TaskScheduler; import org.apache.hugegraph.traversal.optimize.HugeConnectiveLabelStepStrategy; @@ -71,7 +72,7 @@ /** * Graph interface for Gremlin operations */ -public interface HugeGraph extends Graph { +public interface HugeGraph extends Graph, HugeGraphSupplier { HugeGraph hugegraph(); @@ -216,6 +217,19 @@ public interface HugeGraph extends Graph { void mode(GraphMode mode); + default Map loadingMode() { + throw new UnsupportedOperationException("Loading ownership is not supported by this graph implementation"); + } + + default Map loadingMode(String action, String instance, long epoch, + String token, String principal) { + throw new UnsupportedOperationException("Loading ownership is not supported by this graph implementation"); + } + + default Map recoverLoadingMode(String instance, long epoch, long revision) { + throw new UnsupportedOperationException("Loading recovery is not supported by this graph implementation"); + } + GraphReadMode readMode(); void readMode(GraphReadMode readMode); @@ -273,8 +287,6 @@ public interface HugeGraph extends Graph { AuthManager authManager(); - RoleElectionStateMachine roleElectionStateMachine(); - void switchAuthManager(AuthManager authManager); TaskScheduler taskScheduler(); @@ -283,15 +295,25 @@ public interface HugeGraph extends Graph { void proxy(HugeGraph graph); - boolean sameAs(HugeGraph graph); + boolean sameAs(HugeGraphSupplier graph); long now(); V option(TypedOption option); + // Build the same term matcher used by SEARCH indexes, without reading data. + default Predicate searchPredicate(String text) { + throw new UnsupportedOperationException("Local SEARCH matching is not supported by this graph"); + } + void registerRpcServices(RpcServiceConfig4Server serverConfig, RpcServiceConfig4Client clientConfig); + default void unregisterRpcServices() { + // Legacy implementations do not participate in the owned RPC cleanup contract. + // Implementations owning graph-scoped RPC resources must override this hook. + } + default List mapPkId2Name(Collection ids) { List names = new ArrayList<>(ids.size()); for (Id id : ids) { @@ -310,15 +332,6 @@ default List mapVlId2Name(Collection ids) { return names; } - default List mapElId2Name(Collection ids) { - List names = new ArrayList<>(ids.size()); - for (Id id : ids) { - SchemaElement schema = this.edgeLabel(id); - names.add(schema.name()); - } - return names; - } - default List mapIlId2Name(Collection ids) { List names = new ArrayList<>(ids.size()); for (Id id : ids) { @@ -350,16 +363,6 @@ default Id[] mapElName2Id(String[] edgeLabels) { return ids; } - default Set> mapPairId2Name( - Set> pairs) { - Set> results = new HashSet<>(pairs.size()); - for (Pair pair : pairs) { - results.add(Pair.of(this.vertexLabel(pair.getLeft()).name(), - this.vertexLabel(pair.getRight()).name())); - } - return results; - } - default Id[] mapVlName2Id(String[] vertexLabels) { Id[] ids = new Id[vertexLabels.length]; for (int i = 0; i < vertexLabels.length; i++) { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/LoadingMode.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/LoadingMode.java new file mode 100644 index 0000000000..51265bfed2 --- /dev/null +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/LoadingMode.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Supplier; + +import org.apache.hugegraph.type.define.GraphMode; +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.Log; +import org.slf4j.Logger; + +import com.google.common.collect.ImmutableMap; + +/** Same graph-instance ownership; this state is neither persisted nor distributed. */ +public final class LoadingMode { + + private static final Logger LOG = Log.logger(LoadingMode.class); + + private static final int MAX_ACTIVE = 1024; + private static final int MAX_RELEASED = 1024; + + private final Supplier graphName; + private final String instance = UUID.randomUUID().toString(); + private final Map active = new HashMap<>(); + private final Map released = new HashMap<>(); + private long epoch; + private long revision; + private volatile GraphMode mode = GraphMode.NONE; + + public LoadingMode(Supplier graphName) { + this.graphName = Objects.requireNonNull(graphName, "graphName"); + } + + public GraphMode mode() { + return this.mode; + } + + public synchronized void mode(GraphMode mode) { + Objects.requireNonNull(mode, "mode"); + checkState(this.active.isEmpty(), "Loading owners are active; release them before setting graph mode"); + this.advanceRevision(); + this.mode = mode; + } + + public synchronized Map capability() { + return ImmutableMap.of("protocol", 1, "instance", this.instance, "epoch", this.epoch, + "revision", this.revision); + } + + public synchronized Map recover(String instance, long epoch, long revision) { + E.checkArgument(this.instance.equals(instance), "Loading owner belongs to another graph instance"); + E.checkArgument(epoch >= 0 && revision >= 0, "Invalid loading recovery epoch or revision"); + checkState(epoch == this.epoch && revision == this.revision, + "Loading state changed; inspect it before recovery"); + checkState(this.epoch < Long.MAX_VALUE, "Loading owner epoch exhausted"); + GraphMode previousMode = this.mode; + int clearedOwners = this.active.size(); + this.advanceRevision(); + this.epoch++; + this.active.clear(); + this.released.clear(); + this.mode = GraphMode.NONE; + this.logChange("recover", previousMode, clearedOwners); + return ImmutableMap.builder().putAll(this.capability()) + .put("state", "recovered").put("mode", this.mode).build(); + } + + public synchronized Map update(String action, String instance, long epoch, + String token, String principal) { + E.checkArgument("acquire".equals(action) || "release".equals(action), + "Loading owner action must be acquire or release"); + E.checkArgument(this.instance.equals(instance), "Loading owner belongs to another graph instance"); + E.checkArgument(epoch >= 0 && epoch <= this.epoch, "Invalid loading owner epoch"); + E.checkArgument(token != null && token.length() == 36 && UUID.fromString(token).toString().equals(token), + "Loading owner token must be a canonical UUID"); + Objects.requireNonNull(principal, "principal"); + GraphMode previousMode = this.mode; + String key = epoch + "/" + token; + String owner = this.active.get(key); + if (owner != null) { + checkState(owner.equals(principal), "Loading owner belongs to another authenticated principal"); + if ("acquire".equals(action)) { + return this.receipt(epoch, token, "active"); + } + // Exhaustion must fail before changing an active registration. + checkState(epoch != this.epoch || this.released.size() < MAX_RELEASED || + this.epoch < Long.MAX_VALUE, "Loading owner epoch exhausted"); + this.advanceRevision(); + this.active.remove(key); + if (this.active.isEmpty()) { + this.mode = GraphMode.NONE; + } + } else if ("acquire".equals(action)) { + if (epoch < this.epoch || this.released.containsKey(key)) { + return this.receipt(epoch, token, "released"); + } + checkState(this.active.isEmpty() ? this.mode == GraphMode.NONE : this.mode == GraphMode.LOADING, + "Cannot acquire loading ownership of manually set graph mode %s", this.mode); + checkState(this.active.size() < MAX_ACTIVE, "Loading owner limit %s reached", MAX_ACTIVE); + this.advanceRevision(); + this.active.put(key, principal); + this.mode = GraphMode.LOADING; + this.logChange("acquire", previousMode, 0); + return this.receipt(epoch, token, "active"); + } + // Unknown release cancels an acquire which may still be in transit. + if (epoch == this.epoch) { + String releasedOwner = this.released.get(key); + checkState(releasedOwner == null || releasedOwner.equals(principal), + "Loading owner belongs to another authenticated principal"); + if (releasedOwner != null) { + return this.receipt(epoch, token, "released"); + } + if (this.released.size() == MAX_RELEASED) { + checkState(this.epoch < Long.MAX_VALUE, "Loading owner epoch exhausted"); + if (owner == null) { + this.advanceRevision(); + } + this.epoch++; + this.released.clear(); + // Old nonactive tuples are terminal; active old tuples remain registered. + } else { + if (owner == null) { + this.advanceRevision(); + } + this.released.put(key, principal); + } + } + if (owner != null) { + this.logChange("release", previousMode, 1); + } + return this.receipt(epoch, token, "released"); + } + + private void logChange(String action, GraphMode previousMode, int clearedOwners) { + LOG.info("Loading ownership {} for graph '{}' instance {}: mode {} -> {}, epoch {}, " + + "activeOwners {}, clearedOwners {}", action, this.graphName.get(), this.instance, + previousMode, this.mode, this.epoch, this.active.size(), clearedOwners); + } + + private void advanceRevision() { + checkState(this.revision < Long.MAX_VALUE, "Loading state revision exhausted"); + this.revision++; + } + + private static void checkState(boolean condition, String message, Object... args) { + if (!condition) { + throw new ConflictException(String.format(message, args)); + } + } + + public static class ConflictException extends IllegalStateException { + + public ConflictException(String message) { + super(message); + } + } + + private Map receipt(long epoch, String token, String state) { + return ImmutableMap.builder().put("protocol", 1).put("instance", this.instance) + .put("epoch", epoch).put("token", token).put("state", state) + .put("mode", this.mode).build(); + } +} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/StandardHugeGraph.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/StandardHugeGraph.java index 8f2ff95d01..90eb5f0704 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/StandardHugeGraph.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/StandardHugeGraph.java @@ -17,16 +17,21 @@ package org.apache.hugegraph; +import org.apache.hugegraph.exception.HugeException; + +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; 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.Future; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.analyzer.Analyzer; @@ -34,21 +39,16 @@ import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.StandardAuthManager; import org.apache.hugegraph.auth.StandardAuthManagerV2; -import org.apache.hugegraph.backend.BackendException; import org.apache.hugegraph.backend.LocalCounter; import org.apache.hugegraph.backend.cache.Cache; -import org.apache.hugegraph.backend.cache.CacheNotifier; import org.apache.hugegraph.backend.cache.CacheNotifier.GraphCacheNotifier; import org.apache.hugegraph.backend.cache.CacheNotifier.SchemaCacheNotifier; +import org.apache.hugegraph.backend.cache.CacheNotifier; import org.apache.hugegraph.backend.cache.CachedGraphTransaction; import org.apache.hugegraph.backend.cache.CachedSchemaTransaction; import org.apache.hugegraph.backend.cache.CachedSchemaTransactionV2; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; import org.apache.hugegraph.backend.id.SnowflakeIdGenerator; -import org.apache.hugegraph.backend.query.Query; import org.apache.hugegraph.backend.serializer.AbstractSerializer; -import org.apache.hugegraph.backend.serializer.BytesBuffer; import org.apache.hugegraph.backend.serializer.SerializerFactory; import org.apache.hugegraph.backend.store.BackendFeatures; import org.apache.hugegraph.backend.store.BackendProviderFactory; @@ -65,36 +65,35 @@ import org.apache.hugegraph.config.TypedOption; import org.apache.hugegraph.event.EventHub; import org.apache.hugegraph.event.EventListener; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.exception.NotAllowException; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.io.HugeGraphIoRegistry; import org.apache.hugegraph.job.EphemeralJob; import org.apache.hugegraph.kvstore.KvStore; -import org.apache.hugegraph.masterelection.ClusterRoleStore; -import org.apache.hugegraph.masterelection.Config; import org.apache.hugegraph.masterelection.GlobalMasterInfo; -import org.apache.hugegraph.masterelection.RoleElectionConfig; -import org.apache.hugegraph.masterelection.RoleElectionOptions; -import org.apache.hugegraph.masterelection.RoleElectionStateMachine; -import org.apache.hugegraph.masterelection.StandardClusterRoleStore; -import org.apache.hugegraph.masterelection.StandardRoleElectionStateMachine; import org.apache.hugegraph.memory.MemoryManager; import org.apache.hugegraph.memory.util.RoundUtil; import org.apache.hugegraph.meta.MetaManager; import org.apache.hugegraph.perf.PerfUtil.Watched; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.rpc.RpcServiceConfig4Client; import org.apache.hugegraph.rpc.RpcServiceConfig4Server; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.SchemaLabel; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeEdgeProperty; import org.apache.hugegraph.structure.HugeFeatures; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.structure.HugeVertexProperty; +import org.apache.hugegraph.structure.builder.IndexBuilder; import org.apache.hugegraph.task.EphemeralJobQueue; import org.apache.hugegraph.task.ServerInfoManager; import org.apache.hugegraph.task.TaskManager; @@ -104,6 +103,7 @@ import org.apache.hugegraph.type.define.GraphReadMode; import org.apache.hugegraph.util.ConfigUtil; import org.apache.hugegraph.util.DateUtil; +import org.apache.hugegraph.util.CloseFailure; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Events; import org.apache.hugegraph.util.LockUtil; @@ -169,6 +169,8 @@ public class StandardHugeGraph implements HugeGraph { private final EventHub schemaEventHub; private final EventHub graphEventHub; private final EventHub indexEventHub; + private final List rpcServiceCleanup = new ArrayList<>(); + private final List closeResources = new ArrayList<>(); private final LocalCounter localCounter; private final RateLimiter writeRateLimiter; private final RateLimiter readRateLimiter; @@ -179,12 +181,13 @@ public class StandardHugeGraph implements HugeGraph { private final RamTable ramtable; private volatile boolean started; private volatile boolean closed; - private volatile GraphMode mode; + private boolean closeCompleted; + private volatile boolean dropBackendCleared; + private final LoadingMode loadingMode = new LoadingMode(this::spaceGraphName); private volatile GraphReadMode readMode; private volatile HugeVariables variables; private String graphSpace; private AuthManager authManager; - private RoleElectionStateMachine roleElectionStateMachine; private String nickname; private String creator; private Date createTime; @@ -226,16 +229,9 @@ public StandardHugeGraph(HugeConfig config) { this.taskManager = TaskManager.instance(); this.name = config.get(CoreOptions.STORE); - // Keep old config files upgrade-safe while ignoring the legacy scheduler. - if (config.containsKey("task.scheduler_type")) { - LOG.warn("Config key 'task.scheduler_type' is deprecated and " + - "ignored. The scheduler is auto-selected by backend " + - "type (hstore -> distributed, others -> local)."); - } - this.started = false; this.closed = false; - this.mode = GraphMode.NONE; + this.closeCompleted = false; this.readMode = GraphReadMode.OLTP_ONLY; // Init process-wide static configs before lock, so that validation @@ -364,7 +360,6 @@ public void serverStarted(GlobalMasterInfo nodeInfo) { if (nodeInfo != null && nodeInfo.nodeId() != null) { this.serverInfoManager().initServerInfo(nodeInfo); - this.initRoleStateMachine(nodeInfo.nodeId()); } // TODO: check necessary? @@ -381,22 +376,6 @@ public void serverStarted(GlobalMasterInfo nodeInfo) { this.started = true; } - private void initRoleStateMachine(Id serverId) { - HugeConfig conf = this.configuration; - Config roleConfig = new RoleElectionConfig(serverId.toString(), - conf.get(RoleElectionOptions.NODE_EXTERNAL_URL), - conf.get(RoleElectionOptions.EXCEEDS_FAIL_COUNT), - conf.get( - RoleElectionOptions.RANDOM_TIMEOUT_MILLISECOND), - conf.get( - RoleElectionOptions.HEARTBEAT_INTERVAL_SECOND), - conf.get(RoleElectionOptions.MASTER_DEAD_TIMES), - conf.get( - RoleElectionOptions.BASE_TIMEOUT_MILLISECOND)); - ClusterRoleStore roleStore = new StandardClusterRoleStore(this.params); - this.roleElectionStateMachine = new StandardRoleElectionStateMachine(roleConfig, roleStore); - } - @Override public boolean started() { return this.started; @@ -415,25 +394,81 @@ public boolean closed() { return this.closed; } + void closeCurrentThreadTransaction() { + Throwable failure = null; + try { + if (this.tx.isOpen()) { + // Request/task cleanup must never commit unfinished writes. + this.tx.rollback(); + } + } catch (RuntimeException | Error error) { + failure = error; + throw error; + } finally { + this.tx.clearTransactionListeners(); + this.tx.resetState(); + try { + this.tx.destroyTransaction(); + } catch (RuntimeException | Error error) { + if (failure == null) { + throw error; + } + if (failure != error) { + failure.addSuppressed(error); + } + } + } + } + private void closeTx() { + Throwable failure = null; try { if (this.tx.isOpen()) { this.tx.close(); } + } catch (RuntimeException | Error error) { + failure = error; + throw error; } finally { - this.tx.destroyTransaction(); + try { + this.tx.destroyTransaction(); + } catch (RuntimeException | Error error) { + if (failure == null) { + throw error; + } + Throwable combined = CloseFailure.collect(failure, error); + if (combined != failure) { + throw (Error) combined; + } + } } } @Override public GraphMode mode() { - return this.mode; + return this.loadingMode.mode(); } @Override public void mode(GraphMode mode) { + this.loadingMode.mode(mode); LOG.info("Graph {} will work in {} mode", this, mode); - this.mode = mode; + } + + @Override + public Map loadingMode() { + return this.loadingMode.capability(); + } + + @Override + public Map loadingMode(String action, String instance, long epoch, + String token, String principal) { + return this.loadingMode.update(action, instance, epoch, token, principal); + } + + @Override + public Map recoverLoadingMode(String instance, long epoch, long revision) { + return this.loadingMode.recover(instance, epoch, revision); } @Override @@ -526,6 +561,7 @@ public void initBackend() { @Override public void clearBackend() { + this.checkGraphNotClosed(); this.waitUntilAllTasksCompleted(); this.loadSchemaStore().open(this.configuration); @@ -722,6 +758,11 @@ private Analyzer analyzer() { return AnalyzerFactory.analyzer(name, mode); } + @Override + public Predicate searchPredicate(String text) { + return IndexBuilder.searchPredicate(this.analyzer(), text); + } + protected void reloadRamtable() { this.reloadRamtable(false); } @@ -1118,33 +1159,64 @@ public Transaction tx() { @Override public synchronized void close() throws Exception { - if (this.closed()) { + if (this.closed() && this.closeCompleted) { return; } LOG.info("Close graph {}", this); - if (StandardAuthManager.isLocal(this.authManager)) { - this.authManager.close(); - } - this.taskManager.closeScheduler(this.params); - try { - this.closeTx(); - } finally { - this.closed = true; - this.storeProvider.close(); - LockUtil.destroy(this.spaceGraphName()); + Throwable failure = null; + for (Runnable close : new Runnable[]{() -> { + if (StandardAuthManager.isLocal(this.authManager)) { + this.authManager.close(); + } + }, () -> this.taskManager.closeScheduler(this.params), this::closeTx}) { + try { + close.run(); + } catch (RuntimeException | Error error) { + failure = CloseFailure.collect(failure, error); + } } - // Make sure that all transactions are closed in all threads - if (!this.tx.closed()) { + // A failed task drain retains its scheduler and remains retryable. + boolean drained = this.tx.closed() && this.taskManager.getScheduler(this.params) == null; + if (!drained) { for (String key : this.tx.openedThreads) { LOG.warn("thread [{}] did not close transaction", key); } + RuntimeException active = new IllegalStateException(String.format( + "Ensure scheduler and tx closed in all threads when closing graph '%s'", + this.spaceGraphName())); + failure = CloseFailure.collect(failure, active); + } + if (drained) { + // Plain provider close only notifies listeners; the Raft wrapper also + // stops its context. Owner-close errors do not imply native release. + if (!this.closed) { + this.closeResources.addAll(Arrays.asList( + this::unregisterRpcServices, + () -> CachedGraphTransaction.closeGraph(this.params), + () -> CachedSchemaTransaction.closeGraph(this.params), + () -> CachedSchemaTransactionV2.closeGraph(this.params), + this.storeProvider::close, () -> LockUtil.destroy(this.spaceGraphName()))); + this.closed = true; + } + for (Iterator closes = this.closeResources.iterator(); closes.hasNext();) { + try { + closes.next().run(); + closes.remove(); + } catch (RuntimeException | Error error) { + failure = CloseFailure.collect(failure, error); + } + } } - E.checkState(this.tx.closed(), - "Ensure tx closed in all threads when closing graph '%s'", - this.spaceGraphName()); - + if (failure instanceof Error) { + throw (Error) failure; + } + if (failure != null) { + throw (RuntimeException) failure; + } + // Logical closure fences access before resources close; only success ends cleanup retries. + this.closeCompleted = true; } @Override @@ -1160,24 +1232,39 @@ public void create(String configPath, GlobalMasterInfo nodeInfo) { @Override public void drop() { - this.clearBackend(); - - HugeConfig config = this.configuration(); - this.storeProvider.onDeleteConfig(config); - ConfigUtil.deleteFile(config.file()); + if (!this.dropBackendCleared) { + this.checkGraphNotClosed(); + TaskScheduler scheduler = this.taskManager.getScheduler(this.params); + if (scheduler != null && !scheduler.close()) { + // Fence admission before waiting for actual task owners to exit. + try { + scheduler.waitUntilAllTasksCompleted(this.configuration().get(CoreOptions.TASK_WAIT_TIMEOUT)); + } catch (TimeoutException e) { + throw new HugeException("Tasks are still active in graph '%s'; task admission remains closed. " + + "Retry deletion after owners exit, or restart to resume task admission", + e, this.spaceGraphName()); + } + E.checkState(scheduler.close(), + "Tasks are still active in graph '%s'; task admission remains closed. " + + "Retry deletion after owners exit, or restart to resume task admission", + this.spaceGraphName()); + } + this.clearBackend(); + // Only this same owner's successful drop clear permits cleanup after logical close. + this.dropBackendCleared = true; + } try { - /* - * It's hard to ensure all threads close the tx. - * TODO: - * - schedule a tx-close to each thread, - * or - * - add forceClose() method to backend store. - */ this.close(); - } catch (Throwable e) { - LOG.warn("Failed to close graph {} {}", this, e); + } catch (Error failure) { + throw failure; + } catch (Exception failure) { + throw new HugeException("Failed to close graph '%s', please retry later", + failure, this.spaceGraphName()); } + HugeConfig config = this.configuration(); + this.storeProvider.onDeleteConfig(config); + ConfigUtil.deleteFile(config.file()); } @Override @@ -1256,11 +1343,6 @@ public AuthManager authManager() { return this.authManager; } - @Override - public RoleElectionStateMachine roleElectionStateMachine() { - return this.roleElectionStateMachine; - } - @Override public void switchAuthManager(AuthManager authManager) { this.authManager = authManager; @@ -1292,7 +1374,7 @@ public final void proxy(HugeGraph graph) { } @Override - public boolean sameAs(HugeGraph graph) { + public boolean sameAs(HugeGraphSupplier graph) { return this == graph; } @@ -1312,26 +1394,85 @@ public V option(TypedOption option) { } @Override - public void registerRpcServices(RpcServiceConfig4Server serverConfig, - RpcServiceConfig4Client clientConfig) { + public synchronized void registerRpcServices(RpcServiceConfig4Server serverConfig, + RpcServiceConfig4Client clientConfig) { /* * Skip register cache-rpc service if it's non-shared storage, * because we assume cache of non-shared storage is updated by raft. */ - if (!this.backendStoreFeatures().supportsSharedStorage()) { + if (!this.rpcServiceCleanup.isEmpty()) { return; } + boolean opened = this.tx.isOpen(); + boolean shared; + Throwable readFailure = null; + try { + shared = this.backendStoreFeatures().supportsSharedStorage(); + } catch (RuntimeException | Error error) { + readFailure = error; + throw error; + } finally { + if (!opened && this.tx.isOpen()) { + try { + this.closeCurrentThreadTransaction(); + } catch (RuntimeException | Error cleanup) { + if (readFailure == null) { + throw cleanup; + } + if (readFailure != cleanup) { + readFailure.addSuppressed(cleanup); + } + } + } + } + if (!shared) { + return; + } + E.checkState(!this.closed(), "Can't register RPC services for a closed graph"); + String graph = this.spaceGraphName(); + try { + GraphCacheNotifier graphProxy = clientConfig.serviceProxy(graph, GraphCacheNotifier.class); + this.rpcServiceCleanup.add(() -> clientConfig.removeServiceProxy( + graph, GraphCacheNotifier.class.getName(), graphProxy)); + HugeGraphCacheNotifier graphNotifier = new HugeGraphCacheNotifier(this.graphEventHub, graphProxy); + this.rpcServiceCleanup.add(graphNotifier::close); + String graphService = serverConfig.addService(graph, GraphCacheNotifier.class, graphNotifier); + this.rpcServiceCleanup.add(() -> serverConfig.removeService(graphService, graphNotifier)); + + SchemaCacheNotifier schemaProxy = clientConfig.serviceProxy(graph, SchemaCacheNotifier.class); + this.rpcServiceCleanup.add(() -> clientConfig.removeServiceProxy( + graph, SchemaCacheNotifier.class.getName(), schemaProxy)); + HugeSchemaCacheNotifier schemaNotifier = new HugeSchemaCacheNotifier(this.schemaEventHub, schemaProxy); + this.rpcServiceCleanup.add(schemaNotifier::close); + String schemaService = serverConfig.addService(graph, SchemaCacheNotifier.class, schemaNotifier); + this.rpcServiceCleanup.add(() -> serverConfig.removeService(schemaService, schemaNotifier)); + } catch (RuntimeException | Error failure) { + try { + this.unregisterRpcServices(); + } catch (RuntimeException | Error cleanup) { + failure.addSuppressed(cleanup); + } + throw failure; + } + } - Class clazz1 = GraphCacheNotifier.class; - // The proxy is sometimes unavailable (issue #664) - CacheNotifier proxy = clientConfig.serviceProxy(this.spaceGraphName(), clazz1); - serverConfig.addService(this.spaceGraphName(), clazz1, new HugeGraphCacheNotifier( - this.graphEventHub, proxy)); - - Class clazz2 = SchemaCacheNotifier.class; - proxy = clientConfig.serviceProxy(this.spaceGraphName(), clazz2); - serverConfig.addService(this.spaceGraphName(), clazz2, new HugeSchemaCacheNotifier( - this.schemaEventHub, proxy)); + @Override + public synchronized void unregisterRpcServices() { + Throwable failure = null; + for (int i = this.rpcServiceCleanup.size() - 1; i >= 0; i--) { + try { + this.rpcServiceCleanup.get(i).run(); + this.rpcServiceCleanup.remove(i); + } catch (RuntimeException | Error error) { + failure = CloseFailure.collect(failure, error); + } + } + if (failure instanceof Error) { + throw (Error) failure; + } + if (failure != null) { + throw (RuntimeException) failure; + } } private void waitUntilAllTasksCompleted() { @@ -1368,22 +1509,21 @@ public void rollback() { } public void close() { - try { - this.graphTx.close(); - } catch (Exception e) { - LOG.error("Failed to close GraphTransaction", e); + Throwable failure = null; + for (Runnable close : new Runnable[]{this.graphTx::close, + this.systemTx::close, + this.schemaTx::close}) { + try { + close.run(); + } catch (RuntimeException | Error error) { + failure = CloseFailure.collect(failure, error); + } } - - try { - this.systemTx.close(); - } catch (Exception e) { - LOG.error("Failed to close SystemTransaction", e); + if (failure instanceof Error) { + throw (Error) failure; } - - try { - this.schemaTx.close(); - } catch (Exception e) { - LOG.error("Failed to close SchemaTransaction", e); + if (failure != null) { + throw (RuntimeException) failure; } } @@ -1416,10 +1556,14 @@ private static class AbstractCacheNotifier implements CacheNotifier { private final EventHub hub; private final EventListener cacheEventListener; + private volatile boolean closed; public AbstractCacheNotifier(EventHub hub, CacheNotifier proxy) { this.hub = hub; this.cacheEventListener = event -> { + if (this.closed) { + return false; + } try { LOG.info("Received event: {}", event); Object[] args = event.args(); @@ -1463,6 +1607,7 @@ public AbstractCacheNotifier(EventHub hub, CacheNotifier proxy) { @Override public void close() { + this.closed = true; this.hub.unlisten(Events.CACHE, this.cacheEventListener); } @@ -1859,10 +2004,14 @@ private void destroyTransaction() { // Do close if needed, then remove the reference Txs txs = this.transactions.get(); - if (txs != null) { - txs.close(); + try { + if (txs != null) { + txs.close(); + } + } finally { + // A reused request/worker must never inherit partially closed owners. + this.transactions.remove(); } - this.transactions.remove(); } } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/Analyzer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/Analyzer.java deleted file mode 100644 index 8e7537e072..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/Analyzer.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.analyzer; - -import java.util.Set; - -public interface Analyzer { - - Set segment(String text); -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/AnalyzerFactory.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/AnalyzerFactory.java deleted file mode 100644 index 21dd157b11..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/AnalyzerFactory.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.analyzer; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.serializer.SerializerFactory; - -public class AnalyzerFactory { - - private static final Map> ANALYZERS; - - static { - ANALYZERS = new ConcurrentHashMap<>(); - } - - public static Analyzer analyzer(String name, String mode) { - name = name.toLowerCase(); - switch (name) { - case "ansj": - return new AnsjAnalyzer(mode); - case "hanlp": - return new HanLPAnalyzer(mode); - case "smartcn": - return new SmartCNAnalyzer(mode); - case "jieba": - return new JiebaAnalyzer(mode); - case "jcseg": - return new JcsegAnalyzer(mode); - case "mmseg4j": - return new MMSeg4JAnalyzer(mode); - case "ikanalyzer": - return new IKAnalyzer(mode); - default: - return customizedAnalyzer(name, mode); - } - } - - private static Analyzer customizedAnalyzer(String name, String mode) { - Class clazz = ANALYZERS.get(name); - if (clazz == null) { - throw new HugeException("Not exists analyzer: %s", name); - } - - assert Analyzer.class.isAssignableFrom(clazz); - try { - return clazz.getConstructor(String.class).newInstance(mode); - } catch (Exception e) { - throw new HugeException( - "Failed to construct analyzer '%s' with mode '%s'", - e, name, mode); - } - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - public static void register(String name, String classPath) { - ClassLoader classLoader = SerializerFactory.class.getClassLoader(); - Class clazz; - try { - clazz = classLoader.loadClass(classPath); - } catch (Exception e) { - throw new HugeException("Load class path '%s' failed", - e, classPath); - } - - // Check subclass - if (!Analyzer.class.isAssignableFrom(clazz)) { - throw new HugeException("Class '%s' is not a subclass of " + - "class Analyzer", classPath); - } - - // Check exists - if (ANALYZERS.containsKey(name)) { - throw new HugeException("Exists analyzer: %s(%s)", - name, ANALYZERS.get(name).getName()); - } - - // Register class - ANALYZERS.put(name, (Class) clazz); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/AnsjAnalyzer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/AnsjAnalyzer.java deleted file mode 100644 index 2f857cd171..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/AnsjAnalyzer.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.analyzer; - -import java.util.List; -import java.util.Set; - -import org.ansj.domain.Result; -import org.ansj.domain.Term; -import org.ansj.splitWord.analysis.BaseAnalysis; -import org.ansj.splitWord.analysis.IndexAnalysis; -import org.ansj.splitWord.analysis.NlpAnalysis; -import org.ansj.splitWord.analysis.ToAnalysis; -import org.apache.hugegraph.config.ConfigException; -import org.apache.hugegraph.util.InsertionOrderUtil; - -import com.google.common.collect.ImmutableList; - -/** - * Reference from https://github.com/NLPchina/ansj_seg/wiki - */ -public class AnsjAnalyzer implements Analyzer { - - public static final List SUPPORT_MODES = ImmutableList.of( - "BaseAnalysis", - "IndexAnalysis", - "ToAnalysis", - "NlpAnalysis" - ); - - private final String analysis; - - public AnsjAnalyzer(String mode) { - if (!SUPPORT_MODES.contains(mode)) { - throw new ConfigException( - "Unsupported segment mode '%s' for ansj analyzer, " + - "the available values are %s", mode, SUPPORT_MODES); - } - this.analysis = mode; - } - - @Override - public Set segment(String text) { - Result terms; - switch (this.analysis) { - case "BaseAnalysis": - terms = BaseAnalysis.parse(text); - break; - case "ToAnalysis": - terms = ToAnalysis.parse(text); - break; - case "NlpAnalysis": - terms = NlpAnalysis.parse(text); - break; - case "IndexAnalysis": - terms = IndexAnalysis.parse(text); - break; - default: - throw new AssertionError(String.format( - "Unsupported segment mode '%s'", this.analysis)); - } - - assert terms != null; - Set result = InsertionOrderUtil.newSet(); - for (Term term : terms) { - result.add(term.getName()); - } - return result; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/HanLPAnalyzer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/HanLPAnalyzer.java deleted file mode 100644 index 8b5fa6aa0a..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/HanLPAnalyzer.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.analyzer; - -import java.util.List; -import java.util.Set; - -import org.apache.hugegraph.config.ConfigException; -import org.apache.hugegraph.util.InsertionOrderUtil; - -import com.google.common.collect.ImmutableList; -import com.hankcs.hanlp.seg.Dijkstra.DijkstraSegment; -import com.hankcs.hanlp.seg.NShort.NShortSegment; -import com.hankcs.hanlp.seg.Segment; -import com.hankcs.hanlp.seg.common.Term; -import com.hankcs.hanlp.tokenizer.IndexTokenizer; -import com.hankcs.hanlp.tokenizer.NLPTokenizer; -import com.hankcs.hanlp.tokenizer.SpeedTokenizer; -import com.hankcs.hanlp.tokenizer.StandardTokenizer; - -/** - * Reference from https://github.com/hankcs/HanLP/wiki - */ -public class HanLPAnalyzer implements Analyzer { - - public static final List SUPPORT_MODES = - ImmutableList.builder() - .add("standard") - .add("nlp") - .add("index") - .add("nShort") - .add("shortest") - .add("speed") - .build(); - - private static final Segment N_SHORT_SEGMENT = - new NShortSegment().enableCustomDictionary(false) - .enablePlaceRecognize(true) - .enableOrganizationRecognize(true); - private static final Segment DIJKSTRA_SEGMENT = - new DijkstraSegment().enableCustomDictionary(false) - .enablePlaceRecognize(true) - .enableOrganizationRecognize(true); - - private final String tokenizer; - - public HanLPAnalyzer(String mode) { - if (!SUPPORT_MODES.contains(mode)) { - throw new ConfigException( - "Unsupported segment mode '%s' for hanlp analyzer, " + - "the available values are %s", mode, SUPPORT_MODES); - } - this.tokenizer = mode; - } - - @Override - public Set segment(String text) { - List terms; - switch (this.tokenizer) { - case "standard": - terms = StandardTokenizer.segment(text); - break; - case "nlp": - terms = NLPTokenizer.segment(text); - break; - case "index": - terms = IndexTokenizer.segment(text); - break; - case "nShort": - terms = N_SHORT_SEGMENT.seg(text); - break; - case "shortest": - terms = DIJKSTRA_SEGMENT.seg(text); - break; - case "speed": - terms = SpeedTokenizer.segment(text); - break; - default: - throw new AssertionError(String.format( - "Unsupported segment mode '%s'", this.tokenizer)); - } - - assert terms != null; - Set result = InsertionOrderUtil.newSet(); - for (Term term : terms) { - result.add(term.word); - } - return result; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/IKAnalyzer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/IKAnalyzer.java deleted file mode 100644 index ab564f396f..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/IKAnalyzer.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.analyzer; - -import java.io.StringReader; -import java.util.List; -import java.util.Set; - -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.config.ConfigException; -import org.apache.hugegraph.util.InsertionOrderUtil; -import org.wltea.analyzer.core.IKSegmenter; -import org.wltea.analyzer.core.Lexeme; - -import com.google.common.collect.ImmutableList; - -/** - * Reference from https://github.com/yyn0210/IKAnalyzer - */ -public class IKAnalyzer implements Analyzer { - - public static final List SUPPORT_MODES = ImmutableList.of( - "smart", - "max_word" - ); - - private final boolean smartSegMode; - - public IKAnalyzer(String mode) { - if (!SUPPORT_MODES.contains(mode)) { - throw new ConfigException( - "Unsupported segment mode '%s' for ikanalyzer, " + - "the available values are %s", mode, SUPPORT_MODES); - } - this.smartSegMode = SUPPORT_MODES.get(0).equals(mode); - } - - @Override - public Set segment(String text) { - Set result = InsertionOrderUtil.newSet(); - IKSegmenter ik = new IKSegmenter(new StringReader(text), - this.smartSegMode); - try { - Lexeme word; - while ((word = ik.next()) != null) { - result.add(word.getLexemeText()); - } - } catch (Exception e) { - throw new HugeException("IKAnalyzer segment text '%s' failed", - e, text); - } - return result; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/JcsegAnalyzer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/JcsegAnalyzer.java deleted file mode 100644 index 3740e87b19..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/JcsegAnalyzer.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.analyzer; - -import java.io.StringReader; -import java.util.List; -import java.util.Set; - -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.config.ConfigException; -import org.apache.hugegraph.util.InsertionOrderUtil; -import org.lionsoul.jcseg.ISegment; -import org.lionsoul.jcseg.IWord; -import org.lionsoul.jcseg.dic.ADictionary; -import org.lionsoul.jcseg.dic.DictionaryFactory; -import org.lionsoul.jcseg.segmenter.SegmenterConfig; - -import com.google.common.collect.ImmutableList; - -/** - * Reference from https://github.com/lionsoul2014/jcseg - */ -public class JcsegAnalyzer implements Analyzer { - - public static final List SUPPORT_MODES = ImmutableList.of( - "Simple", - "Complex" - ); - - private static final SegmenterConfig CONFIG = new SegmenterConfig(); - private static final ADictionary DIC = DictionaryFactory.createDefaultDictionary(CONFIG); - - private final ISegment.Type type; - - public JcsegAnalyzer(String mode) { - if (!SUPPORT_MODES.contains(mode)) { - throw new ConfigException( - "Unsupported segment mode '%s' for jcseg analyzer, " + - "the available values are %s", mode, SUPPORT_MODES); - } - - if ("Simple".equals(mode)) { - this.type = ISegment.SIMPLE; - } else { - this.type = ISegment.COMPLEX; - } - } - - @Override - public Set segment(String text) { - Set result = InsertionOrderUtil.newSet(); - try { - ISegment segmentor = this.type.factory.create(CONFIG, DIC); - segmentor.reset(new StringReader(text)); - - IWord word; - while ((word = segmentor.next()) != null) { - result.add(word.getValue()); - } - } catch (Exception e) { - throw new HugeException("Jcseg segment text '%s' failed", e, text); - } - return result; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/JiebaAnalyzer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/JiebaAnalyzer.java deleted file mode 100644 index 7c280a50a1..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/JiebaAnalyzer.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.analyzer; - -import java.util.List; -import java.util.Set; - -import org.apache.hugegraph.config.ConfigException; -import org.apache.hugegraph.util.InsertionOrderUtil; - -import com.google.common.collect.ImmutableList; -import com.huaban.analysis.jieba.JiebaSegmenter; -import com.huaban.analysis.jieba.SegToken; - -/** - * Reference from https://github.com/huaban/jieba-analysis - */ -public class JiebaAnalyzer implements Analyzer { - - public static final List SUPPORT_MODES = ImmutableList.of( - "SEARCH", - "INDEX" - ); - - private static final JiebaSegmenter JIEBA_SEGMENTER = new JiebaSegmenter(); - - private final JiebaSegmenter.SegMode segMode; - - public JiebaAnalyzer(String mode) { - if (!SUPPORT_MODES.contains(mode)) { - throw new ConfigException( - "Unsupported segment mode '%s' for jieba analyzer, " + - "the available values are %s", mode, SUPPORT_MODES); - } - this.segMode = JiebaSegmenter.SegMode.valueOf(mode); - } - - @Override - public Set segment(String text) { - Set result = InsertionOrderUtil.newSet(); - for (SegToken token : JIEBA_SEGMENTER.process(text, this.segMode)) { - result.add(token.word); - } - return result; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/MMSeg4JAnalyzer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/MMSeg4JAnalyzer.java deleted file mode 100644 index 26e937a538..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/MMSeg4JAnalyzer.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.analyzer; - -import java.io.StringReader; -import java.util.List; -import java.util.Set; - -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.config.ConfigException; -import org.apache.hugegraph.util.InsertionOrderUtil; - -import com.chenlb.mmseg4j.ComplexSeg; -import com.chenlb.mmseg4j.Dictionary; -import com.chenlb.mmseg4j.MMSeg; -import com.chenlb.mmseg4j.MaxWordSeg; -import com.chenlb.mmseg4j.Seg; -import com.chenlb.mmseg4j.SimpleSeg; -import com.chenlb.mmseg4j.Word; -import com.google.common.collect.ImmutableList; - -/** - * Reference from https://github.com/chenlb/mmseg4j-core - */ -public class MMSeg4JAnalyzer implements Analyzer { - - public static final List SUPPORT_MODES = ImmutableList.of( - "Simple", - "Complex", - "MaxWord" - ); - - private static final Dictionary DIC = Dictionary.getInstance(); - - private final Seg seg; - - public MMSeg4JAnalyzer(String mode) { - if (!SUPPORT_MODES.contains(mode)) { - throw new ConfigException( - "Unsupported segment mode '%s' for mmseg4j analyzer, " + - "the available values are %s", mode, SUPPORT_MODES); - } - int index = SUPPORT_MODES.indexOf(mode); - switch (index) { - case 0: - this.seg = new SimpleSeg(DIC); - break; - case 1: - this.seg = new ComplexSeg(DIC); - break; - case 2: - this.seg = new MaxWordSeg(DIC); - break; - default: - throw new AssertionError(String.format( - "Unsupported segment mode '%s'", mode)); - } - } - - @Override - public Set segment(String text) { - Set result = InsertionOrderUtil.newSet(); - MMSeg mmSeg = new MMSeg(new StringReader(text), this.seg); - try { - Word word; - while ((word = mmSeg.next()) != null) { - result.add(word.getString()); - } - } catch (Exception e) { - throw new HugeException("MMSeg4j segment text '%s' failed", - e, text); - } - return result; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/SmartCNAnalyzer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/SmartCNAnalyzer.java deleted file mode 100644 index f265ade214..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/analyzer/SmartCNAnalyzer.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.analyzer; - -import java.io.Reader; -import java.io.StringReader; -import java.util.Set; - -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.util.InsertionOrderUtil; -import org.apache.lucene.analysis.TokenStream; -import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; -import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; - -/** - * Reference from https://lucene.apache.org/core/8_11_2/analyzers-smartcn/index.html - */ -public class SmartCNAnalyzer implements Analyzer { - - private static final SmartChineseAnalyzer ANALYZER = - new SmartChineseAnalyzer(); - - public SmartCNAnalyzer(String mode) { - // pass - } - - @Override - public Set segment(String text) { - Set result = InsertionOrderUtil.newSet(); - Reader reader = new StringReader(text); - try (TokenStream tokenStream = ANALYZER.tokenStream("text", reader)) { - tokenStream.reset(); - CharTermAttribute term; - while (tokenStream.incrementToken()) { - term = tokenStream.getAttribute(CharTermAttribute.class); - result.add(term.toString()); - } - } catch (Exception e) { - throw new HugeException("SmartCN segment text '%s' failed", - e, text); - } - return result; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java index d1c8887238..09b08f3a00 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/AuthManager.java @@ -25,7 +25,7 @@ import javax.security.sasl.AuthenticationException; import org.apache.hugegraph.auth.SchemaDefine.AuthElement; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; public interface AuthManager { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/EntityManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/EntityManager.java index 4633625729..2a0b21ee62 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/EntityManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/EntityManager.java @@ -22,19 +22,19 @@ import java.util.Map; import java.util.function.Function; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.SchemaDefine.Entity; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.iterator.MapperIterator; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.HugeKeys; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeAccess.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeAccess.java index c40b67d57d..eef86c27b4 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeAccess.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeAccess.java @@ -27,9 +27,9 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.SchemaDefine.Relationship; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.type.define.DataType; import org.apache.hugegraph.util.E; import org.apache.tinkerpop.gremlin.structure.Edge; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeBelong.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeBelong.java index 7d668f3c19..4d0353aec7 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeBelong.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeBelong.java @@ -24,9 +24,9 @@ import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.SchemaDefine.Relationship; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Graph.Hidden; import org.apache.tinkerpop.gremlin.structure.T; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeGroup.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeGroup.java index defe5d130f..8e5ee9759e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeGroup.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeGroup.java @@ -25,9 +25,9 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.SchemaDefine.Entity; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.util.E; import org.apache.tinkerpop.gremlin.structure.Graph.Hidden; import org.apache.tinkerpop.gremlin.structure.T; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeProject.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeProject.java index faafcddded..10df4442f1 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeProject.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeProject.java @@ -18,8 +18,10 @@ package org.apache.hugegraph.auth; 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.Set; @@ -27,9 +29,9 @@ import org.apache.commons.lang.StringUtils; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.SchemaDefine.Entity; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; import org.apache.hugegraph.util.E; @@ -201,7 +203,6 @@ protected Object[] asArray() { return super.asArray(list); } - @SuppressWarnings("unchecked") @Override protected boolean property(String key, Object value) { if (super.property(key, value)) { @@ -212,7 +213,19 @@ protected boolean property(String key, Object value) { this.name = (String) value; break; case HugeProject.P.GRAPHS: - this.graphs = (Set) value; + if (value == null) { + this.graphs = null; + break; + } + E.checkArgument(value instanceof Collection, + "The graphs of project must be a collection"); + Set graphs = new HashSet<>(); + for (Object graph : (Collection) value) { + E.checkArgument(graph instanceof String, + "The graphs of project must contain only strings"); + graphs.add((String) graph); + } + this.graphs = graphs; break; case HugeProject.P.DESCRIPTIONS: this.description = (String) value; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeResource.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeResource.java index 74577c258e..7dbd152d4e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeResource.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeResource.java @@ -27,12 +27,12 @@ import java.util.Objects; import java.util.Set; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.auth.SchemaDefine.AuthElement; import org.apache.hugegraph.structure.HugeElement; import org.apache.hugegraph.traversal.optimize.TraversalUtil; -import org.apache.hugegraph.type.Nameable; -import org.apache.hugegraph.type.Typeable; +import org.apache.hugegraph.type.Namifiable; +import org.apache.hugegraph.type.Typifiable; import org.apache.hugegraph.util.JsonUtil; import org.apache.tinkerpop.gremlin.structure.Graph.Hidden; import org.apache.tinkerpop.gremlin.structure.Property; @@ -160,7 +160,7 @@ public boolean filter(ResourceObject resourceObject) { return this.filter((AuthElement) resourceObject.operated()); } if (resType.isSchema() || CHECK_NAME_RESS.contains(resType)) { - return this.filter((Nameable) resourceObject.operated()); + return this.filter((Namifiable) resourceObject.operated()); } } @@ -173,15 +173,15 @@ public boolean filter(ResourceObject resourceObject) { private boolean filter(AuthElement element) { assert this.type.match(element.type()); - if (element instanceof Nameable) { - return this.filter((Nameable) element); + if (element instanceof Namifiable) { + return this.filter((Namifiable) element); } return true; } - private boolean filter(Nameable element) { - assert !(element instanceof Typeable) || this.type.match( - ResourceType.from(((Typeable) element).type())); + private boolean filter(Namifiable element) { + assert !(element instanceof Typifiable) || this.type.match( + ResourceType.from(((Typifiable) element).type())); return this.matchLabel(element.name()); } @@ -288,7 +288,7 @@ public String toString() { public static boolean allowed(ResourceObject resourceObject) { // Allowed to access system(hidden) schema by anyone if (resourceObject.type().isSchema()) { - Nameable schema = (Nameable) resourceObject.operated(); + Namifiable schema = (Namifiable) resourceObject.operated(); return Hidden.isHidden(schema.name()); } @@ -302,7 +302,7 @@ public static HugeResource parseResource(String resource) { public boolean matchProperties(HugeResource other) { return matchProperties(other.properties); } - public static class NameObject implements Nameable { + public static class NameObject implements Namifiable { public static final NameObject ANY = new NameObject("*"); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeRole.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeRole.java index ee5955bbaf..78d1ff1197 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeRole.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeRole.java @@ -25,9 +25,9 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.SchemaDefine.Entity; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.util.E; import org.apache.tinkerpop.gremlin.structure.Graph.Hidden; import org.apache.tinkerpop.gremlin.structure.T; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java index 34ff5aa87d..70fbb2803e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeTarget.java @@ -23,12 +23,12 @@ import java.util.Map; import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.SchemaDefine.Entity; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.JsonUtil; import org.apache.tinkerpop.gremlin.structure.Graph.Hidden; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeUser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeUser.java index 8a90de0b17..c12946e7eb 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeUser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/HugeUser.java @@ -25,9 +25,9 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.SchemaDefine.Entity; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.util.E; import org.apache.tinkerpop.gremlin.structure.Graph.Hidden; import org.apache.tinkerpop.gremlin.structure.T; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/RelationshipManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/RelationshipManager.java index a1c440611e..fa7fd1299e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/RelationshipManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/RelationshipManager.java @@ -22,20 +22,20 @@ import java.util.Map; import java.util.function.Function; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.SchemaDefine.Relationship; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.iterator.MapperIterator; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.HugeType; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/ResourceObject.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/ResourceObject.java index b745e07fe7..1bbf66d9b4 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/ResourceObject.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/ResourceObject.java @@ -18,9 +18,9 @@ package org.apache.hugegraph.auth; import org.apache.hugegraph.auth.SchemaDefine.AuthElement; -import org.apache.hugegraph.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaElement; import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.type.Nameable; +import org.apache.hugegraph.type.Namifiable; import org.apache.hugegraph.util.E; public class ResourceObject { @@ -90,12 +90,12 @@ public static ResourceObject of(String graph, } public static ResourceObject of(String graphSpace, String graph, - ResourceType type, Nameable elem) { + ResourceType type, Namifiable elem) { return new ResourceObject<>(graphSpace, graph, type, elem); } public static ResourceObject of(String graph, ResourceType type, - Nameable elem) { + Namifiable elem) { return of("DEFAULT", graph, type, elem); } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/SchemaDefine.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/SchemaDefine.java index fe3556a59e..b5ea7331c7 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/SchemaDefine.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/SchemaDefine.java @@ -26,15 +26,15 @@ import java.util.Locale; import java.util.Map; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.HugeTarget.P; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; @@ -298,9 +298,9 @@ protected Object[] asArray(List list) { protected abstract Object[] asArray(); } - // NOTE: travis-ci fails if class Entity implements Nameable + // NOTE: travis-ci fails if class Entity implements Namifiable public abstract static class Entity extends AuthElement - implements org.apache.hugegraph.type.Nameable { + implements org.apache.hugegraph.type.Namifiable { private static final long serialVersionUID = 4113319546914811762L; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java index 02f5e9b385..cc677ec7b1 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java @@ -31,14 +31,14 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.HugeUser.P; import org.apache.hugegraph.auth.SchemaDefine.AuthElement; import org.apache.hugegraph.backend.cache.Cache; import org.apache.hugegraph.backend.cache.CacheManager; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.config.AuthOptions; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.type.define.Directions; @@ -109,7 +109,7 @@ public StandardAuthManager(HugeGraphParams graph) { this.access = new RelationshipManager<>(this.graph, HugeAccess.P.ACCESS, HugeAccess::fromEdge); - this.tokenGenerator = new TokenGenerator(config); + this.tokenGenerator = new TokenGenerator(config.get(AuthOptions.AUTH_TOKEN_SECRET)); LOG.info("Randomly generate a JWT secret key now"); this.ipWhiteList = new HashSet<>(); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java index d2d12b0f16..be019260bf 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2.java @@ -35,13 +35,13 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.auth.SchemaDefine.AuthElement; import org.apache.hugegraph.backend.cache.Cache; import org.apache.hugegraph.backend.cache.CacheManager; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.config.AuthOptions; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.meta.MetaManager; @@ -159,7 +159,7 @@ public StandardAuthManagerV2(HugeGraphParams graph) { this.pwdCache = this.cache("users_pwd", capacity, expired); this.tokenCache = this.cache("token", capacity, expired); - this.tokenGenerator = new TokenGenerator(config); + this.tokenGenerator = new TokenGenerator(config.get(AuthOptions.AUTH_TOKEN_SECRET)); LOG.info("Randomly generate a JWT secret key now"); this.ipWhiteList = new HashSet<>(); @@ -252,6 +252,12 @@ private void invalidateUserCache() { } private void invalidatePasswordCache(Id id) { + // TODO: invalidate the password and token caches on every Server replica, not only on the + // one that handled updateUser. AuthMetaManager.updateUser publishes no AuthEvent and + // nothing calls MetaManager.listenAuthEvent, so the other replicas on the shared PD catalog + // accept the old password until auth.cache_expire elapses. Publish a USER update event and + // listen for it here. The Helm chart (helm/hugegraph) documents the per-replica expiry as a + // limitation of admin password rotation; drop that note once fixed. this.pwdCache.invalidate(id); // Clear all tokenCache because can't get userId in it this.tokenCache.clear(); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/UserWithRole.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/UserWithRole.java index a32517bfd5..8b822056a2 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/UserWithRole.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/auth/UserWithRole.java @@ -17,7 +17,7 @@ package org.apache.hugegraph.auth; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; public class UserWithRole { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/BackendException.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/BackendException.java deleted file mode 100644 index a1f587866e..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/BackendException.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.backend; - -import org.apache.hugegraph.HugeException; - -public class BackendException extends HugeException { - - private static final long serialVersionUID = -1947589125372576298L; - - public BackendException(String message) { - super(message); - } - - public BackendException(String message, Throwable cause) { - super(message, cause); - } - - public BackendException(String message, Object... args) { - super(message, args); - } - - public BackendException(String message, Throwable cause, Object... args) { - super(message, cause, args); - } - - public BackendException(Throwable cause) { - this("Exception in backend", cause); - } - - public static final void check(boolean expression, String message, Object... args) - throws BackendException { - if (!expression) { - throw new BackendException(message, args); - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/LocalCounter.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/LocalCounter.java index 148703c072..6a166c7fbe 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/LocalCounter.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/LocalCounter.java @@ -21,8 +21,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.type.HugeType; public class LocalCounter { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/Transaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/Transaction.java index 1c7e18424d..4d93eb49be 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/Transaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/Transaction.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.backend; +import org.apache.hugegraph.exception.BackendException; + public interface Transaction { void commit() throws BackendException; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CacheListenerHolder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CacheListenerHolder.java index b4133679bb..f0c7c738eb 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CacheListenerHolder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CacheListenerHolder.java @@ -19,23 +19,37 @@ import org.apache.hugegraph.event.EventHub; import org.apache.hugegraph.event.EventListener; +import org.apache.hugegraph.util.Events; /* - * Listener lifetime must cover all active transactions for the graph. - * The holder is removed from the registry and unregistered from EventHub - * only when the last transaction releases it. + * Caches and listeners belong to the graph, including between request leases. + * Transaction close releases a lease; graph close disposes the holder. */ final class CacheListenerHolder { final EventListener listener; final EventHub hub; + final Runnable cleanup; // Must only be read or written inside ConcurrentMap.compute() for the // enclosing registry; ConcurrentHashMap.compute() serialises per-key access. int refCount; CacheListenerHolder(EventListener listener, EventHub hub) { + this(listener, hub, () -> { }); + } + + CacheListenerHolder(EventListener listener, EventHub hub, Runnable cleanup) { this.listener = listener; this.hub = hub; + this.cleanup = cleanup; this.refCount = 1; } + + void close() { + try { + this.hub.unlisten(Events.CACHE, this.listener); + } finally { + this.cleanup.run(); + } + } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CacheManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CacheManager.java index 30bffab814..67791483ff 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CacheManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CacheManager.java @@ -25,7 +25,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; import org.slf4j.Logger; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CacheNotifier.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CacheNotifier.java index e470f1067a..947ad1100e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CacheNotifier.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CacheNotifier.java @@ -17,7 +17,7 @@ package org.apache.hugegraph.backend.cache; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.type.HugeType; public interface CacheNotifier extends AutoCloseable { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedBackendStore.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedBackendStore.java index 3cc88bbf71..4a90745454 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedBackendStore.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedBackendStore.java @@ -22,10 +22,10 @@ import java.util.function.Function; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.query.ConditionQueryFlatten; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendFeatures; import org.apache.hugegraph.backend.store.BackendMutation; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedGraphTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedGraphTransaction.java index a6085d32c4..d814f52f55 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedGraphTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedGraphTransaction.java @@ -29,9 +29,9 @@ import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.backend.cache.CachedBackendStore.QueryId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryBatch; import org.apache.hugegraph.backend.query.QueryResultContext; import org.apache.hugegraph.backend.query.QueryResults; @@ -48,7 +48,7 @@ import org.apache.hugegraph.iterator.ExtendableIterator; import org.apache.hugegraph.iterator.ListIterator; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.HugeType; @@ -66,9 +66,8 @@ public final class CachedGraphTransaction extends GraphTransaction { private static final long AVG_EDGE_ENTRY_SIZE = 100L; /* - * Listener lifetime must cover all active transactions for the graph. - * The holder is removed from the registry and unregistered from EventHub - * only when the last transaction releases it. + * Retain graph caches and invalidation listeners between request leases. + * Only graph close disposes the holder. */ private static final ConcurrentMap GRAPH_CACHE_EVENT_LISTENERS = new ConcurrentHashMap<>(); @@ -158,10 +157,11 @@ private void listenChanges() { return false; }; BackendStoreProvider provider = this.store().provider(); + EventHub graphEventHub = this.params().graphEventHub(); String graphName = this.params().spaceGraphName(); StoreListenerHolder storeAcquired = STORE_EVENT_LISTENERS.compute( graphName, (key, existing) -> { - if (existing == null || existing.provider != provider) { + if (existing == null || existing.provider != provider || existing.hub != graphEventHub) { // Graph close/reopen creates a new provider for the // same graph name; replace the stale holder. Old // transactions skip decrement via identity check. @@ -169,7 +169,7 @@ private void listenChanges() { existing.provider.unlisten(existing.listener); } provider.listen(storeListener); - return new StoreListenerHolder(storeListener, provider); + return new StoreListenerHolder(storeListener, provider, graphEventHub); } existing.refCount++; return existing; @@ -223,7 +223,6 @@ private void listenChanges() { } return false; }; - EventHub graphEventHub = this.params().graphEventHub(); CacheListenerHolder acquired = GRAPH_CACHE_EVENT_LISTENERS.compute( graphName, (key, existing) -> { if (existing == null || existing.hub != graphEventHub) { @@ -231,11 +230,10 @@ private void listenChanges() { // same graph name; replace the stale holder. Old // transactions skip decrement via identity check. if (existing != null) { - existing.hub.unlisten(Events.CACHE, - existing.listener); + existing.close(); } graphEventHub.listen(Events.CACHE, listener); - return new CacheListenerHolder(listener, graphEventHub); + return new CacheListenerHolder(listener, graphEventHub, () -> this.clearCache(null, false)); } existing.refCount++; return existing; @@ -253,10 +251,6 @@ private void unlistenChanges() { return existing; } existing.refCount--; - if (existing.refCount == 0) { - existing.hub.unlisten(Events.CACHE, existing.listener); - return null; - } return existing; }); this.holder = null; @@ -269,16 +263,35 @@ private void unlistenChanges() { return existing; } existing.refCount--; - if (existing.refCount == 0) { - existing.provider.unlisten(existing.listener); - return null; - } return existing; }); this.storeHolder = null; } } + public static void closeGraph(HugeGraphParams params) { + String graphName = params.spaceGraphName(); + EventHub graphEventHub = params.graphEventHub(); + try { + GRAPH_CACHE_EVENT_LISTENERS.computeIfPresent(graphName, (key, existing) -> { + if (existing.hub != graphEventHub) { + return existing; + } + existing.close(); + return null; + }); + } finally { + STORE_EVENT_LISTENERS.computeIfPresent(graphName, (key, storeHolder) -> { + // The hub owns this graph generation even if the provider is pooled. + if (storeHolder.hub != graphEventHub) { + return storeHolder; + } + storeHolder.provider.unlisten(storeHolder.listener); + return null; + }); + } + } + private void notifyChanges(String action, HugeType type, Id[] ids) { EventHub graphEventHub = this.params().graphEventHub(); graphEventHub.notifyExcept(Events.CACHE, this.cacheEventListener, @@ -533,24 +546,24 @@ public void removeIndex(IndexLabel indexLabel) { } /* - * Listener lifetime must cover all active transactions for the graph. - * The holder is removed from the registry and unregistered from the - * BackendStoreProvider only when the last transaction releases it. - * Mirror of CacheListenerHolder for the store event path. + * Store invalidation outlives request leases just like the graph cache. + * The hub identifies the graph generation even when a provider is pooled. */ private static final class StoreListenerHolder { final EventListener listener; final BackendStoreProvider provider; + final EventHub hub; // Must only be read or written inside ConcurrentMap.compute() for the // enclosing registry; ConcurrentHashMap.compute() serialises per-key // access. int refCount; StoreListenerHolder(EventListener listener, - BackendStoreProvider provider) { + BackendStoreProvider provider, EventHub hub) { this.listener = listener; this.provider = provider; + this.hub = hub; this.refCount = 1; } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransaction.java index d393be7461..5ed7844e5a 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransaction.java @@ -25,8 +25,8 @@ import java.util.function.Consumer; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.store.BackendStore; import org.apache.hugegraph.backend.store.ram.IntObjectMap; import org.apache.hugegraph.backend.tx.SchemaTransaction; @@ -34,7 +34,7 @@ import org.apache.hugegraph.event.EventHub; import org.apache.hugegraph.event.EventListener; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaElement; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Events; @@ -44,9 +44,8 @@ public final class CachedSchemaTransaction extends SchemaTransaction { /* - * Listener lifetime must cover all active transactions for the graph. - * The holder is removed from the registry and unregistered from EventHub - * only when the last transaction releases it. + * Retain graph caches and invalidation listeners between request leases. + * Only graph close disposes the holder. */ private static final ConcurrentMap SCHEMA_CACHE_EVENT_LISTENERS = new ConcurrentHashMap<>(); @@ -93,7 +92,6 @@ public void close() { try { super.close(); } finally { - this.clearCache(false); this.unlistenChanges(); } } @@ -118,7 +116,7 @@ private void listenChanges() { } return false; }; - this.store().provider().listen(this.storeEventListener); + // Listen cache event: "cache"(invalid cache item) EventListener listener = event -> { @@ -150,12 +148,17 @@ private void listenChanges() { // same graph name; replace the stale holder. Old // transactions skip decrement via identity check. if (existing != null) { - existing.hub.unlisten(Events.CACHE, - existing.listener); + existing.close(); } schemaEventHub.listen(Events.CACHE, listener); - return new CacheListenerHolder(listener, - schemaEventHub); + this.store().provider().listen(this.storeEventListener); + return new CacheListenerHolder(listener, schemaEventHub, () -> { + try { + this.store().provider().unlisten(this.storeEventListener); + } finally { + this.clearCache(false); + } + }); } existing.refCount++; return existing; @@ -165,9 +168,6 @@ private void listenChanges() { } private void unlistenChanges() { - // Unlisten store event - this.store().provider().unlisten(this.storeEventListener); - // Unlisten cache event CacheListenerHolder ours = this.holder; if (ours != null) { @@ -177,11 +177,6 @@ private void unlistenChanges() { return existing; } existing.refCount--; - if (existing.refCount == 0) { - existing.hub.unlisten(Events.CACHE, - existing.listener); - return null; - } return existing; }); this.holder = null; @@ -189,6 +184,16 @@ private void unlistenChanges() { } } + public static void closeGraph(HugeGraphParams params) { + SCHEMA_CACHE_EVENT_LISTENERS.computeIfPresent(params.spaceGraphName(), (key, existing) -> { + if (existing.hub != params.schemaEventHub()) { + return existing; + } + existing.close(); + return null; + }); + } + private void notifyChanges(String action, HugeType type, Id id) { EventHub graphEventHub = this.params().schemaEventHub(); graphEventHub.notifyExcept(Events.CACHE, this.cacheEventListener, @@ -401,10 +406,10 @@ public V get(HugeType type, Id id) { assert false : id; return null; } - int key = (int) longId; - if (key >= this.size) { + if (longId >= this.size) { return null; } + int key = (int) longId; switch (type) { case PROPERTY_KEY: return this.pks.get(key); @@ -426,10 +431,10 @@ public void set(HugeType type, Id id, V value) { assert false : id; return; } - int key = (int) longId; - if (key >= this.size) { + if (longId >= this.size) { return; } + int key = (int) longId; this.setValue(type, key, value); } @@ -440,10 +445,10 @@ public void remove(HugeType type, Id id) { if (longId <= 0L) { return; } - int key = (int) longId; - if (key >= this.size) { + if (longId >= this.size) { return; } + int key = (int) longId; this.setValue(type, key, null); } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java index 74986b2e99..8affb5a30b 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java @@ -23,12 +23,13 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.store.ram.IntObjectMap; import org.apache.hugegraph.backend.tx.SchemaTransactionV2; import org.apache.hugegraph.config.CoreOptions; @@ -38,7 +39,7 @@ import org.apache.hugegraph.meta.MetaManager; import org.apache.hugegraph.meta.MetaManager.SchemaCacheClearEvent; import org.apache.hugegraph.perf.PerfUtil; -import org.apache.hugegraph.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaElement; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Events; @@ -76,6 +77,10 @@ public class CachedSchemaTransactionV2 extends SchemaTransactionV2 { private static final String SCHEMA_CACHE_CLEAR_SOURCE = UUID.randomUUID().toString(); + private static final ConcurrentMap + SCHEMA_CACHE_EVENT_LISTENERS = new ConcurrentHashMap<>(); + + private CacheListenerHolder holder; private final Cache idCache; private final Cache nameCache; @@ -113,8 +118,8 @@ private static Id generateId(HugeType type, String name) { return IdGenerator.of(type.string() + "-" + name); } + @Override public void close() { - this.clearCache(false); this.unlistenChanges(); } @@ -166,10 +171,9 @@ private void listenChanges() { } return false; }; - this.graphParams().loadGraphStore().provider().listen(this.storeEventListener); // Listen cache event: "cache"(invalid cache item) - this.cacheEventListener = event -> { + EventListener listener = event -> { LOG.debug("Graph {} received schema cache event: {}", this.graph(), event); Object[] args = event.args(); @@ -203,11 +207,28 @@ private void listenChanges() { return false; }; EventHub schemaEventHub = this.graphParams().schemaEventHub(); - if (!schemaEventHub.containsListener(Events.CACHE)) { - schemaEventHub.listen(Events.CACHE, this.cacheEventListener); - } - listenSchemaCacheClear(); + CacheListenerHolder acquired = SCHEMA_CACHE_EVENT_LISTENERS.compute( + this.graphParams().spaceGraphName(), (key, existing) -> { + if (existing == null || existing.hub != schemaEventHub) { + if (existing != null) { + existing.close(); + } + schemaEventHub.listen(Events.CACHE, listener); + this.graphParams().loadGraphStore().provider().listen(this.storeEventListener); + return new CacheListenerHolder(listener, schemaEventHub, () -> { + try { + this.graphParams().loadGraphStore().provider().unlisten(this.storeEventListener); + } finally { + this.clearCache(false); + } + }); + } + existing.refCount++; + return existing; + }); + this.holder = acquired; + this.cacheEventListener = acquired.listener; } private static void listenSchemaCacheClear() { @@ -278,13 +299,27 @@ private void resetCachedAllIfReachedCapacity() { } private void unlistenChanges() { - // Unlisten store event - this.graphParams().loadGraphStore().provider() - .unlisten(this.storeEventListener); + CacheListenerHolder ours = this.holder; + if (ours != null) { + SCHEMA_CACHE_EVENT_LISTENERS.compute(this.graphParams().spaceGraphName(), (key, existing) -> { + if (existing == ours) { + existing.refCount--; + } + return existing; + }); + this.holder = null; + this.cacheEventListener = null; + } + } - // Unlisten cache event - EventHub schemaEventHub = this.graphParams().schemaEventHub(); - schemaEventHub.unlisten(Events.CACHE, this.cacheEventListener); + public static void closeGraph(HugeGraphParams params) { + SCHEMA_CACHE_EVENT_LISTENERS.computeIfPresent(params.spaceGraphName(), (key, existing) -> { + if (existing.hub != params.schemaEventHub()) { + return existing; + } + existing.close(); + return null; + }); } private CachedTypes cachedTypes() { @@ -511,10 +546,10 @@ public V get(HugeType type, Id id) { assert false : id; return null; } - int key = (int) longId; - if (key >= this.size) { + if (longId >= this.size) { return null; } + int key = (int) longId; switch (type) { case PROPERTY_KEY: return this.pks.get(key); @@ -536,10 +571,10 @@ public void set(HugeType type, Id id, V value) { assert false : id; return; } - int key = (int) longId; - if (key >= this.size) { + if (longId >= this.size) { return; } + int key = (int) longId; switch (type) { case PROPERTY_KEY: this.pks.set(key, value); @@ -565,11 +600,11 @@ public void remove(HugeType type, Id id) { if (longId <= 0L) { return; } - int key = (int) longId; - V value = null; - if (key >= this.size) { + if (longId >= this.size) { return; } + int key = (int) longId; + V value = null; switch (type) { case PROPERTY_KEY: this.pks.set(key, value); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/LevelCache.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/LevelCache.java index 7f3e971314..d0ee73fa73 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/LevelCache.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/LevelCache.java @@ -20,7 +20,7 @@ import java.util.Iterator; import java.util.function.Consumer; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.iterator.ExtendableIterator; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/OffheapCache.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/OffheapCache.java index 7ed4efcd66..16f1f20f4e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/OffheapCache.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/OffheapCache.java @@ -24,15 +24,15 @@ import java.util.List; import java.util.function.Consumer; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.serializer.AbstractSerializer; import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; import org.apache.hugegraph.backend.serializer.BinarySerializer; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.HugeType; @@ -100,17 +100,23 @@ public long size() { @Override public boolean containsKey(Id id) { - return this.cache.containsKey(id); + return cacheableKey(id) && this.cache.containsKey(id); } @Override protected Object access(Id id) { + if (!cacheableKey(id)) { + return null; + } Value value = this.cache.get(id); return value == null ? null : value.value(); } @Override protected boolean write(Id id, Object value, long timeOffset) { + if (!cacheableKey(id)) { + return false; + } Value serializedValue = new Value(value); int serializedSize; try { @@ -144,7 +150,28 @@ protected boolean write(Id id, Object value, long timeOffset) { @Override protected void remove(Id id) { - this.cache.remove(id); + if (cacheableKey(id)) { + this.cache.remove(id); + } + } + + private static boolean cacheableKey(Id id) { + // Query text is a cache key, not a persistent graph ID. A large + // request must still reach the backend when its optional cache key + // exceeds the binary ID codec's byte limit. + if (!(id instanceof CachedBackendStore.QueryId)) { + return true; + } + int length = id.asString().length(); + // UTF-8 needs at most three bytes per UTF-16 code unit; + // a surrogate pair uses four bytes for two units. + if (length <= BytesBuffer.ID_LEN_MAX / 3) { + return true; + } + if (length > BytesBuffer.ID_LEN_MAX) { + return false; + } + return id.asBytes().length <= BytesBuffer.ID_LEN_MAX; } @Override diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/RamCache.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/RamCache.java index a5e8d162a8..b3364359dd 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/RamCache.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/RamCache.java @@ -25,7 +25,7 @@ import java.util.concurrent.locks.Lock; import java.util.function.Consumer; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.concurrent.KeyLock; import org.apache.hugegraph.perf.PerfUtil.Watched; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/EdgeId.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/EdgeId.java deleted file mode 100644 index 14116d8575..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/EdgeId.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.backend.id; - -import org.apache.hugegraph.exception.NotFoundException; -import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.structure.HugeVertex; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.define.Directions; -import org.apache.hugegraph.type.define.HugeKeys; -import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.StringEncoding; - -/** - * Class used to format and parse id of edge, the edge id consists of: - * EdgeId = { source-vertex-id > direction > parentEdgeLabelId > subEdgeLabelId - * > sortKeys > target-vertex-id } - * NOTE: - *

1. for edges with edgeLabelType = NORMAL: edgeLabelId = parentEdgeLabelId = subEdgeLabelId; - * for edges with edgeLabelType = PARENT: edgeLabelId = subEdgeLabelId, parentEdgeLabelId = - * edgeLabelId.fatherId - *

2.if we use `entry.type()` which is IN or OUT as a part of id, - * an edge's id will be different due to different directions (belongs - * to 2 owner vertex) - */ -public class EdgeId implements Id { - - public static final HugeKeys[] KEYS = new HugeKeys[]{ - HugeKeys.OWNER_VERTEX, - HugeKeys.DIRECTION, - HugeKeys.LABEL, - HugeKeys.SUB_LABEL, - HugeKeys.SORT_VALUES, - HugeKeys.OTHER_VERTEX - }; - - protected final Id ownerVertexId; - protected final Id edgeLabelId; - protected final Id subLabelId; - protected final Id otherVertexId; - protected final Directions direction; - protected final boolean directed; - protected String sortValues; - protected String cache; - - public EdgeId(HugeVertex ownerVertex, Directions direction, - Id edgeLabelId, Id subLabelId, String sortValues, HugeVertex otherVertex) { - this(ownerVertex.id(), direction, edgeLabelId, subLabelId, - sortValues, otherVertex.id()); - } - - public EdgeId(Id ownerVertexId, Directions direction, Id edgeLabelId, Id subLabelId, - String sortValues, Id otherVertexId) { - this(ownerVertexId, direction, edgeLabelId, subLabelId, - sortValues, otherVertexId, false); - } - - public EdgeId(Id ownerVertexId, Directions direction, Id edgeLabelId, Id subLabelId, - String sortValues, Id otherVertexId, boolean directed) { - this.ownerVertexId = ownerVertexId; - this.direction = direction; - this.edgeLabelId = edgeLabelId; - this.subLabelId = subLabelId; - this.sortValues = sortValues; - this.otherVertexId = otherVertexId; - this.directed = directed; - this.cache = null; - } - - @Watched - public EdgeId switchDirection() { - Directions direction = this.direction.opposite(); - return new EdgeId(this.otherVertexId, direction, this.edgeLabelId, this.subLabelId, - this.sortValues, this.ownerVertexId, this.directed); - } - - public EdgeId directed(boolean directed) { - return new EdgeId(this.ownerVertexId, this.direction, this.edgeLabelId, this.subLabelId, - this.sortValues, this.otherVertexId, directed); - } - - private Id sourceVertexId() { - return this.direction == Directions.OUT ? - this.ownerVertexId : - this.otherVertexId; - } - - private Id targetVertexId() { - return this.direction == Directions.OUT ? - this.otherVertexId : - this.ownerVertexId; - } - - public Id ownerVertexId() { - return this.ownerVertexId; - } - - public Id edgeLabelId() { - return this.edgeLabelId; - } - - public Id subLabelId() { - return this.subLabelId; - } - - public Directions direction() { - return this.direction; - } - - public byte directionCode() { - return directionToCode(this.direction); - } - - public String sortValues() { - return this.sortValues; - } - - public Id otherVertexId() { - return this.otherVertexId; - } - - @Override - public Object asObject() { - return this.asString(); - } - - @Override - public String asString() { - if (this.cache != null) { - return this.cache; - } - if (this.directed) { - this.cache = SplicingIdGenerator.concat( - IdUtil.writeString(this.ownerVertexId), - this.direction.type().string(), - IdUtil.writeLong(this.edgeLabelId), - IdUtil.writeLong(this.subLabelId), - this.sortValues, - IdUtil.writeString(this.otherVertexId)); - } else { - this.cache = SplicingIdGenerator.concat( - IdUtil.writeString(this.sourceVertexId()), - IdUtil.writeLong(this.edgeLabelId), - IdUtil.writeLong(this.subLabelId), - this.sortValues, - IdUtil.writeString(this.targetVertexId())); - } - return this.cache; - } - - @Override - public long asLong() { - throw new UnsupportedOperationException(); - } - - @Override - public byte[] asBytes() { - return StringEncoding.encode(this.asString()); - } - - @Override - public int length() { - return this.asString().length(); - } - - @Override - public IdType type() { - return IdType.EDGE; - } - - @Override - public int compareTo(Id other) { - return this.asString().compareTo(other.asString()); - } - - @Override - public int hashCode() { - if (this.directed) { - return this.ownerVertexId.hashCode() ^ - this.direction.hashCode() ^ - this.edgeLabelId.hashCode() ^ - this.subLabelId.hashCode() ^ - this.sortValues.hashCode() ^ - this.otherVertexId.hashCode(); - } else { - return this.sourceVertexId().hashCode() ^ - this.edgeLabelId.hashCode() ^ - this.subLabelId.hashCode() ^ - this.sortValues.hashCode() ^ - this.targetVertexId().hashCode(); - } - } - - @Override - public boolean equals(Object object) { - if (!(object instanceof EdgeId)) { - return false; - } - EdgeId other = (EdgeId) object; - if (this.directed) { - return this.ownerVertexId.equals(other.ownerVertexId) && - this.direction == other.direction && - this.edgeLabelId.equals(other.edgeLabelId) && - this.subLabelId.equals(other.subLabelId) && - this.sortValues.equals(other.sortValues) && - this.otherVertexId.equals(other.otherVertexId); - } else { - return this.sourceVertexId().equals(other.sourceVertexId()) && - this.edgeLabelId.equals(other.edgeLabelId) && - this.subLabelId.equals(other.subLabelId) && - this.sortValues.equals(other.sortValues) && - this.targetVertexId().equals(other.targetVertexId()); - } - } - - @Override - public String toString() { - return this.asString(); - } - - public static byte directionToCode(Directions direction) { - return direction.type().code(); - } - - public static Directions directionFromCode(byte code) { - return Directions.convert(HugeType.fromCode(code)); - } - - public static boolean isOutDirectionFromCode(byte code) { - return code == HugeType.EDGE_OUT.code(); - } - - public static EdgeId parse(String id) throws NotFoundException { - return parse(id, false); - } - - public static EdgeId parse(String id, boolean returnNullIfError) - throws NotFoundException { - String[] idParts = SplicingIdGenerator.split(id); - if (!(idParts.length == 5 || idParts.length == 6)) { - if (returnNullIfError) { - return null; - } - throw new NotFoundException("Edge id must be formatted as 5~6 " + - "parts, but got %s parts: '%s'", - idParts.length, id); - } - try { - if (idParts.length == 5) { - Id ownerVertexId = IdUtil.readString(idParts[0]); - Id edgeLabelId = IdUtil.readLong(idParts[1]); - Id subLabelId = IdUtil.readLong(idParts[2]); - String sortValues = idParts[3]; - Id otherVertexId = IdUtil.readString(idParts[4]); - return new EdgeId(ownerVertexId, Directions.OUT, edgeLabelId, subLabelId, - sortValues, otherVertexId); - } else { - assert idParts.length == 6; - Id ownerVertexId = IdUtil.readString(idParts[0]); - HugeType direction = HugeType.fromString(idParts[1]); - Id edgeLabelId = IdUtil.readLong(idParts[2]); - Id subLabelId = IdUtil.readLong(idParts[3]); - String sortValues = idParts[4]; - Id otherVertexId = IdUtil.readString(idParts[5]); - return new EdgeId(ownerVertexId, Directions.convert(direction), - edgeLabelId, subLabelId, sortValues, otherVertexId); - } - } catch (Throwable e) { - if (returnNullIfError) { - return null; - } - throw new NotFoundException("Invalid format of edge id '%s'", - e, id); - } - } - - public static Id parseStoredString(String id) { - String[] idParts = split(id); - E.checkArgument(idParts.length == 5, "Invalid id format: %s", id); - Id ownerVertexId = IdUtil.readStoredString(idParts[0]); - Id edgeLabelId = IdGenerator.ofStoredString(idParts[1], IdType.LONG); - Id subLabelId = IdGenerator.ofStoredString(idParts[2], IdType.LONG); - String sortValues = idParts[3]; - Id otherVertexId = IdUtil.readStoredString(idParts[4]); - return new EdgeId(ownerVertexId, Directions.OUT, edgeLabelId, subLabelId, - sortValues, otherVertexId); - } - - public static String asStoredString(Id id) { - EdgeId eid = (EdgeId) id; - return SplicingIdGenerator.concat( - IdUtil.writeStoredString(eid.sourceVertexId()), - IdGenerator.asStoredString(eid.edgeLabelId()), - IdGenerator.asStoredString(eid.subLabelId()), - eid.sortValues(), - IdUtil.writeStoredString(eid.targetVertexId())); - } - - public static String concat(String... ids) { - return SplicingIdGenerator.concat(ids); - } - - public static String[] split(Id id) { - return EdgeId.split(id.asString()); - } - - public static String[] split(String id) { - return SplicingIdGenerator.split(id); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/Id.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/Id.java deleted file mode 100644 index 077aa43dbb..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/Id.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.backend.id; - -import org.apache.hugegraph.util.E; - -public interface Id extends Comparable { - - int UUID_LENGTH = 16; - - Object asObject(); - - String asString(); - - long asLong(); - - byte[] asBytes(); - - int length(); - - IdType type(); - - default boolean number() { - return this.type() == IdType.LONG; - } - - default boolean uuid() { - return this.type() == IdType.UUID; - } - - default boolean string() { - return this.type() == IdType.STRING; - } - - default boolean edge() { - return this.type() == IdType.EDGE; - } - - enum IdType { - - UNKNOWN, - LONG, - UUID, - STRING, - EDGE; - - public char prefix() { - if (this == UNKNOWN) { - return 'N'; - } - return this.name().charAt(0); - } - - public static IdType valueOfPrefix(String id) { - E.checkArgument(id != null && !id.isEmpty(), "Invalid id '%s'", id); - switch (id.charAt(0)) { - case 'L': - return IdType.LONG; - case 'U': - return IdType.UUID; - case 'S': - return IdType.STRING; - case 'E': - return IdType.EDGE; - default: - return IdType.UNKNOWN; - } - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/IdGenerator.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/IdGenerator.java deleted file mode 100644 index 9f1bcc56fc..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/IdGenerator.java +++ /dev/null @@ -1,471 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.backend.id; - -import java.util.Objects; -import java.util.UUID; - -import org.apache.hugegraph.backend.id.Id.IdType; -import org.apache.hugegraph.backend.serializer.BytesBuffer; -import org.apache.hugegraph.structure.HugeVertex; -import org.apache.hugegraph.util.Bytes; -import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.LongEncoding; -import org.apache.hugegraph.util.NumericUtil; -import org.apache.hugegraph.util.StringEncoding; - -public abstract class IdGenerator { - - public static final Id ZERO = IdGenerator.of(0L); - - public abstract Id generate(HugeVertex vertex); - - public static Id of(String id) { - return new StringId(id); - } - - public static Id of(UUID id) { - return new UuidId(id); - } - - public static Id of(String id, boolean uuid) { - return uuid ? new UuidId(id) : new StringId(id); - } - - public static Id of(long id) { - return new LongId(id); - } - - public static Id of(Object id) { - if (id instanceof Id) { - return (Id) id; - } else if (id instanceof String) { - return of((String) id); - } else if (id instanceof Number) { - return of(((Number) id).longValue()); - } else if (id instanceof UUID) { - return of((UUID) id); - } - return new ObjectId(id); - } - - public static Id of(byte[] bytes, IdType type) { - switch (type) { - case LONG: - return new LongId(bytes); - case UUID: - return new UuidId(bytes); - case STRING: - return new StringId(bytes); - default: - throw new AssertionError("Invalid id type " + type); - } - } - - public static Id ofStoredString(String id, IdType type) { - switch (type) { - case LONG: - return of(LongEncoding.decodeSignedB64(id)); - case UUID: - byte[] bytes = StringEncoding.decodeBase64(id); - return of(bytes, IdType.UUID); - case STRING: - return of(id); - default: - throw new AssertionError("Invalid id type " + type); - } - } - - public static String asStoredString(Id id) { - switch (id.type()) { - case LONG: - return LongEncoding.encodeSignedB64(id.asLong()); - case UUID: - return StringEncoding.encodeBase64(id.asBytes()); - case STRING: - return id.asString(); - default: - throw new AssertionError("Invalid id type " + id.type()); - } - } - - public static IdType idType(Id id) { - if (id instanceof LongId) { - return IdType.LONG; - } - if (id instanceof UuidId) { - return IdType.UUID; - } - if (id instanceof StringId) { - return IdType.STRING; - } - if (id instanceof EdgeId) { - return IdType.EDGE; - } - return IdType.UNKNOWN; - } - - public static int compareType(Id id1, Id id2) { - return idType(id1).ordinal() - idType(id2).ordinal(); - } - - /****************************** id defines ******************************/ - - public static class StringId implements Id { - - protected String id; - protected byte[] bytes; - - public StringId(String id) { - E.checkArgument(id != null && !id.isEmpty(), - "The id can't be null or empty"); - this.id = id; - this.bytes = null; - } - - public StringId(byte[] bytes) { - E.checkArgument(bytes != null && bytes.length > 0, - "The id bytes can't be null or empty"); - this.bytes = bytes; - this.id = null; - } - - @Override - public IdType type() { - return IdType.STRING; - } - - @Override - public Object asObject() { - return this.asString(); - } - - @Override - public String asString() { - if (this.id == null) { - assert this.bytes != null; - this.id = StringEncoding.decode(this.bytes); - } - return this.id; - } - - @Override - public long asLong() { - return Long.parseLong(this.asString()); - } - - @Override - public byte[] asBytes() { - if (this.bytes == null) { - assert this.id != null; - this.bytes = StringEncoding.encode(this.id); - } - return this.bytes; - } - - @Override - public int length() { - return this.asString().length(); - } - - @Override - public int compareTo(Id other) { - int cmp = compareType(this, other); - if (cmp != 0) { - return cmp; - } - if (this.id != null) { - return this.id.compareTo(other.asString()); - } else { - return Bytes.compare(this.bytes, other.asBytes()); - } - } - - @Override - public int hashCode() { - return this.asString().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof StringId)) { - return false; - } - StringId other = (StringId) obj; - if (this.id != null) { - return this.id.equals(other.asString()); - } else if (other.bytes == null) { - return this.asString().equals(other.asString()); - } else { - assert this.bytes != null; - return Bytes.equals(this.bytes, other.asBytes()); - } - } - - @Override - public String toString() { - return this.asString(); - } - } - - public static class LongId extends Number implements Id { - - private static final long serialVersionUID = -7732461469037400190L; - - protected Long id; - - public LongId(long id) { - this.id = id; - } - - public LongId(byte[] bytes) { - this.id = NumericUtil.bytesToLong(bytes); - } - - @Override - public IdType type() { - return IdType.LONG; - } - - @Override - public Object asObject() { - return this.id; - } - - @Override - public String asString() { - // TODO: encode with base64 - return Long.toString(this.id); - } - - @Override - public long asLong() { - return this.id; - } - - @Override - public byte[] asBytes() { - return NumericUtil.longToBytes(this.id); - } - - @Override - public int length() { - return Long.BYTES; - } - - @Override - public int compareTo(Id other) { - int cmp = compareType(this, other); - if (cmp != 0) { - return cmp; - } - return Long.compare(this.id, other.asLong()); - } - - @Override - public int hashCode() { - return Long.hashCode(this.id); - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof Number)) { - return false; - } - return this.id == ((Number) other).longValue(); - } - - @Override - public String toString() { - return String.valueOf(this.id); - } - - @Override - public int intValue() { - return this.id.intValue(); - } - - @Override - public long longValue() { - return this.id; - } - - @Override - public float floatValue() { - return this.id; - } - - @Override - public double doubleValue() { - return this.id; - } - } - - public static class UuidId implements Id { - - protected UUID uuid; - - public UuidId(String string) { - this(StringEncoding.uuid(string)); - } - - public UuidId(byte[] bytes) { - this(fromBytes(bytes)); - } - - public UuidId(UUID uuid) { - E.checkArgument(uuid != null, "The uuid can't be null"); - this.uuid = uuid; - } - - @Override - public IdType type() { - return IdType.UUID; - } - - @Override - public Object asObject() { - return this.uuid; - } - - @Override - public String asString() { - return this.uuid.toString(); - } - - @Override - public long asLong() { - throw new UnsupportedOperationException(); - } - - @Override - public byte[] asBytes() { - BytesBuffer buffer = BytesBuffer.allocate(16); - buffer.writeLong(this.uuid.getMostSignificantBits()); - buffer.writeLong(this.uuid.getLeastSignificantBits()); - return buffer.bytes(); - } - - private static UUID fromBytes(byte[] bytes) { - E.checkArgument(bytes != null, "The UUID can't be null"); - BytesBuffer buffer = BytesBuffer.wrap(bytes); - long high = buffer.readLong(); - long low = buffer.readLong(); - return new UUID(high, low); - } - - @Override - public int length() { - return UUID_LENGTH; - } - - @Override - public int compareTo(Id other) { - E.checkNotNull(other, "compare id"); - int cmp = compareType(this, other); - if (cmp != 0) { - return cmp; - } - return this.uuid.compareTo(((UuidId) other).uuid); - } - - @Override - public int hashCode() { - return this.uuid.hashCode(); - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof UuidId)) { - return false; - } - return this.uuid.equals(((UuidId) other).uuid); - } - - @Override - public String toString() { - return this.uuid.toString(); - } - } - - /** - * This class is just used by backend store for wrapper object as Id - */ - public static class ObjectId implements Id { - - protected Object object; - - public ObjectId(Object object) { - E.checkNotNull(object, "object"); - this.object = object; - } - - @Override - public IdType type() { - return IdType.UNKNOWN; - } - - @Override - public Object asObject() { - return this.object; - } - - @Override - public String asString() { - throw new UnsupportedOperationException(); - } - - @Override - public long asLong() { - throw new UnsupportedOperationException(); - } - - @Override - public byte[] asBytes() { - throw new UnsupportedOperationException(); - } - - @Override - public int length() { - throw new UnsupportedOperationException(); - } - - @Override - public int compareTo(Id o) { - throw new UnsupportedOperationException(); - } - - @Override - public int hashCode() { - return this.object.hashCode(); - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof ObjectId)) { - return false; - } - return Objects.equals(this.object, ((ObjectId) other).object); - } - - @Override - public String toString() { - return this.object.toString(); - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/IdUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/IdUtil.java deleted file mode 100644 index 90cd98d4b7..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/IdUtil.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.backend.id; - -import java.nio.ByteBuffer; - -import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.backend.id.Id.IdType; -import org.apache.hugegraph.backend.serializer.BytesBuffer; - -public final class IdUtil { - - public static String writeStoredString(Id id) { - String idString; - switch (id.type()) { - case LONG: - case STRING: - case UUID: - idString = IdGenerator.asStoredString(id); - break; - case EDGE: - idString = EdgeId.asStoredString(id); - break; - default: - throw new AssertionError("Invalid id type " + id.type()); - } - return id.type().prefix() + idString; - } - - public static Id readStoredString(String id) { - IdType type = IdType.valueOfPrefix(id); - String idContent = id.substring(1); - switch (type) { - case LONG: - case STRING: - case UUID: - return IdGenerator.ofStoredString(idContent, type); - case EDGE: - return EdgeId.parseStoredString(idContent); - default: - throw new IllegalArgumentException("Invalid id: " + id); - } - } - - public static Object writeBinString(Id id) { - int len = id.edge() ? BytesBuffer.BUF_EDGE_ID : id.length() + 1; - BytesBuffer buffer = BytesBuffer.allocate(len).writeId(id); - buffer.forReadWritten(); - return buffer.asByteBuffer(); - } - - public static Id readBinString(Object id) { - BytesBuffer buffer = BytesBuffer.wrap((ByteBuffer) id); - return buffer.readId(); - } - - public static String writeString(Id id) { - String idString = id.asString(); - return id.type().prefix() + idString; - } - - public static Id readString(String id) { - IdType type = IdType.valueOfPrefix(id); - String idContent = id.substring(1); - switch (type) { - case LONG: - return IdGenerator.of(Long.parseLong(idContent)); - case STRING: - case UUID: - return IdGenerator.of(idContent, type == IdType.UUID); - case EDGE: - return EdgeId.parse(idContent); - default: - throw new IllegalArgumentException("Invalid id: " + id); - } - } - - public static String writeLong(Id id) { - return String.valueOf(id.asLong()); - } - - public static Id readLong(String id) { - return IdGenerator.of(Long.parseLong(id)); - } - - public static String escape(char splitor, char escape, String... values) { - int length = values.length + 4; - for (String value : values) { - length += value.length(); - } - StringBuilder escaped = new StringBuilder(length); - // Do escape for every item in values - for (String value : values) { - if (escaped.length() > 0) { - escaped.append(splitor); - } - - if (value.indexOf(splitor) == -1) { - escaped.append(value); - continue; - } - - // Do escape for current item - for (int i = 0, n = value.length(); i < n; i++) { - char ch = value.charAt(i); - if (ch == splitor) { - escaped.append(escape); - } - escaped.append(ch); - } - } - return escaped.toString(); - } - - public static String[] unescape(String id, String splitor, String escape) { - /* - * Note that the `splitter`/`escape` maybe special characters in regular - * expressions, but this is a frequently called method, for faster - * execution, we forbid the use of special characters as delimiter - * or escape sign. - * - * The `limit` param -1 in split method can ensure empty string be - * split to a part. - */ - String[] parts = id.split("(?'; - private static final char ID_SPLITOR = ':'; - private static final char NAME_SPLITOR = '!'; - - public static final String ESCAPE_STR = String.valueOf(ESCAPE); - public static final String IDS_SPLITOR_STR = String.valueOf(IDS_SPLITOR); - public static final String ID_SPLITOR_STR = String.valueOf(ID_SPLITOR); - - /****************************** id generate ******************************/ - - /** - * Generate a string id of HugeVertex from Vertex name - */ - @Override - public Id generate(HugeVertex vertex) { - /* - * Hash for row-key which will be evenly distributed. - * We can also use LongEncoding.encode() to encode the int/long hash - * if needed. - * id = String.format("%s%s%s", HashUtil.hash(id), ID_SPLITOR, id); - */ - // TODO: use binary Id with binary fields instead of string id - return splicing(vertex.schemaLabel().id().asString(), vertex.name()); - } - - /** - * Concat multiple ids into one composite id with IDS_SPLITOR - * - * @param ids the string id values to be concatted - * @return concatted string value - */ - public static String concat(String... ids) { - // NOTE: must support string id when using this method - return IdUtil.escape(IDS_SPLITOR, ESCAPE, ids); - } - - /** - * Split a composite id into multiple ids with IDS_SPLITOR - * - * @param ids the string id value to be splitted - * @return splitted string values - */ - public static String[] split(String ids) { - return IdUtil.unescape(ids, IDS_SPLITOR_STR, ESCAPE_STR); - } - - /** - * Concat property values with NAME_SPLITOR - * - * @param values the property values to be concatted - * @return concatted string value - */ - public static String concatValues(List values) { - // Convert the object list to string array - int valuesSize = values.size(); - String[] parts = new String[valuesSize]; - for (int i = 0; i < valuesSize; i++) { - parts[i] = values.get(i).toString(); - } - return IdUtil.escape(NAME_SPLITOR, ESCAPE, parts); - } - - /** - * Concat property values with NAME_SPLITOR - * - * @param values the property values to be concatted - * @return concatted string value - */ - public static String concatValues(Object... values) { - return concatValues(Arrays.asList(values)); - } - - /** - * Concat multiple parts into a single id with ID_SPLITOR - * - * @param parts the string id values to be spliced - * @return spliced id object - */ - public static Id splicing(String... parts) { - String escaped = IdUtil.escape(ID_SPLITOR, ESCAPE, parts); - return IdGenerator.of(escaped); - } - - /** - * Parse a single id into multiple parts with ID_SPLITOR - * - * @param id the id object to be parsed - * @return parsed string id parts - */ - public static String[] parse(Id id) { - return IdUtil.unescape(id.asString(), ID_SPLITOR_STR, ESCAPE_STR); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/IdHolder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/IdHolder.java index ab64959a01..d5933cb0ff 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/IdHolder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/IdHolder.java @@ -23,9 +23,10 @@ import java.util.function.Function; import org.apache.commons.lang.NotImplementedException; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.backend.query.QueryBatch; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.iterator.CIter; import org.apache.hugegraph.iterator.Metadatable; @@ -231,16 +232,25 @@ public PageIds fetchNext(String page, long batchSize) { @Override public Set all() { + Set ids; try { - Set ids = this.fetcher.apply(this.remaining()); + ids = this.fetcher.apply(this.remaining()); if (this.currentBatch != null) { ids.addAll(this.getFromCurrentBatch(Query.NO_LIMIT).ids()); } this.count += ids.size(); - return ids; - } finally { - this.close(); + } catch (Throwable failure) { + try { + this.close(); + } catch (Throwable closing) { + if (closing != failure) { + failure.addSuppressed(closing); + } + } + throw QueryBatch.propagate(failure); } + this.close(); + return ids; } public PageIds peekNext(long size) { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageEntryIterator.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageEntryIterator.java index 17996bae9e..832754afb8 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageEntryIterator.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageEntryIterator.java @@ -19,7 +19,7 @@ import java.util.Iterator; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryBatch; import org.apache.hugegraph.backend.query.QueryBatch.BatchIterator; import org.apache.hugegraph.exception.NotSupportException; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageIds.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageIds.java index 879821f3db..e0e23c5668 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageIds.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageIds.java @@ -19,7 +19,7 @@ import java.util.Set; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import com.google.common.collect.ImmutableSet; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageInfo.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageInfo.java index 3662b6e660..2a913e32ab 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageInfo.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageInfo.java @@ -19,8 +19,8 @@ import java.util.Iterator; -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.iterator.Metadatable; import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageState.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageState.java index d0cdf74bce..4f08e87dbc 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageState.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/PageState.java @@ -17,8 +17,8 @@ package org.apache.hugegraph.backend.page; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.StringEncoding; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/QueryList.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/QueryList.java index d3d8e6960f..7609196982 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/QueryList.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/QueryList.java @@ -22,13 +22,13 @@ import java.util.List; import java.util.Set; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.page.IdHolder.BatchIdHolder; import org.apache.hugegraph.backend.page.IdHolder.FixedIdHolder; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.ConditionQuery.OptimizedType; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.ConditionQuery.OptimizedType; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryBatch; import org.apache.hugegraph.backend.query.QueryBatch.BatchIterator; import org.apache.hugegraph.backend.query.QueryResults; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/SortByCountIdHolderList.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/SortByCountIdHolderList.java index eeac978672..e9b85bd257 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/SortByCountIdHolderList.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/SortByCountIdHolderList.java @@ -22,9 +22,9 @@ import java.util.Map; import java.util.Set; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.page.IdHolder.FixedIdHolder; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.util.CollectionUtil; import org.apache.hugegraph.util.InsertionOrderUtil; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Aggregate.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Aggregate.java deleted file mode 100644 index e81f0a9d73..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Aggregate.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.backend.query; - -import java.util.Iterator; -import java.util.function.BiFunction; - -import org.apache.tinkerpop.gremlin.util.NumberHelper; - -public class Aggregate { - - private final AggregateFunc func; - private final String column; - - public Aggregate(AggregateFunc func, String column) { - this.func = func; - this.column = column; - } - - public AggregateFunc func() { - return this.func; - } - - public String column() { - return this.column; - } - - public boolean countAll() { - return this.func == AggregateFunc.COUNT && this.column == null; - } - - public Number reduce(Iterator results) { - return this.func.reduce(results); - } - - public Number defaultValue() { - return this.func.defaultValue(); - } - - @Override - public String toString() { - return String.format("%s(%s)", this.func.string(), - this.column == null ? "*" : this.column); - } - - public enum AggregateFunc { - - COUNT("count", 0L, NumberHelper::add), - MAX("max", -Double.MAX_VALUE, NumberHelper::max), - MIN("min", Double.MAX_VALUE, NumberHelper::min), - AVG("avg", 0D, NumberHelper::add), - SUM("sum", 0L, NumberHelper::add); - - private final String name; - private final Number defaultValue; - private final BiFunction merger; - - AggregateFunc(String name, Number defaultValue, - BiFunction merger) { - this.name = name; - this.defaultValue = defaultValue; - this.merger = merger; - } - - public String string() { - return this.name; - } - - public Number defaultValue() { - return this.defaultValue; - } - - public Number reduce(Iterator results) { - Number number; - long count = 0L; - if (results.hasNext()) { - number = results.next(); - count++; - } else { - return this.defaultValue; - } - while (results.hasNext()) { - number = this.merger.apply(number, results.next()); - count++; - } - if (this == AVG) { - number = NumberHelper.div(number, count); - } - return number; - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Condition.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Condition.java deleted file mode 100644 index e77f2f0b1e..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Condition.java +++ /dev/null @@ -1,912 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.backend.query; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.BiFunction; -import java.util.function.BiPredicate; - -import org.apache.commons.lang.ArrayUtils; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.store.Shard; -import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.structure.HugeProperty; -import org.apache.hugegraph.type.define.HugeKeys; -import org.apache.hugegraph.util.Bytes; -import org.apache.hugegraph.util.DateUtil; -import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.NumericUtil; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; - -public abstract class Condition { - - public enum ConditionType { - NONE, - RELATION, - AND, - OR, - NOT - } - - public enum RelationType implements BiPredicate { - - EQ("==", RelationType::equals), - - GT(">", (v1, v2) -> { - return compare(v1, v2) > 0; - }), - - GTE(">=", (v1, v2) -> { - return compare(v1, v2) >= 0; - }), - - LT("<", (v1, v2) -> { - return compare(v1, v2) < 0; - }), - - LTE("<=", (v1, v2) -> { - return compare(v1, v2) <= 0; - }), - - NEQ("!=", (v1, v2) -> { - return compare(v1, v2) != 0; - }), - - IN("in", null, Collection.class, (v1, v2) -> { - assert v2 != null; - return ((Collection) v2).contains(v1); - }), - - NOT_IN("notin", null, Collection.class, (v1, v2) -> { - assert v2 != null; - return !((Collection) v2).contains(v1); - }), - - PREFIX("prefix", Id.class, Id.class, (v1, v2) -> { - assert v2 != null; - return v1 != null && Bytes.prefixWith(((Id) v2).asBytes(), - ((Id) v1).asBytes()); - }), - - TEXT_CONTAINS("textcontains", String.class, String.class, (v1, v2) -> { - // TODO: support collection-property textcontains - return v1 != null && ((String) v1).contains((String) v2); - }), - - TEXT_CONTAINS_ANY("textcontainsany", String.class, Collection.class, (v1, v2) -> { - assert v2 != null; - if (v1 == null) { - return false; - } - - @SuppressWarnings("unchecked") - Collection words = (Collection) v2; - - for (String word : words) { - if (((String) v1).contains(word)) { - return true; - } - } - return false; - }), - - CONTAINS("contains", Collection.class, null, (v1, v2) -> { - assert v2 != null; - return v1 != null && ((Collection) v1).contains(v2); - }), - - CONTAINS_VALUE("containsv", Map.class, null, (v1, v2) -> { - assert v2 != null; - return v1 != null && ((Map) v1).containsValue(v2); - }), - - CONTAINS_KEY("containsk", Map.class, null, (v1, v2) -> { - assert v2 != null; - return v1 != null && ((Map) v1).containsKey(v2); - }), - - SCAN("scan", (v1, v2) -> { - assert v2 != null; - /* - * TODO: we still have no way to determine accurately for all - * backend scan implementations. - */ - return true; - }); - - private static final Set RANGE_TYPES = - ImmutableSet.of(GT, GTE, LT, LTE); - - private static final Set UNFLATTEN_TYPES = - ImmutableSet.of(IN, NOT_IN, TEXT_CONTAINS_ANY); - - private final String operator; - private final BiFunction tester; - private final Class v1Class; - private final Class v2Class; - - RelationType(String op, - BiFunction tester) { - this(op, null, null, tester); - } - - RelationType(String op, Class v1Class, Class v2Class, - BiFunction tester) { - this.operator = op; - this.tester = tester; - this.v1Class = v1Class; - this.v2Class = v2Class; - } - - public String string() { - return this.operator; - } - - /** - * Determine two values of any type equal - * - * @param first is actual value - * @param second is value in query condition - * @return true if equal, otherwise false - */ - private static boolean equals(final Object first, - final Object second) { - assert second != null; - if (first instanceof Id) { - if (second instanceof String) { - return second.equals(((Id) first).asString()); - } else if (second instanceof Long) { - return second.equals(((Id) first).asLong()); - } - } else if (second instanceof Number) { - return compare(first, second) == 0; - } else if (second.getClass().isArray()) { - return ArrayUtils.isEquals(first, second); - } - - return Objects.equals(first, second); - } - - /** - * Determine two numbers equal - * - * @param first is actual value, might be Number/Date or String, It is - * probably that the `first` is serialized to String. - * @param second is value in query condition, must be Number/Date/Boolean - * @return the value 0 if first is numerically equal to second; - * a value less than 0 if first is numerically less than - * second; and a value greater than 0 if first is - * numerically greater than second. - */ - private static int compare(final Object first, final Object second) { - assert second != null; - if (second instanceof Number) { - return NumericUtil.compareNumber(first == null ? 0 : first, - (Number) second); - } else if (second instanceof Date) { - return compareDate(first, (Date) second); - } else if (second instanceof Boolean) { - return compareBoolean(first, (Boolean) second); - } - - throw new IllegalArgumentException(String.format( - "Can't compare between %s(%s) and %s(%s)", first, - first == null ? null : first.getClass().getSimpleName(), - second, second.getClass().getSimpleName())); - } - - private static int compareDate(Object first, Date second) { - if (first == null) { - first = DateUtil.DATE_ZERO; - } - if (first instanceof Date) { - return ((Date) first).compareTo(second); - } - - throw new IllegalArgumentException(String.format( - "Can't compare between %s(%s) and %s(%s)", - first, first.getClass().getSimpleName(), - second, second.getClass().getSimpleName())); - } - - private static int compareBoolean(Object first, Boolean second) { - if (first instanceof Boolean) { - return Boolean.compare((Boolean) first, second); - } - - throw new IllegalArgumentException(String.format( - "Can't compare between %s(%s) and %s(%s)", - first, first == null ? null : - first.getClass().getSimpleName(), - second, second.getClass().getSimpleName())); - } - - private void checkBaseType(Object value, Class clazz) { - if (!clazz.isInstance(value)) { - String valueClass = value == null ? "null" : - value.getClass().getSimpleName(); - E.checkArgument(false, - "Can't execute `%s` on type %s, expect %s", - this.operator, valueClass, - clazz.getSimpleName()); - } - } - - private void checkValueType(Object value, Class clazz) { - if (!clazz.isInstance(value)) { - String valueClass = value == null ? "null" : - value.getClass().getSimpleName(); - E.checkArgument(false, - "Can't test '%s'(%s) for `%s`, expect %s", - value, valueClass, this.operator, - clazz.getSimpleName()); - } - } - - @Override - public boolean test(Object first, Object second) { - E.checkState(this.tester != null, "Can't test %s", this.name()); - E.checkArgument(second != null, - "Can't test null value for `%s`", this.operator); - if (this.v1Class != null) { - this.checkBaseType(first, this.v1Class); - } - if (this.v2Class != null) { - this.checkValueType(second, this.v2Class); - } - return this.tester.apply(first, second); - } - - public boolean isRangeType() { - return RANGE_TYPES.contains(this); - } - - public boolean isSearchType() { - return this == TEXT_CONTAINS || this == TEXT_CONTAINS_ANY; - } - - public boolean isSecondaryType() { - return this == EQ; - } - } - - public abstract ConditionType type(); - - public abstract boolean isSysprop(); - - public abstract List relations(); - - public abstract boolean test(Object value); - - public abstract boolean test(HugeElement element); - - public abstract Condition copy(); - - public abstract Condition replace(Relation from, Relation to); - - public Condition and(Condition other) { - return new And(this, other); - } - - public Condition or(Condition other) { - return new Or(this, other); - } - - public boolean isRelation() { - return this.type() == ConditionType.RELATION; - } - - public boolean isLogic() { - return this.type() == ConditionType.AND || - this.type() == ConditionType.OR || - this.type() == ConditionType.NOT; - } - - public boolean isFlattened() { - return this.isRelation(); - } - - public static Condition and(Condition left, Condition right) { - return new And(left, right); - } - - public static Condition or(Condition left, Condition right) { - return new Or(left, right); - } - - public static Condition not(Condition condition) { - return new Not(condition); - } - - public static Relation eq(HugeKeys key, Object value) { - return new SyspropRelation(key, RelationType.EQ, value); - } - - public static Relation gt(HugeKeys key, Object value) { - return new SyspropRelation(key, RelationType.GT, value); - } - - public static Relation gte(HugeKeys key, Object value) { - return new SyspropRelation(key, RelationType.GTE, value); - } - - public static Relation lt(HugeKeys key, Object value) { - return new SyspropRelation(key, RelationType.LT, value); - } - - public static Relation lte(HugeKeys key, Object value) { - return new SyspropRelation(key, RelationType.LTE, value); - } - - public static Relation neq(HugeKeys key, Object value) { - return new SyspropRelation(key, RelationType.NEQ, value); - } - - public static Condition in(HugeKeys key, List value) { - return new SyspropRelation(key, RelationType.IN, value); - } - - public static Condition nin(HugeKeys key, List value) { - return new SyspropRelation(key, RelationType.NOT_IN, value); - } - - public static Condition prefix(HugeKeys key, Id value) { - return new SyspropRelation(key, RelationType.PREFIX, value); - } - - public static Condition containsValue(HugeKeys key, Object value) { - return new SyspropRelation(key, RelationType.CONTAINS_VALUE, value); - } - - public static Condition containsKey(HugeKeys key, Object value) { - return new SyspropRelation(key, RelationType.CONTAINS_KEY, value); - } - - public static Condition contains(HugeKeys key, Object value) { - return new SyspropRelation(key, RelationType.CONTAINS, value); - } - - public static Condition scan(String start, String end) { - Shard value = new Shard(start, end, 0); - return new SyspropRelation(HugeKeys.ID, RelationType.SCAN, value); - } - - public static Relation eq(Id key, Object value) { - return new UserpropRelation(key, RelationType.EQ, value); - } - - public static Relation gt(Id key, Object value) { - return new UserpropRelation(key, RelationType.GT, value); - } - - public static Relation gte(Id key, Object value) { - return new UserpropRelation(key, RelationType.GTE, value); - } - - public static Relation lt(Id key, Object value) { - return new UserpropRelation(key, RelationType.LT, value); - } - - public static Relation lte(Id key, Object value) { - return new UserpropRelation(key, RelationType.LTE, value); - } - - public static Relation neq(Id key, Object value) { - return new UserpropRelation(key, RelationType.NEQ, value); - } - - public static Relation in(Id key, List value) { - return new UserpropRelation(key, RelationType.IN, value); - } - - public static Relation nin(Id key, List value) { - return new UserpropRelation(key, RelationType.NOT_IN, value); - } - - public static Relation textContains(Id key, String word) { - return new UserpropRelation(key, RelationType.TEXT_CONTAINS, word); - } - - public static Relation textContainsAny(Id key, Set words) { - return new UserpropRelation(key, RelationType.TEXT_CONTAINS_ANY, words); - } - - public static Condition contains(Id key, Object value) { - return new UserpropRelation(key, RelationType.CONTAINS, value); - } - - /** - * Condition defines - */ - public abstract static class BinCondition extends Condition { - - private Condition left; - private Condition right; - - public BinCondition(Condition left, Condition right) { - E.checkNotNull(left, "left condition"); - E.checkNotNull(right, "right condition"); - this.left = left; - this.right = right; - } - - public Condition left() { - return this.left; - } - - public Condition right() { - return this.right; - } - - @Override - public boolean isSysprop() { - return this.left.isSysprop() && this.right.isSysprop(); - } - - @Override - public List relations() { - List list = new ArrayList<>(this.left.relations()); - list.addAll(this.right.relations()); - return list; - } - - @Override - public Condition replace(Relation from, Relation to) { - this.left = this.left.replace(from, to); - this.right = this.right.replace(from, to); - return this; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(64); - sb.append(this.left).append(' '); - sb.append(this.type().name()).append(' '); - sb.append(this.right); - return sb.toString(); - } - - @Override - public boolean equals(Object object) { - if (!(object instanceof BinCondition)) { - return false; - } - BinCondition other = (BinCondition) object; - return this.type().equals(other.type()) && - this.left().equals(other.left()) && - this.right().equals(other.right()); - } - - @Override - public int hashCode() { - return this.type().hashCode() ^ - this.left().hashCode() ^ - this.right().hashCode(); - } - } - - public static class And extends BinCondition { - - public And(Condition left, Condition right) { - super(left, right); - } - - @Override - public ConditionType type() { - return ConditionType.AND; - } - - @Override - public boolean test(Object value) { - return this.left().test(value) && this.right().test(value); - } - - @Override - public boolean test(HugeElement element) { - return this.left().test(element) && this.right().test(element); - } - - @Override - public Condition copy() { - return new And(this.left().copy(), this.right().copy()); - } - } - - public static class Or extends BinCondition { - - public Or(Condition left, Condition right) { - super(left, right); - } - - @Override - public ConditionType type() { - return ConditionType.OR; - } - - @Override - public boolean test(Object value) { - return this.left().test(value) || this.right().test(value); - } - - @Override - public boolean test(HugeElement element) { - return this.left().test(element) || this.right().test(element); - } - - @Override - public Condition copy() { - return new Or(this.left().copy(), this.right().copy()); - } - } - - public static class Not extends Condition { - - Condition condition; - - public Not(Condition condition) { - super(); - this.condition = condition; - } - - public Condition condition() { - return condition; - } - - @Override - public ConditionType type() { - return ConditionType.NOT; - } - - @Override - public boolean test(Object value) { - return !this.condition.test(value); - } - - @Override - public boolean test(HugeElement element) { - return !this.condition.test(element); - } - - @Override - public Condition copy() { - return new Not(this.condition.copy()); - } - - @Override - public boolean isSysprop() { - return this.condition.isSysprop(); - } - - @Override - public List relations() { - return new ArrayList(this.condition.relations()); - } - - @Override - public Condition replace(Relation from, Relation to) { - this.condition = this.condition.replace(from, to); - return this; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(64); - sb.append(this.type().name()).append(' '); - sb.append(this.condition); - return sb.toString(); - } - - @Override - public boolean equals(Object object) { - if (!(object instanceof Not)) { - return false; - } - Not other = (Not) object; - return this.type().equals(other.type()) && - this.condition.equals(other.condition()); - } - - @Override - public int hashCode() { - return this.type().hashCode() ^ - this.condition.hashCode(); - } - } - - public abstract static class Relation extends Condition { - - // Relational operator (like: =, >, <, in, ...) - protected RelationType relation; - // Single-type value or a list of single-type value - protected Object value; - - // The key serialized(code/string) by backend store. - protected Object serialKey; - // The value serialized(code/string) by backend store. - protected Object serialValue; - - @Override - public ConditionType type() { - return ConditionType.RELATION; - } - - public RelationType relation() { - return this.relation; - } - - public Object value() { - return this.value; - } - - public void value(Object value) { - this.value = value; - } - - public void serialKey(Object key) { - this.serialKey = key; - } - - public Object serialKey() { - return this.serialKey != null ? this.serialKey : this.key(); - } - - public void serialValue(Object value) { - this.serialValue = value; - } - - public Object serialValue() { - return this.serialValue != null ? this.serialValue : this.value(); - } - - @Override - public boolean test(Object value) { - return this.relation.test(value, this.value()); - } - - @Override - public boolean isFlattened() { - return !RelationType.UNFLATTEN_TYPES.contains(this.relation); - } - - @Override - public List relations() { - return ImmutableList.of(this); - } - - @Override - public Condition replace(Relation from, Relation to) { - if (this == from) { - return to; - } else { - return this; - } - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(64); - sb.append(this.key()).append(' '); - sb.append(this.relation.string()).append(' '); - sb.append(this.value); - return sb.toString(); - } - - @Override - public boolean equals(Object object) { - if (!(object instanceof Relation)) { - return false; - } - Relation other = (Relation) object; - return this.relation().equals(other.relation()) && - this.key().equals(other.key()) && - this.value().equals(other.value()); - } - - @Override - public int hashCode() { - return this.type().hashCode() ^ - this.relation().hashCode() ^ - this.key().hashCode() ^ - this.value().hashCode(); - } - - @Override - public abstract boolean isSysprop(); - - public abstract Object key(); - - @Override - public abstract Relation copy(); - } - - public static class SyspropRelation extends Relation { - - private final HugeKeys key; - - public SyspropRelation(HugeKeys key, Object value) { - this(key, RelationType.EQ, value); - } - - public SyspropRelation(HugeKeys key, RelationType op, Object value) { - E.checkNotNull(op, "relation type"); - this.key = key; - this.relation = op; - this.value = value; - } - - @Override - public HugeKeys key() { - return this.key; - } - - @Override - public boolean isSysprop() { - return true; - } - - @Override - public boolean test(HugeElement element) { - E.checkNotNull(element, "element"); - Object value = element.sysprop(this.key); - return this.relation.test(value, this.value()); - } - - @Override - public Relation copy() { - Relation clone = new SyspropRelation(this.key, this.relation(), - this.value); - clone.serialKey(this.serialKey); - clone.serialValue(this.serialValue); - return clone; - } - } - - public static class FlattenSyspropRelation extends SyspropRelation { - - public FlattenSyspropRelation(SyspropRelation relation) { - super(relation.key(), relation.relation(), relation.value()); - } - - @Override - public boolean isFlattened() { - return true; - } - } - - public static class UserpropRelation extends Relation { - - // Id of property key - private final Id key; - - public UserpropRelation(Id key, Object value) { - this(key, RelationType.EQ, value); - } - - public UserpropRelation(Id key, RelationType op, Object value) { - E.checkNotNull(op, "relation type"); - this.key = key; - this.relation = op; - this.value = value; - } - - @Override - public Id key() { - return this.key; - } - - @Override - public boolean isSysprop() { - return false; - } - - @Override - public boolean test(HugeElement element) { - HugeProperty prop = element.getProperty(this.key); - Object value = prop != null ? prop.value() : null; - if (value == null) { - /* - * Fix #611 - * TODO: It's possible some scenes can't be returned false - * directly, such as: EQ with p1 == null, it should be returned - * true, but the query has(p, null) is not allowed by - * TraversalUtil.validPredicateValue(). - */ - return false; - } - return this.relation.test(value, this.value()); - } - - @Override - public Relation copy() { - Relation clone = new UserpropRelation(this.key, this.relation(), - this.value); - clone.serialKey(this.serialKey); - clone.serialValue(this.serialValue); - return clone; - } - } - - public static class RangeConditions { - - private Object keyEq = null; - private Object keyMin = null; - private boolean keyMinEq = false; - private Object keyMax = null; - private boolean keyMaxEq = false; - - public RangeConditions(List conditions) { - for (Condition c : conditions) { - Relation r = (Relation) c; - switch (r.relation()) { - case EQ: - this.keyEq = r.value(); - break; - case GTE: - this.keyMinEq = true; - this.keyMin = r.value(); - break; - case GT: - this.keyMin = r.value(); - break; - case LTE: - this.keyMaxEq = true; - this.keyMax = r.value(); - break; - case LT: - this.keyMax = r.value(); - break; - default: - E.checkArgument(false, "Unsupported relation '%s'", - r.relation()); - } - } - } - - public Object keyEq() { - return this.keyEq; - } - - public Object keyMin() { - return this.keyMin; - } - - public Object keyMax() { - return this.keyMax; - } - - public boolean keyMinEq() { - return this.keyMinEq; - } - - public boolean keyMaxEq() { - return this.keyMaxEq; - } - - public boolean hasRange() { - return this.keyMin != null || this.keyMax != null; - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQuery.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQuery.java deleted file mode 100644 index 6da8da23db..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQuery.java +++ /dev/null @@ -1,971 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.backend.query; - -import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; -import org.apache.hugegraph.backend.query.Condition.Relation; -import org.apache.hugegraph.backend.query.Condition.RelationType; -import org.apache.hugegraph.backend.query.serializer.QueryAdapter; -import org.apache.hugegraph.backend.query.serializer.QueryIdAdapter; -import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.structure.HugeProperty; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.define.HugeKeys; -import org.apache.hugegraph.util.CollectionUtil; -import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.InsertionOrderUtil; -import org.apache.hugegraph.util.LongEncoding; -import org.apache.hugegraph.util.NumericUtil; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -public class ConditionQuery extends IdQuery { - - public static final char INDEX_SYM_MIN = '\u0000'; - public static final String INDEX_SYM_ENDING = "\u0000"; - public static final String INDEX_SYM_NULL = "\u0001"; - public static final String INDEX_SYM_EMPTY = "\u0002"; - public static final char INDEX_SYM_MAX = '\u0003'; - - // Note: here we use "new String" to distinguish normal string code - public static final String INDEX_VALUE_NULL = ""; - public static final String INDEX_VALUE_EMPTY = ""; - - public static final Set IGNORE_SYM_SET; - - static { - List list = new ArrayList<>(INDEX_SYM_MAX - INDEX_SYM_MIN); - for (char ch = INDEX_SYM_MIN; ch <= INDEX_SYM_MAX; ch++) { - list.add(String.valueOf(ch)); - } - IGNORE_SYM_SET = ImmutableSet.copyOf(list); - } - - private static final List EMPTY_CONDITIONS = ImmutableList.of(); - - private static final Gson gson = new GsonBuilder() - .registerTypeAdapter(Condition.class, new QueryAdapter()) - .registerTypeAdapter(Id.class, new QueryIdAdapter()) - .setDateFormat("yyyy-MM-dd HH:mm:ss.SSS") - .create(); - - // Conditions will be contacted with `and` by default - private List conditions = EMPTY_CONDITIONS; - - private OptimizedType optimizedType = OptimizedType.NONE; - private ResultsFilter resultsFilter = null; - private Element2IndexValueMap element2IndexValueMap = null; - - public ConditionQuery(HugeType resultType) { - super(resultType); - } - - public ConditionQuery(HugeType resultType, Query originQuery) { - super(resultType, originQuery); - } - - public ConditionQuery query(Condition condition) { - // Query by id (HugeGraph-259) - if (condition instanceof Relation) { - Relation relation = (Relation) condition; - if (relation.key().equals(HugeKeys.ID) && - relation.relation() == RelationType.EQ) { - E.checkArgument(relation.value() instanceof Id, - "Invalid id value '%s'", relation.value()); - super.query((Id) relation.value()); - return this; - } - } - - if (this.conditions == EMPTY_CONDITIONS) { - this.conditions = InsertionOrderUtil.newList(); - } - this.conditions.add(condition); - return this; - } - - public ConditionQuery query(List conditions) { - for (Condition condition : conditions) { - this.query(condition); - } - return this; - } - - public ConditionQuery eq(HugeKeys key, Object value) { - // Filter value by key - return this.query(Condition.eq(key, value)); - } - - public ConditionQuery gt(HugeKeys key, Object value) { - return this.query(Condition.gt(key, value)); - } - - public ConditionQuery gte(HugeKeys key, Object value) { - return this.query(Condition.gte(key, value)); - } - - public ConditionQuery lt(HugeKeys key, Object value) { - return this.query(Condition.lt(key, value)); - } - - public ConditionQuery lte(HugeKeys key, Object value) { - return this.query(Condition.lte(key, value)); - } - - public ConditionQuery neq(HugeKeys key, Object value) { - return this.query(Condition.neq(key, value)); - } - - public ConditionQuery prefix(HugeKeys key, Id value) { - return this.query(Condition.prefix(key, value)); - } - - public ConditionQuery key(HugeKeys key, Object value) { - return this.query(Condition.containsKey(key, value)); - } - - public ConditionQuery scan(String start, String end) { - return this.query(Condition.scan(start, end)); - } - - @Override - public int conditionsSize() { - return this.conditions.size(); - } - - @Override - public Collection conditions() { - return Collections.unmodifiableList(this.conditions); - } - - public void resetConditions(List conditions) { - this.conditions = conditions; - } - - public void resetConditions() { - this.conditions = EMPTY_CONDITIONS; - } - - public void recordIndexValue(Id propertyId, Id id, Object indexValue) { - this.element2IndexValueMap().addIndexValue(propertyId, id, indexValue); - } - - public void selectedIndexField(Id indexField) { - this.element2IndexValueMap().selectedIndexField(indexField); - } - - public void removeElementLeftIndex(Id elementId) { - if (this.element2IndexValueMap == null) { - return; - } - this.element2IndexValueMap.removeElementLeftIndex(elementId); - } - - public boolean existLeftIndex(Id elementId) { - return this.getLeftIndexOfElement(elementId) != null; - } - - public Set getLeftIndexOfElement(Id elementId) { - if (this.element2IndexValueMap == null) { - return null; - } - return this.element2IndexValueMap.getLeftIndex(elementId); - } - - private Element2IndexValueMap element2IndexValueMap() { - if (this.element2IndexValueMap == null) { - this.element2IndexValueMap = new Element2IndexValueMap(); - } - return this.element2IndexValueMap; - } - - public List relations() { - List relations = new ArrayList<>(); - for (Condition c : this.conditions) { - relations.addAll(c.relations()); - } - return relations; - } - - public Relation relation(Id key) { - for (Relation r : this.relations()) { - if (r.key().equals(key)) { - return r; - } - } - return null; - } - - public boolean containsLabelOrUserpropRelation() { - for (Condition c : this.conditions) { - while (c instanceof Condition.Not) { - c = ((Condition.Not) c).condition(); - } - if (c.isLogic()) { - Condition.BinCondition binCondition = - (Condition.BinCondition) c; - ConditionQuery query = new ConditionQuery(HugeType.EDGE); - query.query(binCondition.left()); - query.query(binCondition.right()); - if (query.containsLabelOrUserpropRelation()) { - return true; - } - } else { - Condition.Relation r = (Condition.Relation) c; - if (r.key().equals(HugeKeys.LABEL) || - c instanceof Condition.UserpropRelation) { - return true; - } - } - } - return false; - } - - @Watched - public T condition(Object key) { - List valuesEQ = InsertionOrderUtil.newList(); - List valuesIN = InsertionOrderUtil.newList(); - for (Condition c : this.conditions) { - if (c.isRelation()) { - Condition.Relation r = (Condition.Relation) c; - if (r.key().equals(key)) { - if (r.relation() == RelationType.EQ) { - valuesEQ.add(r.value()); - } else if (r.relation() == RelationType.IN) { - Object value = r.value(); - assert value instanceof List; - valuesIN.add(value); - } - } - } - } - if (valuesEQ.isEmpty() && valuesIN.isEmpty()) { - return null; - } - if (valuesEQ.size() == 1 && valuesIN.isEmpty()) { - @SuppressWarnings("unchecked") - T value = (T) valuesEQ.get(0); - return value; - } - if (valuesEQ.isEmpty() && valuesIN.size() == 1) { - @SuppressWarnings("unchecked") - T value = (T) valuesIN.get(0); - return value; - } - - boolean initialized = false; - Set intersectValues = InsertionOrderUtil.newSet(); - for (Object value : valuesEQ) { - List valueAsList = ImmutableList.of(value); - if (!initialized) { - intersectValues.addAll(valueAsList); - initialized = true; - } else { - CollectionUtil.intersectWithModify(intersectValues, - valueAsList); - } - } - for (Object value : valuesIN) { - @SuppressWarnings("unchecked") - List valueAsList = (List) value; - if (!initialized) { - intersectValues.addAll(valueAsList); - initialized = true; - } else { - CollectionUtil.intersectWithModify(intersectValues, - valueAsList); - } - } - - if (intersectValues.isEmpty()) { - return null; - } - E.checkState(intersectValues.size() == 1, - "Illegal key '%s' with more than one value: %s", - key, intersectValues); - @SuppressWarnings("unchecked") - T value = (T) intersectValues.iterator().next(); - return value; - } - - public void unsetCondition(Object key) { - this.conditions.removeIf(c -> c.isRelation() && ((Relation) c).key().equals(key)); - } - - public boolean containsCondition(HugeKeys key) { - for (Condition c : this.conditions) { - if (c.isRelation()) { - Condition.Relation r = (Condition.Relation) c; - if (r.key().equals(key)) { - return true; - } - } - } - return false; - } - - public boolean containsCondition(Condition.RelationType type) { - for (Relation r : this.relations()) { - if (r.relation().equals(type)) { - return true; - } - } - return false; - } - - public boolean containsScanCondition() { - return this.containsCondition(Condition.RelationType.SCAN); - } - - public boolean containsRelation(HugeKeys key, Condition.RelationType type) { - for (Relation r : this.relations()) { - if (r.key().equals(key) && r.relation().equals(type)) { - return true; - } - } - return false; - } - - public boolean containsRelation(Condition.RelationType type) { - for (Relation r : this.relations()) { - if (r.relation().equals(type)) { - return true; - } - } - return false; - } - - public boolean containsScanRelation() { - return this.containsRelation(Condition.RelationType.SCAN); - } - - public boolean containsContainsCondition(Id key) { - for (Relation r : this.relations()) { - if (r.key().equals(key)) { - return r.relation().equals(RelationType.CONTAINS) || - r.relation().equals(RelationType.TEXT_CONTAINS); - } - } - return false; - } - - public boolean allSysprop() { - for (Condition c : this.conditions) { - if (!c.isSysprop()) { - return false; - } - } - return true; - } - - public boolean allRelation() { - for (Condition c : this.conditions) { - if (!c.isRelation()) { - return false; - } - } - return true; - } - - public List syspropConditions() { - this.checkFlattened(); - List conds = new ArrayList<>(); - for (Condition c : this.conditions) { - if (c.isSysprop()) { - conds.add(c); - } - } - return conds; - } - - public List syspropConditions(HugeKeys key) { - this.checkFlattened(); - List conditions = new ArrayList<>(); - for (Condition condition : this.conditions) { - Relation relation = (Relation) condition; - if (relation.key().equals(key)) { - conditions.add(relation); - } - } - return conditions; - } - - public List userpropConditions() { - this.checkFlattened(); - List conds = new ArrayList<>(); - for (Condition c : this.conditions) { - if (!c.isSysprop()) { - conds.add(c); - } - } - return conds; - } - - public List userpropConditions(Id key) { - this.checkFlattened(); - List conditions = new ArrayList<>(); - for (Condition condition : this.conditions) { - Relation relation = (Relation) condition; - if (relation.key().equals(key)) { - conditions.add(relation); - } - } - return conditions; - } - - public List userpropRelations() { - List relations = new ArrayList<>(); - for (Relation r : this.relations()) { - if (!r.isSysprop()) { - relations.add(r); - } - } - return relations; - } - - public void resetUserpropConditions() { - this.conditions.removeIf(condition -> !condition.isSysprop()); - } - - public Set userpropKeys() { - Set keys = new LinkedHashSet<>(); - for (Relation r : this.relations()) { - if (!r.isSysprop()) { - Condition.UserpropRelation ur = (Condition.UserpropRelation) r; - keys.add(ur.key()); - } - } - return keys; - } - - /** - * This method is only used for secondary index scenario, - * its relation must be EQ - * - * @param fields the user property fields - * @return the corresponding user property serial values of fields - */ - public String userpropValuesString(List fields) { - List values = new ArrayList<>(fields.size()); - for (Id field : fields) { - boolean got = false; - for (Relation r : this.userpropRelations()) { - if (r.key().equals(field) && !r.isSysprop()) { - E.checkState(r.relation == RelationType.EQ || - r.relation == RelationType.CONTAINS, - "Method userpropValues(List) only " + - "used for secondary index, " + - "relation must be EQ or CONTAINS, but got %s", - r.relation()); - values.add(r.serialValue()); - got = true; - } - } - if (!got) { - throw new BackendException( - "No such userprop named '%s' in the query '%s'", - field, this); - } - } - return concatValues(values); - } - - public Set userpropValues(Id field) { - Set values = new HashSet<>(); - for (Relation r : this.userpropRelations()) { - if (r.key().equals(field)) { - values.add(r.serialValue()); - } - } - return values; - } - - public Object userpropValue(Id field) { - Set values = this.userpropValues(field); - if (values.isEmpty()) { - return null; - } - E.checkState(values.size() == 1, - "Expect one user-property value of field '%s', " + - "but got '%s'", field, values.size()); - return values.iterator().next(); - } - - public boolean hasRangeCondition() { - // NOTE: we need to judge all the conditions, including the nested - for (Condition.Relation r : this.relations()) { - if (r.relation().isRangeType()) { - return true; - } - } - return false; - } - - public boolean hasSearchCondition() { - // NOTE: we need to judge all the conditions, including the nested - for (Condition.Relation r : this.relations()) { - if (r.relation().isSearchType()) { - return true; - } - } - return false; - } - - public boolean hasSecondaryCondition() { - // NOTE: we need to judge all the conditions, including the nested - for (Condition.Relation r : this.relations()) { - if (r.relation().isSecondaryType()) { - return true; - } - } - return false; - } - - public boolean hasNeqCondition() { - // NOTE: we need to judge all the conditions, including the nested - for (Condition.Relation r : this.relations()) { - if (r.relation() == RelationType.NEQ) { - return true; - } - } - return false; - } - - public boolean matchUserpropKeys(List keys) { - Set conditionKeys = this.userpropKeys(); - return !keys.isEmpty() && conditionKeys.containsAll(keys); - } - - @Override - public ConditionQuery copy() { - ConditionQuery query = (ConditionQuery) super.copy(); - query.originQuery(this); - if (query.conditions != EMPTY_CONDITIONS) { - query.conditions = InsertionOrderUtil.newList(this.conditions); - } - query.optimizedType = OptimizedType.NONE; - query.resultsFilter = null; - - return query; - } - - public ConditionQuery copyAndResetUnshared() { - ConditionQuery query = this.copy(); - // These fields should not be shared by multiple sub-query - query.optimizedType = OptimizedType.NONE; - query.resultsFilter = null; - return query; - } - - public Condition.Relation copyRelationAndUpdateQuery(Object key) { - Condition.Relation copyRes = null; - for (int i = 0; i < this.conditions.size(); i++) { - Condition c = this.conditions.get(i); - if (c.isRelation()) { - Condition.Relation r = (Condition.Relation) c; - if (r.key().equals(key)) { - copyRes = r.copy(); - this.conditions.set(i, copyRes); - break; - } - } - } - E.checkArgument(copyRes != null, "Failed to copy Condition.Relation: %s", key); - return copyRes; - } - - @Override - public boolean test(HugeElement element) { - return this.test(element, this.resultsFilter); - } - - public boolean test(HugeElement element, ResultsFilter filter) { - if (!this.ids().isEmpty() && !super.test(element)) { - return false; - } - - /* - * Currently results-filter is used to filter unmatched results returned - * by search index, and there may be multiple results-filter for every - * sub-query like within() + Text.contains(). - * We can't use sub-query results-filter here for fresh element which is - * not committed to backend store, because it's not from a sub-query. - */ - if (filter != null && !element.fresh()) { - return filter.test(element); - } - - /* - * NOTE: seems need to keep call checkRangeIndex() for each condition, - * so don't break early even if test() return false. - */ - boolean valid = true; - Map rangeIndexMatches = null; - if (this.element2IndexValueMap != null) { - rangeIndexMatches = new HashMap<>(); - } - for (Condition cond : this.conditions) { - valid &= cond.test(element); - if (this.element2IndexValueMap != null) { - valid &= this.element2IndexValueMap.checkRangeIndex( - element, cond, rangeIndexMatches); - } - } - return valid; - } - - public void checkFlattened() { - E.checkState(this.isFlattened(), - "Query has none-flatten condition: %s", this); - } - - public boolean isFlattened() { - for (Condition condition : this.conditions) { - if (!condition.isFlattened()) { - return false; - } - } - return true; - } - - public boolean mayHasDupKeys(Set keys) { - Map keyCounts = new HashMap<>(); - for (Condition condition : this.conditions) { - if (!condition.isRelation()) { - // Assume may exist duplicate keys when has nested conditions - return true; - } - Relation relation = (Relation) condition; - if (keys.contains(relation.key())) { - int keyCount = keyCounts.getOrDefault(relation.key(), 0); - if (++keyCount > 1) { - return true; - } - keyCounts.put((HugeKeys) relation.key(), keyCount); - } - } - return false; - } - - public void optimized(OptimizedType optimizedType) { - assert this.optimizedType.ordinal() <= optimizedType.ordinal() : - this.optimizedType + " !<= " + optimizedType; - this.optimizedType = optimizedType; - - Query originQuery = this.originQuery(); - if (originQuery instanceof ConditionQuery) { - ConditionQuery cq = ((ConditionQuery) originQuery); - /* - * Two sub-query(flatten) will both set optimized of originQuery, - * here we just keep the higher one, this may not be a perfect way - */ - if (optimizedType.ordinal() > cq.optimized().ordinal()) { - cq.optimized(optimizedType); - } - } - } - - public OptimizedType optimized() { - return this.optimizedType; - } - - public void registerResultsFilter(ResultsFilter filter) { - assert this.resultsFilter == null; - this.resultsFilter = filter; - } - - ResultsFilter resultsFilter() { - return this.resultsFilter; - } - - public ConditionQuery originConditionQuery() { - Query originQuery = this.originQuery(); - if (!(originQuery instanceof ConditionQuery)) { - return null; - } - - while (originQuery.originQuery() instanceof ConditionQuery) { - originQuery = originQuery.originQuery(); - } - return (ConditionQuery) originQuery; - } - - public static String concatValues(List values) { - assert !values.isEmpty(); - List newValues = new ArrayList<>(values.size()); - for (Object v : values) { - newValues.add(concatValues(v)); - } - return SplicingIdGenerator.concatValues(newValues); - } - - public static String concatValues(Object value) { - if (value instanceof String) { - return escapeSpecialValueIfNeeded((String) value); - } - if (value instanceof List) { - return concatValues((List) value); - } else if (needConvertNumber(value)) { - return LongEncoding.encodeNumber(value); - } else { - return escapeSpecialValueIfNeeded(value.toString()); - } - } - - public static ConditionQuery fromBytes(byte[] bytes) { - Gson gson = new GsonBuilder() - .registerTypeAdapter(Condition.class, new QueryAdapter()) - .registerTypeAdapter(Id.class, new QueryIdAdapter()) - .setDateFormat("yyyy-MM-dd HH:mm:ss.SSS") - .create(); - String cqs = new String(bytes, StandardCharsets.UTF_8); - ConditionQuery conditionQuery = gson.fromJson(cqs, ConditionQuery.class); - - return conditionQuery; - } - - private static boolean needConvertNumber(Object value) { - // Numeric or date values should be converted to number from string - return NumericUtil.isNumber(value) || value instanceof Date; - } - - private static String escapeSpecialValueIfNeeded(String value) { - if (value.isEmpty()) { - // Escape empty String to INDEX_SYM_EMPTY (char `\u0002`) - value = INDEX_SYM_EMPTY; - } else if (value == INDEX_VALUE_EMPTY) { - value = ""; - } else if (value == INDEX_VALUE_NULL) { - value = INDEX_SYM_NULL; - } else { - char ch = value.charAt(0); - if (ch <= INDEX_SYM_MAX) { - /* - * Special symbols can't be used due to impossible to parse, - * and treat it as illegal value for the origin text property. - * TODO: escape special symbols - */ - E.checkArgument(false, - "Illegal leading char '\\u%s' " + - "in index property: '%s'", - (int) ch, value); - } - } - return value; - } - - public enum OptimizedType { - NONE, - PRIMARY_KEY, - SORT_KEYS, - INDEX, - INDEX_FILTER - } - - public static final class Element2IndexValueMap { - - private final Map> leftIndexMap; - private final Map>> filed2IndexValues; - private Id selectedIndexField; - - public Element2IndexValueMap() { - this.filed2IndexValues = new HashMap<>(); - this.leftIndexMap = new HashMap<>(); - } - - public void addIndexValue(Id indexField, Id elementId, - Object indexValue) { - if (!this.filed2IndexValues.containsKey(indexField)) { - this.filed2IndexValues.putIfAbsent(indexField, new HashMap<>()); - } - Map> element2IndexValueMap = - this.filed2IndexValues.get(indexField); - if (element2IndexValueMap.containsKey(elementId)) { - element2IndexValueMap.get(elementId).add(indexValue); - } else { - element2IndexValueMap.put(elementId, - Sets.newHashSet(indexValue)); - } - } - - public void selectedIndexField(Id indexField) { - this.selectedIndexField = indexField; - } - - public Set toRemoveIndexValues(Id indexField, Id elementId) { - if (!this.filed2IndexValues.containsKey(indexField)) { - return null; - } - return this.filed2IndexValues.get(indexField).get(elementId); - } - - public void addLeftIndex(Id elementId, Id indexField, - Set indexValues) { - LeftIndex leftIndex = new LeftIndex(indexValues, indexField); - if (this.leftIndexMap.containsKey(elementId)) { - this.leftIndexMap.get(elementId).add(leftIndex); - } else { - this.leftIndexMap.put(elementId, Sets.newHashSet(leftIndex)); - } - } - - public Set getLeftIndex(Id elementId) { - return this.leftIndexMap.get(elementId); - } - - public void removeElementLeftIndex(Id elementId) { - this.leftIndexMap.remove(elementId); - } - - public boolean checkRangeIndex(HugeElement element, Condition cond, - Map rangeIndexMatches) { - // Not UserpropRelation - if (!(cond instanceof Condition.UserpropRelation)) { - return true; - } - - Condition.UserpropRelation propRelation = - (Condition.UserpropRelation) cond; - Id propId = propRelation.key(); - Set fieldValues = this.toRemoveIndexValues(propId, - element.id()); - if (fieldValues == null) { - // Not range index - return true; - } - - if (rangeIndexMatches.containsKey(propId)) { - return rangeIndexMatches.get(propId); - } - - HugeProperty property = element.getProperty(propId); - if (property == null) { - // Property value has been deleted, so it's not matched - this.addLeftIndex(element.id(), propId, fieldValues); - return this.cacheRangeIndexMatch(rangeIndexMatches, propId, - false); - } - - /* - * NOTE: If removing successfully means there is correct index, - * else we should add left-index values to left index map to - * wait the left-index to be removed. - */ - boolean hasRightValue = removeFieldValue(fieldValues, - property.value()); - if (!fieldValues.isEmpty()) { - this.addLeftIndex(element.id(), propId, fieldValues); - } - - /* - * NOTE: When query by more than one range index field, - * if current field is not the selected one, it can only be used to - * determine whether the index values matched, can't determine - * the element is valid or not. - */ - if (this.selectedIndexField != null) { - hasRightValue = !propId.equals(this.selectedIndexField) || - hasRightValue; - } - - return this.cacheRangeIndexMatch(rangeIndexMatches, propId, - hasRightValue); - } - - private boolean cacheRangeIndexMatch(Map rangeIndexMatches, - Id propertyId, - boolean matched) { - rangeIndexMatches.put(propertyId, matched); - return matched; - } - - private static boolean removeFieldValue(Set values, - Object value) { - for (Object elem : values) { - if (numberEquals(elem, value)) { - values.remove(elem); - return true; - } - } - return false; - } - - private static boolean numberEquals(Object number1, Object number2) { - // Same class compare directly - if (number1.getClass().equals(number2.getClass())) { - return number1.equals(number2); - } - - // Otherwise convert to BigDecimal to make two numbers comparable - Number n1 = NumericUtil.convertToNumber(number1); - Number n2 = NumericUtil.convertToNumber(number2); - BigDecimal b1 = BigDecimal.valueOf(n1.doubleValue()); - BigDecimal b2 = BigDecimal.valueOf(n2.doubleValue()); - return b1.compareTo(b2) == 0; - } - } - - public static final class LeftIndex { - - private final Set indexFieldValues; - private final Id indexField; - - public LeftIndex(Set indexFieldValues, Id indexField) { - this.indexFieldValues = indexFieldValues; - this.indexField = indexField; - } - - public Set indexFieldValues() { - return this.indexFieldValues; - } - - public Id indexField() { - return this.indexField; - } - } - - public interface ResultsFilter { - - boolean test(HugeElement element); - } - - public byte[] bytes() { - String cqs = gson.toJson(this); - return cqs.getBytes(StandardCharsets.UTF_8); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQueryFlatten.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQueryFlatten.java index 649f88e7b3..1cc91be0d1 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQueryFlatten.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQueryFlatten.java @@ -26,9 +26,12 @@ import java.util.Set; import java.util.stream.Collectors; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Condition.Relation; -import org.apache.hugegraph.backend.query.Condition.SyspropRelation; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Condition.Relation; +import org.apache.hugegraph.query.Condition.SyspropRelation; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.InsertionOrderUtil; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/EdgesQueryIterator.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/EdgesQueryIterator.java index d8bce082c6..99c436b289 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/EdgesQueryIterator.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/EdgesQueryIterator.java @@ -20,8 +20,10 @@ import java.util.Iterator; import java.util.List; -import org.apache.hugegraph.backend.id.Id; import org.apache.hugegraph.backend.tx.GraphTransaction; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.type.define.Directions; public class EdgesQueryIterator implements Iterator { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/IdQuery.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/IdQuery.java deleted file mode 100644 index f7a1c732b3..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/IdQuery.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.backend.query; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.InsertionOrderUtil; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; - -public class IdQuery extends Query { - - private static final List EMPTY_IDS = ImmutableList.of(); - - // The id(s) will be concated with `or` - private List ids = EMPTY_IDS; - private boolean mustSortByInput = true; - - public IdQuery(HugeType resultType) { - super(resultType); - } - - public IdQuery(HugeType resultType, Query originQuery) { - super(resultType, originQuery); - } - - public IdQuery(HugeType resultType, Set ids) { - this(resultType); - this.query(ids); - } - - public IdQuery(HugeType resultType, Id id) { - this(resultType); - this.query(id); - } - - public IdQuery(Query originQuery, Id id) { - this(originQuery.resultType(), originQuery); - this.query(id); - } - - public IdQuery(Query originQuery, Set ids) { - this(originQuery.resultType(), originQuery); - this.query(ids); - } - - public boolean mustSortByInput() { - return this.mustSortByInput; - } - - public void mustSortByInput(boolean mustSortedByInput) { - this.mustSortByInput = mustSortedByInput; - } - - @Override - public int idsSize() { - return this.ids.size(); - } - - @Override - public Collection ids() { - return Collections.unmodifiableList(this.ids); - } - - public void resetIds() { - this.ids = EMPTY_IDS; - } - - public IdQuery query(Id id) { - E.checkArgumentNotNull(id, "Query id can't be null"); - if (this.ids == EMPTY_IDS) { - this.ids = InsertionOrderUtil.newList(); - } - - int last = this.ids.size() - 1; - if (last >= 0 && id.equals(this.ids.get(last))) { - // The same id as the previous one, just ignore it - return this; - } - - this.ids.add(id); - this.checkCapacity(this.ids.size()); - return this; - } - - public IdQuery query(Set ids) { - for (Id id : ids) { - this.query(id); - } - return this; - } - - @Override - public boolean test(HugeElement element) { - return this.ids.contains(element.id()); - } - - @Override - public IdQuery copy() { - IdQuery query = (IdQuery) super.copy(); - query.ids = this.ids == EMPTY_IDS ? EMPTY_IDS : - InsertionOrderUtil.newList(this.ids); - return query; - } - - public static final class OneIdQuery extends IdQuery { - - private Id id; - - public OneIdQuery(HugeType resultType, Id id) { - super(resultType); - super.mustSortByInput = false; - this.id = id; - } - - public OneIdQuery(Query originQuery, Id id) { - super(originQuery.resultType(), originQuery); - super.mustSortByInput = false; - this.id = id; - } - - public Id id() { - return this.id; - } - - public void resetId(Id id) { - this.id = id; - } - - @Override - public int idsSize() { - return this.id == null ? 0 : 1; - } - - @Override - public Set ids() { - return this.id == null ? ImmutableSet.of() : - ImmutableSet.of(this.id); - } - - @Override - public void resetIds() { - this.id = null; - } - - @Override - public IdQuery query(Id id) { - E.checkArgumentNotNull(id, "Query id can't be null"); - this.id = id; - return this; - } - - @Override - public boolean test(HugeElement element) { - if (this.id == null) { - return true; - } - return this.id.equals(element.id()); - } - - @Override - public IdQuery copy() { - OneIdQuery query = (OneIdQuery) super.copy(); - assert this.id.equals(query.id); - return query; - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Query.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Query.java deleted file mode 100644 index b8fc8e2964..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Query.java +++ /dev/null @@ -1,655 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.backend.query; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Aggregate.AggregateFunc; -import org.apache.hugegraph.exception.LimitExceedException; -import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.define.CollectionType; -import org.apache.hugegraph.type.define.HugeKeys; -import org.apache.hugegraph.util.CollectionUtil; -import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.InsertionOrderUtil; -import org.apache.hugegraph.util.collection.IdSet; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; - -public class Query implements Cloneable { - - // TODO: we should better not use Long.Max as the unify limit number - public static final long NO_LIMIT = Long.MAX_VALUE; - - public static final long COMMIT_BATCH = 500L; - public static final long QUERY_BATCH = 100L; - - public static final long NO_CAPACITY = -1L; - public static final long DEFAULT_CAPACITY = 800000L; // HugeGraph-777 - - private static final ThreadLocal CAPACITY_CONTEXT = new ThreadLocal<>(); - - protected static final Query NONE = new Query(HugeType.UNKNOWN); - - private static final Set EMPTY_OLAP_PKS = ImmutableSet.of(); - - private HugeType resultType; - private Map orders; - private long offset; - private long actualOffset; - private long actualStoreOffset; - private long limit; - private long skipDegree; - private String page; - private long capacity; - private boolean showHidden; - private boolean showDeleting; - private boolean showExpired; - private boolean olap; - private boolean withProperties; - private OrderType orderType; - private Set olapPks; - - private Aggregate aggregate; - - private Query originQuery; - - public Query(HugeType resultType) { - this(resultType, null); - } - - public Query(HugeType resultType, Query originQuery) { - this.resultType = resultType; - this.originQuery = originQuery; - - this.orders = null; - - this.offset = 0L; - this.actualOffset = 0L; - this.actualStoreOffset = 0L; - this.limit = NO_LIMIT; - this.skipDegree = NO_LIMIT; - this.page = null; - - this.capacity = defaultCapacity(); - - this.showHidden = false; - this.showDeleting = false; - - this.withProperties = true; - this.orderType = OrderType.ORDER_STRICT; - - this.aggregate = null; - this.showExpired = false; - this.olap = false; - this.olapPks = EMPTY_OLAP_PKS; - } - - public void copyBasic(Query query) { - E.checkNotNull(query, "query"); - this.offset = query.offset(); - this.limit = query.limit(); - this.skipDegree = query.skipDegree(); - this.page = query.page(); - this.capacity = query.capacity(); - this.showHidden = query.showHidden(); - this.showDeleting = query.showDeleting(); - this.withProperties = query.withProperties(); - this.orderType = query.orderType(); - this.aggregate = query.aggregate(); - this.showExpired = query.showExpired(); - this.olap = query.olap(); - if (query.orders != null) { - this.orders(query.orders); - } - } - - public HugeType resultType() { - return this.resultType; - } - - public void resultType(HugeType resultType) { - this.resultType = resultType; - } - - public Query originQuery() { - return this.originQuery; - } - - public void setOriginQuery(Query query) { - this.originQuery = query; - } - - public Query rootOriginQuery() { - Query root = this; - while (root.originQuery != null) { - root = root.originQuery; - } - return root; - } - - protected void originQuery(Query originQuery) { - this.originQuery = originQuery; - } - - public Map orders() { - return Collections.unmodifiableMap(this.getOrNewOrders()); - } - - public void orders(Map orders) { - this.orders = InsertionOrderUtil.newMap(orders); - } - - public void order(HugeKeys key, Order order) { - this.getOrNewOrders().put(key, order); - } - - protected Map getOrNewOrders() { - if (this.orders != null) { - return this.orders; - } - this.orders = InsertionOrderUtil.newMap(); - return this.orders; - } - - public long offset() { - return this.offset; - } - - public void offset(long offset) { - if (offset < 0L) { - E.checkArgument(false, "Invalid offset %s", offset); - } - this.offset = offset; - } - - public void copyOffset(Query parent) { - assert this.offset == 0L || this.offset == parent.offset; - assert this.actualOffset == 0L || - this.actualOffset == parent.actualOffset; - this.offset = parent.offset; - this.actualOffset = parent.actualOffset; - } - - public long actualOffset() { - return this.actualOffset; - } - - public void resetActualOffset() { - this.actualOffset = 0L; - this.actualStoreOffset = 0L; - } - - public long goOffset(long offset) { - if (offset < 0L) { - E.checkArgument(false, "Invalid offset value: %s", offset); - } - if (this.originQuery != null) { - this.goParentOffset(offset); - } - return this.goSelfOffset(offset); - } - - private void goParentOffset(long offset) { - assert offset >= 0L; - Query parent = this.originQuery; - while (parent != null) { - parent.actualOffset += offset; - parent = parent.originQuery; - } - } - - private long goSelfOffset(long offset) { - assert offset >= 0L; - if (this.originQuery != null) { - this.originQuery.goStoreOffsetBySubQuery(offset); - } - this.actualOffset += offset; - return this.actualOffset; - } - - private long goStoreOffsetBySubQuery(long offset) { - Query parent = this.originQuery; - while (parent != null) { - parent.actualStoreOffset += offset; - parent = parent.originQuery; - } - this.actualStoreOffset += offset; - return this.actualStoreOffset; - } - - public Set skipOffsetIfNeeded(Set elems) { - /* - * Skip index(index query with offset) for performance optimization. - * We assume one result is returned by each index, but if there are - * overridden index it will cause confusing offset and results. - */ - long fromIndex = this.offset() - this.actualOffset(); - if (fromIndex < 0L) { - // Skipping offset is overhead, no need to skip - fromIndex = 0L; - } - // An index holder yields ids in batches, only count this batch's ids - fromIndex = Math.min(fromIndex, elems.size()); - if (fromIndex > 0L) { - this.goOffset(fromIndex); - } - - if (fromIndex >= elems.size()) { - return ImmutableSet.of(); - } - long toIndex = this.total(); - if (this.noLimit() || toIndex > elems.size()) { - toIndex = elems.size(); - } - if (fromIndex == 0L && toIndex == elems.size()) { - return elems; - } - assert fromIndex < elems.size(); - assert toIndex <= elems.size(); - return CollectionUtil.subSet(elems, (int) fromIndex, (int) toIndex); - } - - public long remaining() { - if (this.limit == NO_LIMIT) { - return NO_LIMIT; - } else { - return this.total() - this.actualOffset(); - } - } - - public long total() { - if (this.limit == NO_LIMIT) { - return NO_LIMIT; - } else { - return this.offset + this.limit; - } - } - - public long limit() { - if (this.capacity != NO_CAPACITY) { - if (this.limit == NO_LIMIT) { - /* - * TODO: may need to return this.capacity here. - * In most cases, capacity == DEFAULT_CAPACITY, then limit() - * will return NO_LIMIT, thus rely on Query.checkCapacity() - * to check in the next step. - */ - } - if (this.limit != NO_LIMIT && this.limit > this.capacity) { - E.checkArgument(false, - "Invalid limit %s, must be <= capacity(%s)", - this.limit, this.capacity); - } - } - return this.limit; - } - - public void limit(long limit) { - if (limit != NO_LIMIT && limit < 0L) { - E.checkArgument(false, "Invalid limit %s", limit); - } - this.limit = limit; - } - - public boolean noLimit() { - return this.limit() == NO_LIMIT; - } - - public boolean noLimitAndOffset() { - return this.limit() == NO_LIMIT && this.offset() == 0L; - } - - public boolean reachLimit(long count) { - long limit = this.limit(); - if (limit == NO_LIMIT) { - return false; - } - return count >= (limit + this.offset()); - } - - /** - * Set or update the offset and limit by a range [start, end) - * NOTE: it will use the min range one: max start and min end - * - * @param start the range start, include it - * @param end the range end, exclude it - */ - public long range(long start, long end) { - // Update offset - long offset = this.offset(); - start = Math.max(start, offset); - this.offset(start); - - // Update limit - if (end != -1L) { - if (!this.noLimit()) { - end = Math.min(end, offset + this.limit()); - } else { - assert end < Query.NO_LIMIT; - } - if (end < start) { - E.checkArgument(false, "Invalid range: [%s, %s)", start, end); - } - this.limit(end - start); - } else { - // Keep the origin limit - assert this.limit() <= Query.NO_LIMIT; - } - return this.limit; - } - - public String page() { - if (this.page != null) { - if (this.limit() == 0L) { - E.checkState(false, - "Can't set limit=0 when using paging"); - } - if (this.offset() != 0L) { - E.checkState(false, - "Can't set offset when using paging, but got '%s'", - this.offset()); - } - } - return this.page; - } - - public String pageWithoutCheck() { - return this.page; - } - - public void page(String page) { - this.page = page; - } - - public boolean paging() { - return this.page != null; - } - - public void olap(boolean olap) { - this.olap = olap; - } - - public boolean olap() { - return this.olap; - } - - public void olapPks(Set olapPks) { - for (Id olapPk : olapPks) { - this.olapPk(olapPk); - } - } - - public void olapPk(Id olapPk) { - if (this.olapPks == EMPTY_OLAP_PKS) { - this.olapPks = new IdSet(CollectionType.EC); - } - this.olapPks.add(olapPk); - } - - public Set olapPks() { - return this.olapPks; - } - - public long capacity() { - return this.capacity; - } - - public void capacity(long capacity) { - this.capacity = capacity; - } - - public boolean bigCapacity() { - return this.capacity == NO_CAPACITY || this.capacity > DEFAULT_CAPACITY; - } - - public void checkCapacity(long count) throws LimitExceedException { - // Throw LimitExceedException if reach capacity - if (this.capacity != NO_CAPACITY && count > this.capacity) { - final int MAX_CHARS = 256; - String query = this.toString(); - if (query.length() > MAX_CHARS) { - query = query.substring(0, MAX_CHARS) + "..."; - } - throw new LimitExceedException( - "Too many records(must <= %s) for the query: %s", - this.capacity, query); - } - } - - public Aggregate aggregate() { - return this.aggregate; - } - - public Aggregate aggregateNotNull() { - if (this.aggregate == null) { - E.checkArgument(false, - "The aggregate must be set for number query"); - } - return this.aggregate; - } - - public void aggregate(AggregateFunc func, String property) { - this.aggregate = new Aggregate(func, property); - } - - public void aggregate(Aggregate aggregate) { - this.aggregate = aggregate; - } - - public boolean showHidden() { - return this.showHidden; - } - - public void showHidden(boolean showHidden) { - this.showHidden = showHidden; - } - - public boolean showDeleting() { - return this.showDeleting; - } - - public void showDeleting(boolean showDeleting) { - this.showDeleting = showDeleting; - } - - public long skipDegree() { - return this.skipDegree; - } - - public void skipDegree(long skipDegree) { - this.skipDegree = skipDegree; - } - - public boolean withProperties() { - return this.withProperties; - } - - public void withProperties(boolean withProperties) { - this.withProperties = withProperties; - } - - public OrderType orderType() { - return this.orderType; - } - - public void orderType(OrderType orderType) { - this.orderType = orderType; - } - - public boolean showExpired() { - return this.showExpired; - } - - public void showExpired(boolean showExpired) { - this.showExpired = showExpired; - } - - public Collection ids() { - return ImmutableList.of(); - } - - public Collection conditions() { - return ImmutableList.of(); - } - - public int idsSize() { - return 0; - } - - public int conditionsSize() { - return 0; - } - - public boolean empty() { - return this.idsSize() == 0 && this.conditionsSize() == 0; - } - - public boolean test(HugeElement element) { - return true; - } - - public Query copy() { - try { - return (Query) this.clone(); - } catch (CloneNotSupportedException e) { - throw new BackendException(e); - } - } - - @Override - public boolean equals(Object object) { - if (!(object instanceof Query)) { - return false; - } - Query other = (Query) object; - return this.resultType.equals(other.resultType) && - this.orders().equals(other.orders()) && - this.offset == other.offset && - this.limit == other.limit && - Objects.equals(this.page, other.page) && - this.ids().equals(other.ids()) && - this.conditions().equals(other.conditions()) && - this.withProperties == other.withProperties; - } - - @Override - public int hashCode() { - return this.resultType.hashCode() ^ - this.orders().hashCode() ^ - Long.hashCode(this.offset) ^ - Long.hashCode(this.limit) ^ - Objects.hashCode(this.page) ^ - this.ids().hashCode() ^ - this.conditions().hashCode() ^ - Boolean.hashCode(this.withProperties); - } - - @Override - public String toString() { - Map pairs = InsertionOrderUtil.newMap(); - if (this.page != null) { - pairs.put("page", String.format("'%s'", this.page)); - } - if (this.offset != 0) { - pairs.put("offset", this.offset); - } - if (this.limit != NO_LIMIT) { - pairs.put("limit", this.limit); - } - if (!this.orders().isEmpty()) { - pairs.put("order by", this.orders()); - } - - StringBuilder sb = new StringBuilder(128); - sb.append("`Query "); - if (this.aggregate != null) { - sb.append(this.aggregate); - } else { - sb.append('*'); - } - sb.append(" from ").append(this.resultType); - for (Map.Entry entry : pairs.entrySet()) { - sb.append(' ').append(entry.getKey()) - .append(' ').append(entry.getValue()).append(','); - } - if (!pairs.isEmpty()) { - // Delete last comma - sb.deleteCharAt(sb.length() - 1); - } - - if (!this.empty()) { - sb.append(" where"); - } - - // Append ids - if (!this.ids().isEmpty()) { - sb.append(" id in ").append(this.ids()); - } - - // Append conditions - if (!this.conditions().isEmpty()) { - if (!this.ids().isEmpty()) { - sb.append(" and"); - } - sb.append(" ").append(this.conditions()); - } - - sb.append('`'); - return sb.toString(); - } - - public static long defaultCapacity(long capacity) { - Long old = CAPACITY_CONTEXT.get(); - CAPACITY_CONTEXT.set(capacity); - return old != null ? old : DEFAULT_CAPACITY; - } - - public static long defaultCapacity() { - Long capacity = CAPACITY_CONTEXT.get(); - return capacity != null ? capacity : DEFAULT_CAPACITY; - } - - public static void checkForceCapacity(long count) throws LimitExceedException { - if (count > Query.DEFAULT_CAPACITY) { - throw new LimitExceedException( - "Too many records(must <= %s) for one query", - Query.DEFAULT_CAPACITY); - } - } - - public enum OrderType { - // Under batch interface, the requirement for return order - ORDER_NONE, // Allow unordered - ORDER_WITHIN_VERTEX, // Edges within a vertex will not be broken, but there is no order between different vertices. - ORDER_STRICT // Ensure the original input point order - } - - public enum Order { - ASC, - DESC - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryBatch.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryBatch.java index f210a503a2..58088a9263 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryBatch.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryBatch.java @@ -25,10 +25,11 @@ import java.util.function.BiPredicate; import java.util.function.Function; -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.iterator.CIter; import org.apache.hugegraph.iterator.Metadatable; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.type.Idfiable; import org.apache.hugegraph.util.InsertionOrderUtil; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryResultContext.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryResultContext.java index 27e6ec67ae..6392981e50 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryResultContext.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryResultContext.java @@ -21,9 +21,12 @@ import java.util.Collections; import java.util.List; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery.OptimizedType; -import org.apache.hugegraph.backend.query.ConditionQuery.ResultsFilter; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery.OptimizedType; +import org.apache.hugegraph.query.ConditionQuery.ResultsFilter; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.Query; /** Decisions captured for one batch; queries retain shared index cleanup data. */ public final class QueryResultContext { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryResults.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryResults.java index 35675bcf35..ce68722408 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryResults.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryResults.java @@ -25,17 +25,21 @@ import java.util.function.BiPredicate; import java.util.function.Function; -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.id.Id; import org.apache.hugegraph.backend.query.QueryBatch.BatchIterator; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.iterator.CIter; import org.apache.hugegraph.iterator.ListIterator; import org.apache.hugegraph.perf.PerfUtil.Watched; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.Idfiable; /** A lazy stream of query batches. Only the final consumer flattens the stream. */ public class QueryResults { + private static final Query EMPTY_QUERY = new Query(HugeType.UNKNOWN); + private final BatchIterator> batches; private final Object metadata; private Iterator results; @@ -346,7 +350,7 @@ public static Iterator iterator(T value) { } public static QueryResults empty() { - return new QueryResults(QueryResults.emptyIterator(), Query.NONE); + return new QueryResults(QueryResults.emptyIterator(), EMPTY_QUERY); } public static Iterator emptyIterator() { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/serializer/AbstractSerializerAdapter.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/serializer/AbstractSerializerAdapter.java deleted file mode 100644 index 1a66ddf074..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/serializer/AbstractSerializerAdapter.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -package org.apache.hugegraph.backend.query.serializer; - -import java.lang.reflect.Type; -import java.util.Map; - -import org.apache.hugegraph.backend.BackendException; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; - -// TODO: optimize by binary protocol -public abstract class AbstractSerializerAdapter implements JsonSerializer, - JsonDeserializer { - - //Note: By overriding the method to get the mapping - public abstract Map validType(); - - @Override - public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws - JsonParseException { - JsonObject object = json.getAsJsonObject(); - String type = object.get("cls").getAsString(); - JsonElement element = object.get("el"); - try { - return context.deserialize(element, validType().get(type)); - } catch (Exception e) { - throw new BackendException("Unknown element type: " + type, e); - } - } - - /* - * Note: Currently, only the first character of the class name is taken as the key - * to reduce serialization results - * */ - @Override - public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - Class clazz = src.getClass(); - result.add("cls", new JsonPrimitive(clazz.getSimpleName().substring(0, 1).toUpperCase())); - result.add("el", context.serialize(src, clazz)); - return result; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/serializer/QueryAdapter.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/serializer/QueryAdapter.java deleted file mode 100644 index 47b7c76a00..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/serializer/QueryAdapter.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -package org.apache.hugegraph.backend.query.serializer; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.type.define.Directions; - -import com.google.common.collect.ImmutableMap; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.reflect.TypeToken; - -public class QueryAdapter extends AbstractSerializerAdapter { - - static ImmutableMap cls = - ImmutableMap.builder() - // TODO: uncomment later - .put("N", Condition.Not.class) - .put("A", Condition.And.class) - .put("O", Condition.Or.class) - .put("S", Condition.SyspropRelation.class) - .put("U", Condition.UserpropRelation.class) - .build(); - - static boolean isPrimitive(Class clz) { - try { - return (clz == Date.class) || ((Class) clz.getField("TYPE").get(null)).isPrimitive(); - } catch (Exception e) { - return false; - } - } - - @Override - public Map validType() { - return cls; - } - - @Override - public Condition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - Condition condition = super.deserialize(json, typeOfT, context); - if (condition instanceof Condition.Relation) { - JsonObject object = json.getAsJsonObject(); - if (object.has("el")) { - JsonElement elElement = object.get("el"); - JsonElement valueElement = elElement.getAsJsonObject().get("value"); - if (valueElement.isJsonObject()) { - String cls = valueElement.getAsJsonObject().get("cls").getAsString(); - try { - Class actualClass = Class.forName(cls); - Object obj = context.deserialize(valueElement, actualClass); - ((Condition.Relation) condition).value(obj); - } catch (ClassNotFoundException e) { - throw new JsonParseException(e.getMessage()); - } - } else if (elElement.getAsJsonObject().has("valuecls")) { - if (valueElement.isJsonArray()) { - String cls = elElement.getAsJsonObject().get("valuecls").getAsString(); - try { - Class actualClass = Class.forName(cls); - Type type = TypeToken.getParameterized(ArrayList.class, actualClass) - .getType(); - Object value = context.deserialize(valueElement, type); - ((Condition.Relation) condition).value(value); - } catch (ClassNotFoundException e) { - throw new JsonParseException(e.getMessage()); - } - } else { - String cls = elElement.getAsJsonObject().get("valuecls").getAsString(); - try { - Class actualClass = Class.forName(cls); - Object obj = context.deserialize(valueElement, actualClass); - ((Condition.Relation) condition).value(obj); - } catch (ClassNotFoundException e) { - throw new JsonParseException(e.getMessage()); - } - } - - } else if (valueElement.isJsonPrimitive() && - valueElement.getAsJsonPrimitive().isString()) { - switch ((String) ((Condition.Relation) condition).value()) { - case "OUT": - ((Condition.Relation) condition).value(Directions.OUT); - break; - case "IN": - ((Condition.Relation) condition).value(Directions.IN); - break; - default: - break; - } - } - } - } - return condition; - } - - @Override - public JsonElement serialize(Condition src, Type typeOfSrc, JsonSerializationContext context) { - JsonElement result = super.serialize(src, typeOfSrc, context); - if (src instanceof Condition.Relation) { - JsonObject object = result.getAsJsonObject(); - JsonElement valueElement = object.get("el").getAsJsonObject().get("value"); - if (valueElement.isJsonObject()) { - valueElement.getAsJsonObject() - .add("cls", - new JsonPrimitive( - ((Condition.Relation) src).value().getClass().getName())); - } else if (isPrimitive(((Condition.Relation) src).value().getClass())) { - object.get("el").getAsJsonObject() - .add("valuecls", - new JsonPrimitive( - ((Condition.Relation) src).value().getClass().getName())); - } else if (valueElement.isJsonArray()) { - if (((Condition.Relation) src).value() instanceof List) { - String valueCls = - ((List) ((Condition.Relation) src).value()).get(0).getClass().getName(); - object.get("el").getAsJsonObject().add("valuecls", new JsonPrimitive(valueCls)); - } - } - } - return result; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/serializer/QueryIdAdapter.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/serializer/QueryIdAdapter.java deleted file mode 100644 index e7ebaea766..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/serializer/QueryIdAdapter.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -package org.apache.hugegraph.backend.query.serializer; - -import java.lang.reflect.Type; -import java.util.Map; - -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; - -import com.google.common.collect.ImmutableMap; - -public class QueryIdAdapter extends AbstractSerializerAdapter { - - static ImmutableMap cls = - ImmutableMap.builder() - .put("E", EdgeId.class) - .put("S", IdGenerator.StringId.class) - .put("L", IdGenerator.LongId.class) - .put("U", IdGenerator.UuidId.class) - .put("O", IdGenerator.ObjectId.class) - .put("B", BinaryBackendEntry.BinaryId.class) - .build(); - - @Override - public Map validType() { - return cls; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/AbstractSerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/AbstractSerializer.java index 734cb90426..cd89f36dbc 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/AbstractSerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/AbstractSerializer.java @@ -18,15 +18,16 @@ package org.apache.hugegraph.backend.serializer; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.iterator.CIter; import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.HugeKeys; import org.apache.tinkerpop.gremlin.structure.Edge; public abstract class AbstractSerializer @@ -54,6 +55,18 @@ protected BackendEntry convertEntry(BackendEntry entry) { protected abstract Query writeQueryCondition(Query query); + protected Object edgeIdConditionValue(ConditionQuery query, + HugeKeys key) { + if (key == HugeKeys.LABEL) { + /* + * LABEL may still be represented by multiple top-level EQ/IN + * relations before strict edge-id serialization. + */ + return query.conditionValue(key); + } + return query.condition(key); + } + @Override public Query writeQuery(Query query) { HugeType type = query.resultType(); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinaryBackendEntry.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinaryBackendEntry.java index 9dfb8b4e5c..50b8b4436d 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinaryBackendEntry.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinaryBackendEntry.java @@ -17,19 +17,20 @@ package org.apache.hugegraph.backend.serializer; -import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.backend.BinaryId; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendEntryIterator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.Bytes; public class BinaryBackendEntry implements BackendEntry { @@ -207,79 +208,4 @@ public boolean equals(Object obj) { public int hashCode() { return this.id().hashCode() ^ this.columns.size(); } - - public static class BinaryId implements Id { - - protected byte[] bytes; - protected Id id; - - public BinaryId(byte[] bytes, Id id) { - this.bytes = bytes; - this.id = id; - } - - public Id origin() { - return this.id; - } - - @Override - public IdType type() { - return IdType.UNKNOWN; - } - - @Override - public Object asObject() { - return ByteBuffer.wrap(this.bytes); - } - - @Override - public String asString() { - throw new UnsupportedOperationException(); - } - - @Override - public long asLong() { - throw new UnsupportedOperationException(); - } - - @Override - public int compareTo(Id other) { - return Bytes.compare(this.bytes, other.asBytes()); - } - - @Override - public byte[] asBytes() { - return this.bytes; - } - - public byte[] asBytes(int offset) { - E.checkArgument(offset < this.bytes.length, - "Invalid offset %s, must be < length %s", - offset, this.bytes.length); - return Arrays.copyOfRange(this.bytes, offset, this.bytes.length); - } - - @Override - public int length() { - return this.bytes.length; - } - - @Override - public int hashCode() { - return ByteBuffer.wrap(this.bytes).hashCode(); - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof BinaryId)) { - return false; - } - return Arrays.equals(this.bytes, ((BinaryId) other).bytes); - } - - @Override - public String toString() { - return "0x" + Bytes.toHex(this.bytes); - } - } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinaryEntryIterator.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinaryEntryIterator.java index 1095742681..a499ffa00c 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinaryEntryIterator.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinaryEntryIterator.java @@ -20,7 +20,7 @@ import java.util.function.BiFunction; import org.apache.hugegraph.backend.page.PageState; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendEntry.BackendIterator; import org.apache.hugegraph.backend.store.BackendEntryIterator; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinaryScatterSerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinaryScatterSerializer.java index 18b642a001..f69fd703cb 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinaryScatterSerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinaryScatterSerializer.java @@ -17,13 +17,15 @@ package org.apache.hugegraph.backend.serializer; +import org.apache.hugegraph.serializer.BytesBuffer; + import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.config.HugeConfig; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeProperty; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.structure.HugeVertexProperty; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java index 0bb07760a5..04cf773664 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java @@ -17,7 +17,10 @@ package org.apache.hugegraph.backend.serializer; -import static org.apache.hugegraph.schema.SchemaElement.UNDEF; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.BytesBuffer.IndexColumnName; +import org.apache.hugegraph.serializer.BytesBuffer.IndexExpiryLayout; +import org.apache.hugegraph.serializer.BinaryElementSerializer; import java.util.Arrays; import java.util.Collection; @@ -27,32 +30,32 @@ import org.apache.commons.lang.NotImplementedException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.page.PageState; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.Condition.RangeConditions; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdPrefixQuery; -import org.apache.hugegraph.backend.query.IdRangeQuery; -import org.apache.hugegraph.backend.query.Query; -import org.apache.hugegraph.backend.serializer.BinaryBackendEntry.BinaryId; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.Condition.RangeConditions; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdPrefixQuery; +import org.apache.hugegraph.query.IdRangeQuery; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.backend.BinaryId; import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.iterator.CIter; import org.apache.hugegraph.iterator.MapperIterator; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeEdgeProperty; import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.structure.HugeIndex; +import org.apache.hugegraph.structure.Index; import org.apache.hugegraph.structure.HugeProperty; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.structure.HugeVertexProperty; @@ -61,7 +64,6 @@ import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; import org.apache.hugegraph.type.define.Directions; -import org.apache.hugegraph.type.define.EdgeLabelType; import org.apache.hugegraph.type.define.Frequency; import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.type.define.IdStrategy; @@ -198,137 +200,55 @@ protected byte[] formatPropertyName(HugeProperty prop) { protected BackendColumn formatProperty(HugeProperty prop) { BytesBuffer buffer = BytesBuffer.allocate(BytesBuffer.BUF_PROPERTY); - buffer.writeProperty(prop.propertyKey(), prop.value()); + buffer.writeSchemaProperty(prop.propertyKey(), prop.value()); return BackendColumn.of(this.formatPropertyName(prop), buffer.bytes()); } protected void parseProperty(Id pkeyId, BytesBuffer buffer, HugeElement owner) { - PropertyKey pkey = owner.graph().propertyKey(pkeyId); - - // Parse value - Object value = buffer.readProperty(pkey); - - // Set properties of vertex/edge - if (pkey.cardinality() == Cardinality.SINGLE) { - owner.addProperty(pkey, value); - } else { - if (!(value instanceof Collection)) { - throw new BackendException( - "Invalid value of non-single property: %s", value); - } - owner.addProperty(pkey, value); - } + BinaryElementSerializer.getInstance().parseSchemaProperty( + owner.graph(), pkeyId, buffer, owner::addProperty); } - protected void formatProperties(Collection> props, - BytesBuffer buffer) { - // Write properties size - buffer.writeVInt(props.size()); - - // Write properties data - for (HugeProperty property : props) { - PropertyKey pkey = property.propertyKey(); - buffer.writeVInt(SchemaElement.schemaId(pkey.id())); - buffer.writeProperty(pkey, property.value()); - } + protected void formatProperties(HugeElement owner, BytesBuffer buffer) { + BinaryElementSerializer.getInstance().formatSchemaProperties(owner.element(), buffer); } protected void parseProperties(BytesBuffer buffer, HugeElement owner) { - int size = buffer.readVInt(); - assert size >= 0; - for (int i = 0; i < size; i++) { - Id pkeyId = IdGenerator.of(buffer.readVInt()); - this.parseProperty(pkeyId, buffer, owner); - } + BinaryElementSerializer.getInstance().parseSchemaProperties( + owner.graph(), buffer, owner::addProperty); } protected void formatExpiredTime(long expiredTime, BytesBuffer buffer) { - buffer.writeVLong(expiredTime); + BinaryElementSerializer.getInstance().formatExpiredTime(expiredTime, buffer); } protected void parseExpiredTime(BytesBuffer buffer, HugeElement element) { - element.expiredTime(buffer.readVLong()); + BinaryElementSerializer.getInstance().parseExpiredTime(buffer, element.element()); } protected byte[] formatEdgeValue(HugeEdge edge) { - int propsCount = edge.sizeOfProperties(); - BytesBuffer buffer = BytesBuffer.allocate(4 + 16 * propsCount); - - // Write edge id - //buffer.writeId(edge.id()); - - // Write edge properties - this.formatProperties(edge.getProperties(), buffer); - - // Write edge expired time if needed - if (edge.hasTtl()) { - this.formatExpiredTime(edge.expiredTime(), buffer); - } - - return buffer.bytes(); + return BinaryElementSerializer.getInstance().formatSchemaEdgeValue(edge.element()); } protected void parseEdge(BackendColumn col, HugeVertex vertex, HugeGraph graph) { - // owner-vertex + dir + edge-label + sort-values + other-vertex - - BytesBuffer buffer = BytesBuffer.wrap(col.name); - if (this.keyWithIdPrefix) { - // Consume owner-vertex id - buffer.readId(); - } - byte type = buffer.read(); - Id labelId = buffer.readId(); - Id subLabelId = buffer.readId(); - String sortValues = buffer.readStringWithEnding(); - Id otherVertexId = buffer.readId(); - - boolean direction = EdgeId.isOutDirectionFromCode(type); - - HugeEdge edge; - if (graph == null) { /* when calculation sinking */ - EdgeLabel edgeLabel = new EdgeLabel(null, subLabelId, UNDEF); - if (subLabelId != labelId) { - edgeLabel.edgeLabelType(EdgeLabelType.SUB); - edgeLabel.fatherId(labelId); - } - edge = HugeEdge.constructEdgeWithoutGraph(vertex, direction, edgeLabel, - sortValues, otherVertexId); - } else { - EdgeLabel edgeLabel = graph.edgeLabelOrNone(subLabelId); - edge = HugeEdge.constructEdge(vertex, direction, edgeLabel, - sortValues, otherVertexId); - } - - // Parse edge-id + edge-properties - buffer = BytesBuffer.wrap(col.value); - - //Id id = buffer.readId(); - - // Parse edge properties - this.parseProperties(buffer, edge); - - // Parse edge expired time if needed - if (edge.hasTtl()) { - this.parseExpiredTime(buffer, edge); - } + EdgeId id = BytesBuffer.wrap(col.name).readEdgeId(this.keyWithIdPrefix, vertex.id()); + EdgeLabel edgeLabel = BinaryElementSerializer.getInstance().edgeLabel(graph, id, vertex.graph()); + boolean direction = id.direction() == Directions.OUT; + HugeEdge edge = graph == null ? + HugeEdge.constructEdgeWithoutGraph(vertex, direction, edgeLabel, + id.sortValues(), id.otherVertexId()) : + HugeEdge.constructEdge(vertex, direction, edgeLabel, + id.sortValues(), id.otherVertexId()); + + BinaryElementSerializer.getInstance().parseSchemaEdgeValue( + edge.graph(), col.value, edge.element(), edge::addProperty); } protected void parseVertex(byte[] value, HugeVertex vertex) { - BytesBuffer buffer = BytesBuffer.wrap(value); - - // Parse vertex label - VertexLabel label = vertex.graph().vertexLabelOrNone(buffer.readId()); - vertex.correctVertexLabel(label); - - // Parse properties - this.parseProperties(buffer, vertex); - - // Parse vertex expired time if needed - if (vertex.hasTtl()) { - this.parseExpiredTime(buffer, vertex); - } + BinaryElementSerializer.getInstance().parseSchemaVertexValue( + vertex.graph(), value, vertex.element(), vertex::addProperty); } protected void parseColumn(BackendColumn col, HugeVertex vertex) { @@ -357,48 +277,32 @@ else if (type == HugeType.SYS_PROPERTY.code()) { } } - protected byte[] formatIndexName(HugeIndex index) { - BytesBuffer buffer; - Id elemId = index.elementId(); - if (!this.indexWithIdPrefix) { - int idLen = 1 + elemId.length(); - buffer = BytesBuffer.allocate(idLen); - } else { - Id indexId = index.id(); - HugeType type = index.type(); - if (!type.isNumericIndex() && indexIdLengthExceedLimit(indexId)) { - indexId = index.hashId(); - } - int idLen = 1 + elemId.length() + 1 + indexId.length(); - buffer = BytesBuffer.allocate(idLen); - // Write index-id - buffer.writeIndexId(indexId, type); - } - // Write element-id - buffer.writeId(elemId); - // Write expired time if needed - if (index.hasTtl()) { - buffer.writeVLong(index.expiredTime()); - } - - return buffer.bytes(); + protected byte[] formatIndexName(Index index) { + return BinaryElementSerializer.getInstance().formatSchemaIndex( + index.type(), index.indexLabelId(), index.fieldValues(), index.elementId(), + this.indexWithIdPrefix, index.hasTtl(), index.expiredTime()).name; } protected void parseIndexName(HugeGraph graph, ConditionQuery query, BinaryBackendEntry entry, - HugeIndex index, Object fieldValues) { + Index index, Object fieldValues) { for (BackendColumn col : entry.columns()) { - if (indexFieldValuesUnmatched(col.value, fieldValues)) { - // Skip if field-values is not matched (just the same hash) + if (!index.type().isLabelIndex() && indexFieldValuesUnmatched(col.value, fieldValues)) { + // User-index hash collision filtering remains an engine adapter. continue; } BytesBuffer buffer = BytesBuffer.wrap(col.name); - if (this.indexWithIdPrefix) { - buffer.readIndexId(index.type()); + IndexColumnName name = buffer.readIndexColumnName( + index.type(), this.indexWithIdPrefix, + index.hasTtl() ? IndexExpiryLayout.REQUIRED : IndexExpiryLayout.NONE); + Long storedLabelExpiry = BinaryElementSerializer.getInstance().storedLabelIndexExpiredTime( + index.type(), buffer.remaining() == 0, col.value); + if (index.type().isLabelIndex() && storedLabelExpiry == null && + indexFieldValuesUnmatched(col.value, fieldValues)) { + continue; } - Id elemId = buffer.readId(); - long expiredTime = index.hasTtl() ? buffer.readVLong() : 0L; - index.elementIds(elemId, expiredTime); + index.elementIds(name.elementId(), storedLabelExpiry == null ? + name.expiredTime() : storedLabelExpiry); } } @@ -414,25 +318,15 @@ public BackendEntry writeVertex(HugeVertex vertex) { return entry; } - int propsCount = vertex.sizeOfProperties(); - BytesBuffer buffer = BytesBuffer.allocate(8 + 16 * propsCount); - - // Write vertex label - buffer.writeId(vertex.schemaLabel().id()); - - // Write all properties of the vertex - this.formatProperties(vertex.getProperties(), buffer); - - // Write vertex expired time if needed + byte[] value = BinaryElementSerializer.getInstance().formatSchemaVertexValue(vertex.element()); if (vertex.hasTtl()) { entry.ttl(vertex.ttl()); - this.formatExpiredTime(vertex.expiredTime(), buffer); } - // Fill column + // Backend-specific column keys and entry TTL remain in the adapter. byte[] name = this.keyWithIdPrefix ? entry.id().asBytes() : BytesBuffer.BYTES_EMPTY; - entry.column(name, buffer.bytes()); + entry.column(name, value); return entry; } @@ -451,8 +345,7 @@ public BackendEntry writeOlapVertex(HugeVertex vertex) { } HugeProperty property = properties.iterator().next(); PropertyKey propertyKey = property.propertyKey(); - buffer.writeVInt(SchemaElement.schemaId(propertyKey.id())); - buffer.writeProperty(propertyKey, property.value()); + BinaryElementSerializer.getInstance().formatSchemaProperty(property.baseProperty(), buffer); // Fill column byte[] name = this.keyWithIdPrefix ? @@ -504,9 +397,8 @@ public HugeVertex readVertex(HugeGraph graph, BackendEntry bytesEntry) { } protected void parseVertexOlap(byte[] value, HugeVertex vertex) { - BytesBuffer buffer = BytesBuffer.wrap(value); - Id pkeyId = IdGenerator.of(buffer.readVInt()); - this.parseProperty(pkeyId, buffer, vertex); + BinaryElementSerializer.getInstance().parseSchemaPropertyRecord( + vertex.graph(), BytesBuffer.wrap(value), vertex::addProperty); } @Override @@ -576,7 +468,7 @@ public CIter readEdges(HugeGraph graph, BackendEntry bytesEntry) { } @Override - public BackendEntry writeIndex(HugeIndex index) { + public BackendEntry writeIndex(Index index) { BinaryBackendEntry entry; if (index.fieldValues() == null && index.elementIds().isEmpty()) { /* @@ -586,20 +478,17 @@ public BackendEntry writeIndex(HugeIndex index) { */ entry = this.formatILDeletion(index); } else { - Id id = index.id(); HugeType type = index.type(); - byte[] value = null; - if (!type.isNumericIndex() && indexIdLengthExceedLimit(id)) { - id = index.hashId(); - // Save field-values as column value if the key is a hash string - value = StringEncoding.encode(index.fieldValues().toString()); - } - + Id id = BinaryElementSerializer.schemaIndexId(type, index.indexLabelId(), index.fieldValues()); + BackendColumn column = + BinaryElementSerializer.getInstance().formatSchemaIndex( + type, index.indexLabelId(), index.fieldValues(), index.elementId(), + this.indexWithIdPrefix, index.hasTtl(), index.expiredTime()); entry = newBackendEntry(type, id); if (index.indexLabel().olap()) { entry.olap(true); } - entry.column(this.formatIndexName(index), value); + entry.columns(column); entry.subId(index.elementId()); if (index.hasTtl()) { @@ -610,7 +499,7 @@ public BackendEntry writeIndex(HugeIndex index) { } @Override - public HugeIndex readIndex(HugeGraph graph, ConditionQuery query, + public Index readIndex(HugeGraph graph, ConditionQuery query, BackendEntry bytesEntry) { if (bytesEntry == null) { return null; @@ -619,7 +508,7 @@ public HugeIndex readIndex(HugeGraph graph, ConditionQuery query, BinaryBackendEntry entry = this.convertEntry(bytesEntry); // NOTE: index id without length prefix byte[] bytes = entry.id().asBytes(); - HugeIndex index = HugeIndex.parseIndexId(graph, entry.type(), bytes); + Index index = Index.parseIndexId(graph, entry.type(), bytes); Object fieldValues = null; if (!index.type().isRangeIndex()) { @@ -674,7 +563,7 @@ private Query writeQueryEdgeRangeCondition(ConditionQuery cq) { if (direction == null) { direction = Directions.OUT; } - Id label = cq.condition(HugeKeys.LABEL); + Id label = (Id) this.edgeIdConditionValue(cq, HugeKeys.LABEL); BytesBuffer start = BytesBuffer.allocate(BytesBuffer.BUF_EDGE_ID); writePartitionedId(HugeType.EDGE, vertex, start); @@ -722,7 +611,7 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) { int count = 0; BytesBuffer buffer = BytesBuffer.allocate(BytesBuffer.BUF_EDGE_ID); for (HugeKeys key : EdgeId.KEYS) { - Object value = cq.condition(key); + Object value = this.edgeIdConditionValue(cq, key); if (value != null) { count++; @@ -871,7 +760,7 @@ private Query writeRangeIndexQuery(ConditionQuery query) { return new IdRangeQuery(query, start, keyMinEq, max, keyMaxEq); } - private BinaryBackendEntry formatILDeletion(HugeIndex index) { + private BinaryBackendEntry formatILDeletion(Index index) { Id id = index.indexLabelId(); BinaryId bid = new BinaryId(id.asBytes(), id); BinaryBackendEntry entry = new BinaryBackendEntry(index.type(), bid); @@ -967,17 +856,14 @@ protected static BinaryId formatIndexId(HugeType type, Id indexLabel, Object fieldValues, boolean equal) { boolean withEnding = type.isRangeIndex() || equal; - Id id = HugeIndex.formatIndexId(type, indexLabel, fieldValues); - if (!type.isNumericIndex() && indexIdLengthExceedLimit(id)) { - id = HugeIndex.formatIndexHashId(type, indexLabel, fieldValues); - } + Id id = BinaryElementSerializer.schemaIndexId(type, indexLabel, fieldValues); BytesBuffer buffer = BytesBuffer.allocate(1 + id.length()); byte[] idBytes = buffer.writeIndexId(id, type, withEnding).bytes(); return new BinaryId(idBytes, id); } protected static boolean indexIdLengthExceedLimit(Id id) { - return id.asBytes().length > BytesBuffer.INDEX_HASH_ID_THRESHOLD; + return BinaryElementSerializer.indexIdLengthExceedLimit(id); } protected static boolean indexFieldValuesUnmatched(byte[] value, diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BytesBuffer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BytesBuffer.java deleted file mode 100644 index faf1508299..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BytesBuffer.java +++ /dev/null @@ -1,1030 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.backend.serializer; - -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.UUID; - -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.Id.IdType; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.serializer.BinaryBackendEntry.BinaryId; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.define.Cardinality; -import org.apache.hugegraph.type.define.DataType; -import org.apache.hugegraph.util.Blob; -import org.apache.hugegraph.util.Bytes; -import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.KryoUtil; -import org.apache.hugegraph.util.StringEncoding; - -/** - * class BytesBuffer is a util for read/write binary - */ -public final class BytesBuffer extends OutputStream { - - public static final int BYTE_LEN = Byte.BYTES; - public static final int SHORT_LEN = Short.BYTES; - public static final int INT_LEN = Integer.BYTES; - public static final int LONG_LEN = Long.BYTES; - public static final int CHAR_LEN = Character.BYTES; - public static final int FLOAT_LEN = Float.BYTES; - public static final int DOUBLE_LEN = Double.BYTES; - public static final int BYTES_LEN = 4; - public static final int BLOB_LEN = 5; - - public static final int UINT8_MAX = ((byte) -1) & 0xff; - public static final int UINT16_MAX = ((short) -1) & 0xffff; - public static final long UINT32_MAX = (-1) & 0xffffffffL; - - // TODO: support user-defined configuration - // NOTE: +1 to let code 0 represent length 1 - public static final int ID_LEN_MAX = 0x3fff + 1; // 16KB - public static final int EID_LEN_MAX = 64 * 1024; - - public static final byte STRING_ENDING_BYTE = (byte) 0x00; - public static final byte STRING_ENDING_BYTE_FF = (byte) 0xff; - - // TODO: support user-defined configuration - public static final long BYTES_LEN_MAX = 10 * Bytes.MB; - public static final long BLOB_LEN_MAX = 1 * Bytes.GB; - - public static final int MAX_PROPERTIES = BytesBuffer.UINT16_MAX; - - // The value must be in range [8, ID_LEN_MAX] - public static final int INDEX_HASH_ID_THRESHOLD = 32; - - public static final int DEFAULT_CAPACITY = 64; - public static final int MAX_BUFFER_CAPACITY = 128 * 1024 * 1024; // 128M - public static final int MAX_BUFFER_CAPACITY_UPPER_BOUND = (int) Bytes.GB; - - public static final int BUF_EDGE_ID = 128; - public static final int BUF_PROPERTY = 64; - - public static final byte[] BYTES_EMPTY = new byte[0]; - - private static volatile Integer maxBufferCapacity; - - private ByteBuffer buffer; - private final boolean resize; - - public BytesBuffer() { - this(DEFAULT_CAPACITY); - } - - public BytesBuffer(int capacity) { - int maxCapacity = maxBufferCapacity(); - if (capacity > maxCapacity) { - E.checkArgument(false, - "Capacity %s exceeds max buffer capacity: %s", - capacity, maxCapacity); - } - this.buffer = ByteBuffer.allocate(capacity); - this.resize = true; - } - - public BytesBuffer(ByteBuffer buffer) { - E.checkNotNull(buffer, "buffer"); - this.buffer = buffer; - this.resize = false; - } - - public static BytesBuffer allocate(int capacity) { - return new BytesBuffer(capacity); - } - - public static BytesBuffer wrap(ByteBuffer buffer) { - return new BytesBuffer(buffer); - } - - public static BytesBuffer wrap(byte[] array) { - return new BytesBuffer(ByteBuffer.wrap(array)); - } - - public static BytesBuffer wrap(byte[] array, int offset, int length) { - return new BytesBuffer(ByteBuffer.wrap(array, offset, length)); - } - - public static int maxBufferCapacity() { - Integer capacity = maxBufferCapacity; - return capacity != null ? capacity : MAX_BUFFER_CAPACITY; - } - - public static synchronized void initMaxBufferCapacity(int capacity) { - initMaxBufferCapacity(capacity, true); - } - - public static synchronized void initMaxBufferCapacity(int capacity, - boolean explicit) { - E.checkArgument(capacity >= DEFAULT_CAPACITY && - capacity <= MAX_BUFFER_CAPACITY_UPPER_BOUND, - "Max buffer capacity must be in range [%s, %s], " + - "but got %s", - DEFAULT_CAPACITY, MAX_BUFFER_CAPACITY_UPPER_BOUND, - capacity); - - if (!explicit) { - return; - } - - if (maxBufferCapacity == null) { - maxBufferCapacity = capacity; - return; - } - - if (maxBufferCapacity == capacity) { - return; - } - - throw new IllegalArgumentException(String.format( - "The process-wide serializer buffer max capacity has been " + - "initialized to %s, but got conflicting value %s", - maxBufferCapacity, capacity)); - } - - public ByteBuffer asByteBuffer() { - return this.buffer; - } - - public BytesBuffer forReadWritten() { - this.buffer.flip(); - return this; - } - - public BytesBuffer forReadAll() { - this.buffer.position(this.buffer.limit()); - return this; - } - - public byte[] array() { - return this.buffer.array(); - } - - public byte[] bytes() { - byte[] bytes = this.buffer.array(); - int position = this.buffer.position(); - if (position == bytes.length) { - return bytes; - } else { - return Arrays.copyOf(bytes, position); - } - } - - public int position() { - return this.buffer.position(); - } - - public BytesBuffer copyFrom(BytesBuffer other) { - this.write(other.bytes()); - return this; - } - - public int remaining() { - return this.buffer.remaining(); - } - - private void require(int size) { - // Does need to resize? - if (this.buffer.remaining() >= size) { - return; - } - // Can't resize for wrapped buffer since will change the origin ref - if (!this.resize) { - E.checkState(false, "Can't resize for wrapped buffer"); - } - - int maxCapacity = maxBufferCapacity(); - long requiredCapacity = (long) this.buffer.position() + size; - if (requiredCapacity > maxCapacity) { - E.checkArgument(false, - "Capacity %s exceeds max buffer capacity: %s", - requiredCapacity, maxCapacity); - } - - // Extra capacity as buffer - long newCapacity = Math.min(requiredCapacity + DEFAULT_CAPACITY, - maxCapacity); - ByteBuffer newBuffer = ByteBuffer.allocate((int) newCapacity); - this.buffer.flip(); - newBuffer.put(this.buffer); - this.buffer = newBuffer; - } - - public BytesBuffer write(byte val) { - require(BYTE_LEN); - this.buffer.put(val); - return this; - } - - @Override - public void write(int val) { - assert val <= UINT8_MAX; - require(BYTE_LEN); - this.buffer.put((byte) val); - } - - @Override - public void write(byte[] val) { - require(BYTE_LEN * val.length); - this.buffer.put(val); - } - - @Override - public void write(byte[] val, int offset, int length) { - require(BYTE_LEN * length); - this.buffer.put(val, offset, length); - } - - public BytesBuffer writeBoolean(boolean val) { - this.write(val ? 1 : 0); - return this; - } - - public BytesBuffer writeChar(char val) { - require(CHAR_LEN); - this.buffer.putChar(val); - return this; - } - - public BytesBuffer writeShort(short val) { - require(SHORT_LEN); - this.buffer.putShort(val); - return this; - } - - public BytesBuffer writeInt(int val) { - require(INT_LEN); - this.buffer.putInt(val); - return this; - } - - public BytesBuffer writeLong(long val) { - require(LONG_LEN); - this.buffer.putLong(val); - return this; - } - - public BytesBuffer writeFloat(float val) { - require(FLOAT_LEN); - this.buffer.putFloat(val); - return this; - } - - public BytesBuffer writeDouble(double val) { - require(DOUBLE_LEN); - this.buffer.putDouble(val); - return this; - } - - public byte peek() { - return this.buffer.get(this.buffer.position()); - } - - public byte peekLast() { - return this.buffer.get(this.buffer.limit() - 1); - } - - public byte read() { - return this.buffer.get(); - } - - public byte[] read(int length) { - byte[] bytes = new byte[length]; - this.buffer.get(bytes); - return bytes; - } - - public boolean readBoolean() { - return this.buffer.get() != 0; - } - - public char readChar() { - return this.buffer.getChar(); - } - - public short readShort() { - return this.buffer.getShort(); - } - - public int readInt() { - return this.buffer.getInt(); - } - - public long readLong() { - return this.buffer.getLong(); - } - - public float readFloat() { - return this.buffer.getFloat(); - } - - public double readDouble() { - return this.buffer.getDouble(); - } - - public BytesBuffer writeBytes(byte[] bytes) { - if (bytes.length > BYTES_LEN_MAX) { - E.checkArgument(false, - "The max length of bytes is %s, but got %s", - BYTES_LEN_MAX, bytes.length); - } - require(BYTES_LEN + bytes.length); - this.writeVInt(bytes.length); - this.write(bytes); - return this; - } - - public byte[] readBytes() { - int length = this.readVInt(); - assert length >= 0; - return this.read(length); - } - - public BytesBuffer writeBigBytes(byte[] bytes) { - if (bytes.length > BLOB_LEN_MAX) { - E.checkArgument(false, - "The max length of bytes is %s, but got %s", - BLOB_LEN_MAX, bytes.length); - } - require(BLOB_LEN + bytes.length); - this.writeVInt(bytes.length); - this.write(bytes); - return this; - } - - public byte[] readBigBytes() { - int length = this.readVInt(); - assert length >= 0; - return this.read(length); - } - - public BytesBuffer writeStringRaw(String val) { - this.write(StringEncoding.encode(val)); - return this; - } - - public BytesBuffer writeString(String val) { - byte[] bytes = StringEncoding.encode(val); - this.writeBytes(bytes); - return this; - } - - public String readString() { - return StringEncoding.decode(this.readBytes()); - } - - public BytesBuffer writeStringWithEnding(String value) { - if (!value.isEmpty()) { - byte[] bytes = StringEncoding.encode(value); - /* - * assert '0x00'/'0xFF' not exist in string index id - * NOTE: - * 0x00 is NULL in UTF8(or ASCII) bytes - * 0xFF is not a valid byte in UTF8 bytes - */ - assert !Bytes.contains(bytes, STRING_ENDING_BYTE_FF) : - "Invalid UTF8 bytes: " + value; - if (Bytes.contains(bytes, STRING_ENDING_BYTE)) { - E.checkArgument(false, "Can't contains byte '0x00' in string: '%s'", value); - } - this.write(bytes); - } - /* - * Choose 0x00 as ending symbol (see #1057) - * The following is out of date: - * A reasonable ending symbol should be 0x00(to ensure order), but - * considering that some backends like PG do not support 0x00 string, - * so choose 0xFF currently. - */ - this.write(STRING_ENDING_BYTE); - return this; - } - - public String readStringWithEnding() { - return StringEncoding.decode(this.readBytesWithEnding()); - } - - public BytesBuffer writeStringToRemaining(String value) { - byte[] bytes = StringEncoding.encode(value); - this.write(bytes); - return this; - } - - public String readStringFromRemaining() { - byte[] bytes = new byte[this.buffer.remaining()]; - this.buffer.get(bytes); - return StringEncoding.decode(bytes); - } - - public BytesBuffer writeUInt8(int val) { - assert val <= UINT8_MAX; - this.write(val); - return this; - } - - public int readUInt8() { - return this.read() & 0x000000ff; - } - - public BytesBuffer writeUInt16(int val) { - assert val <= UINT16_MAX; - this.writeShort((short) val); - return this; - } - - public int readUInt16() { - return this.readShort() & 0x0000ffff; - } - - public BytesBuffer writeUInt32(long val) { - assert val <= UINT32_MAX; - this.writeInt((int) val); - return this; - } - - public long readUInt32() { - return this.readInt() & 0xffffffffL; - } - - public BytesBuffer writeVInt(int value) { - // NOTE: negative numbers are not compressed - if (value > 0x0fffffff || value < 0) { - this.write(0x80 | ((value >>> 28) & 0x7f)); - } - if (value > 0x1fffff || value < 0) { - this.write(0x80 | ((value >>> 21) & 0x7f)); - } - if (value > 0x3fff || value < 0) { - this.write(0x80 | ((value >>> 14) & 0x7f)); - } - if (value > 0x7f || value < 0) { - this.write(0x80 | ((value >>> 7) & 0x7f)); - } - this.write(value & 0x7f); - - return this; - } - - public int readVInt() { - byte leading = this.read(); - if (leading == 0x80) { - E.checkArgument(false, - "Unexpected varint with leading byte '0x%s'", - Bytes.toHex(leading)); - } - int value = leading & 0x7f; - if (leading >= 0) { - assert (leading & 0x80) == 0; - return value; - } - - int i = 1; - for (; i < 5; i++) { - byte b = this.read(); - if (b >= 0) { - value = b | (value << 7); - break; - } else { - value = (b & 0x7f) | (value << 7); - } - } - - if (i >= 5) { - E.checkArgument(false, - "Unexpected varint %s with too many bytes(%s)", - value, i + 1); - } - if (i >= 4 && (leading & 0x70) != 0) { - E.checkArgument(false, - "Unexpected varint %s with leading byte '0x%s'", - value, Bytes.toHex(leading)); - } - return value; - } - - public BytesBuffer writeVLong(long value) { - if (value < 0) { - this.write((byte) 0x81); - } - if (value > 0xffffffffffffffL || value < 0L) { - this.write(0x80 | ((int) (value >>> 56) & 0x7f)); - } - if (value > 0x1ffffffffffffL || value < 0L) { - this.write(0x80 | ((int) (value >>> 49) & 0x7f)); - } - if (value > 0x3ffffffffffL || value < 0L) { - this.write(0x80 | ((int) (value >>> 42) & 0x7f)); - } - if (value > 0x7ffffffffL || value < 0L) { - this.write(0x80 | ((int) (value >>> 35) & 0x7f)); - } - if (value > 0xfffffffL || value < 0L) { - this.write(0x80 | ((int) (value >>> 28) & 0x7f)); - } - if (value > 0x1fffffL || value < 0L) { - this.write(0x80 | ((int) (value >>> 21) & 0x7f)); - } - if (value > 0x3fffL || value < 0L) { - this.write(0x80 | ((int) (value >>> 14) & 0x7f)); - } - if (value > 0x7fL || value < 0L) { - this.write(0x80 | ((int) (value >>> 7) & 0x7f)); - } - this.write((int) value & 0x7f); - - return this; - } - - public long readVLong() { - byte leading = this.read(); - if (leading == 0x80) { - E.checkArgument(false, - "Unexpected varlong with leading byte '0x%s'", - Bytes.toHex(leading)); - } - long value = leading & 0x7fL; - if (leading >= 0) { - assert (leading & 0x80) == 0; - return value; - } - - int i = 1; - for (; i < 10; i++) { - byte b = this.read(); - if (b >= 0) { - value = b | (value << 7); - break; - } else { - value = (b & 0x7f) | (value << 7); - } - } - - if (i >= 10) { - E.checkArgument(false, - "Unexpected varlong %s with too many bytes(%s)", - value, i + 1); - } - if (i >= 9 && (leading & 0x7e) != 0) { - E.checkArgument(false, - "Unexpected varlong %s with leading byte '0x%s'", - value, Bytes.toHex(leading)); - } - return value; - } - - public BytesBuffer writeProperty(PropertyKey pkey, Object value) { - if (pkey.cardinality() == Cardinality.SINGLE) { - this.writeProperty(pkey.dataType(), value); - return this; - } - - assert pkey.cardinality() == Cardinality.LIST || - pkey.cardinality() == Cardinality.SET; - Collection values = (Collection) value; - this.writeVInt(values.size()); - for (Object o : values) { - this.writeProperty(pkey.dataType(), o); - } - return this; - } - - public Object readProperty(PropertyKey pkey) { - if (pkey.cardinality() == Cardinality.SINGLE) { - return this.readProperty(pkey.dataType()); - } - - assert pkey.cardinality() == Cardinality.LIST || - pkey.cardinality() == Cardinality.SET; - int size = this.readVInt(); - Collection values = pkey.newValue(); - for (int i = 0; i < size; i++) { - values.add(this.readProperty(pkey.dataType())); - } - return values; - } - - public void writeProperty(DataType dataType, Object value) { - switch (dataType) { - case BOOLEAN: - this.writeVInt(((Boolean) value) ? 1 : 0); - break; - case BYTE: - this.writeVInt((Byte) value); - break; - case INT: - this.writeVInt((Integer) value); - break; - case FLOAT: - this.writeFloat((Float) value); - break; - case LONG: - this.writeVLong((Long) value); - break; - case DATE: - this.writeVLong(((Date) value).getTime()); - break; - case DOUBLE: - this.writeDouble((Double) value); - break; - case TEXT: - this.writeString((String) value); - break; - case BLOB: - byte[] bytes = value instanceof byte[] ? (byte[]) value : ((Blob) value).bytes(); - this.writeBigBytes(bytes); - break; - case UUID: - UUID uuid = (UUID) value; - // Generally writeVLong(uuid) can't save space - this.writeLong(uuid.getMostSignificantBits()); - this.writeLong(uuid.getLeastSignificantBits()); - break; - default: - // TODO: replace Kryo with Fury (https://github.com/apache/fury) - this.writeBytes(KryoUtil.toKryoWithType(value)); - break; - } - } - - public static byte getType(int value) { - return (byte) (value & 0x3f); - } - - public Object readProperty(DataType dataType) { - switch (dataType) { - case BOOLEAN: - return this.readVInt() == 1; - case BYTE: - return (byte) this.readVInt(); - case INT: - return this.readVInt(); - case FLOAT: - return this.readFloat(); - case LONG: - return this.readVLong(); - case DATE: - return new Date(this.readVLong()); - case DOUBLE: - return this.readDouble(); - case TEXT: - return this.readString(); - case BLOB: - return Blob.wrap(this.readBigBytes()); - case UUID: - return new UUID(this.readLong(), this.readLong()); - default: - // TODO: replace Kryo with Fury (https://github.com/apache/fury) - return KryoUtil.fromKryoWithType(this.readBytes()); - } - } - - public BytesBuffer writeId(Id id) { - switch (id.type()) { - case LONG: - // Number Id - long value = id.asLong(); - this.writeNumber(value); - break; - case UUID: - // UUID ID - byte[] bytes = id.asBytes(); - assert bytes.length == Id.UUID_LENGTH; - this.writeUInt8(0x7f); // 0b01111111 means UUID - this.write(bytes); - break; - case EDGE: - // Edge ID - this.writeUInt8(0x7e); // 0b01111110 means EdgeId - this.writeEdgeId(id); - break; - default: - // String Id (VertexID) - bytes = id.asBytes(); - int len = bytes.length; - if (len <= 0) { - E.checkArgument(false, "Can't write empty id"); - } - if (len > ID_LEN_MAX) { - E.checkArgument(false, "Big id max length is %s, but got %s {%s}", - ID_LEN_MAX, len, id); - } - len -= 1; // mapping [1, 16384] to [0, 16383] - if (len <= 0x3f) { - // If length is <= 63, use a single byte with the highest bit set to 1 - this.writeUInt8(len | 0x80); - } else { - int high = len >> 8; - int low = len & 0xff; - // Write high 8 bits with highest two bits set to 11 - this.writeUInt8(high | 0xc0); - this.writeUInt8(low); - } - this.write(bytes); - break; - } - return this; - } - - public Id readId() { - byte b = this.read(); - boolean number = (b & 0x80) == 0; - if (number) { - if (b == 0x7f) { - // UUID Id - return IdGenerator.of(this.read(Id.UUID_LENGTH), IdType.UUID); - } else if (b == 0x7e) { - // Edge Id - return this.readEdgeId(); - } else { - // Number Id - return IdGenerator.of(this.readNumber(b)); - } - } else { - // String Id - int len = b & 0x3f; // Take the lowest 6 bits as part of the length - if ((b & 0x40) != 0) { // If the 7th bit is set, length information spans 2 bytes - int high = len << 8; - int low = this.readUInt8(); - len = high + low; - } - len += 1; // restore [0, 16383] to [1, 16384] - byte[] id = this.read(len); - return IdGenerator.of(id, IdType.STRING); - } - } - - public BytesBuffer writeEdgeId(Id id) { - // owner-vertex + dir + edge-label + sub-edge-label + sort-values + other-vertex - EdgeId edge = (EdgeId) id; - this.writeId(edge.ownerVertexId()); - this.write(edge.directionCode()); - this.writeId(edge.edgeLabelId()); - this.writeId(edge.subLabelId()); - this.writeStringWithEnding(edge.sortValues()); - this.writeId(edge.otherVertexId()); - return this; - } - - public Id readEdgeId() { - return new EdgeId(this.readId(), EdgeId.directionFromCode(this.read()), - this.readId(), this.readId(), this.readStringWithEnding(), - this.readId()); - } - - public BytesBuffer writeIndexId(Id id, HugeType type) { - return this.writeIndexId(id, type, true); - } - - public BytesBuffer writeIndexId(Id id, HugeType type, boolean withEnding) { - byte[] bytes = id.asBytes(); - int len = bytes.length; - if (len == 0) { - E.checkArgument(false, "Can't write empty id"); - } - - this.write(bytes); - if (type.isStringIndex()) { - if (Bytes.contains(bytes, STRING_ENDING_BYTE)) { - // Not allow STRING_ENDING_BYTE to exist in string index id - E.checkArgument(false, - "The %s type index id can't contains " + - "byte '0x%s', but got: 0x%s", - type, Bytes.toHex(STRING_ENDING_BYTE), Bytes.toHex(bytes)); - } - if (withEnding) { - this.writeStringWithEnding(""); - } - } - return this; - } - - public BinaryId readIndexId(HugeType type) { - byte[] id; - if (type.isRange4Index()) { - // HugeTypeCode 1 byte + IndexLabel 4 bytes + fieldValue 4 bytes - id = this.read(9); - } else if (type.isRange8Index()) { - // HugeTypeCode 1 byte + IndexLabel 4 bytes + fieldValue 8 bytes - id = this.read(13); - } else { - assert type.isStringIndex(); - id = this.readBytesWithEnding(); - } - return new BinaryId(id, IdGenerator.of(id, IdType.STRING)); - } - - public BinaryId asId() { - return new BinaryId(this.bytes(), null); - } - - public BinaryId parseId(HugeType type, boolean enablePartition) { - if (type.isIndex()) { - return this.readIndexId(type); - } - // Parse id from bytes - if ((type.isVertex() || type.isEdge()) && enablePartition) { - this.readShort(); - } - int start = this.buffer.position(); - /* - * Since edge id in edges table doesn't prefix with leading 0x7e, - * so readId() will return the source vertex id instead of edge id, - * can't call: type.isEdge() ? this.readEdgeId() : this.readId(); - */ - Id id = this.readId(); - int end = this.buffer.position(); - int len = end - start; - byte[] bytes = new byte[len]; - System.arraycopy(this.array(), start, bytes, 0, len); - return new BinaryId(bytes, id); - } - - /** - * Analyze olap id - * @param type - * @param isOlap - * @return - */ - public BinaryId parseOlapId(HugeType type, boolean isOlap) { - if (type.isIndex()) { - return this.readIndexId(type); - } - // Parse id from bytes - int start = this.buffer.position(); - // OLAP {PropertyKey}{VertexId} - if (isOlap) { - // Read olap property id first - Id pkId = this.readId(); - } - Id id = this.readId(); - int end = this.buffer.position(); - int len = end - start; - byte[] bytes = new byte[len]; - System.arraycopy(this.array(), start, bytes, 0, len); - return new BinaryId(bytes, id); - } - - private void writeNumber(long val) { - /* - * 8 kinds of number, 2 ~ 9 bytes number: - * 0b 0kkksxxx X... - * 0(1 bit) + kind(3 bits) + signed(1 bit) + number(n bits) - * - * 2 byte : 0b 0000 1xxx X(8 bits) [0, 2047] - * 0b 0000 0xxx X(8 bits) [-2048, -1] - * 3 bytes: 0b 0001 1xxx X X [0, 524287] - * 0b 0001 0xxx X X [-524288, -1] - * 4 bytes: 0b 0010 1xxx X X X [0, 134217727] - * 0b 0010 0xxx X X X [-134217728, -1] - * 5 bytes: 0b 0011 1xxx X X X X [0, 2^35 - 1] - * 0b 0011 0xxx X X X X [-2^35, -1] - * 6 bytes: 0b 0100 1xxx X X X X X [0, 2^43 - 1] - * 0b 0100 0xxx X X X X X [-2^43, -1] - * 7 bytes: 0b 0101 1xxx X X X X X X [0, 2^51 - 1] - * 0b 0101 0xxx X X X X X X [-2^51, -1] - * 8 bytes: 0b 0110 1xxx X X X X X X X [0, 2^59 - 1] - * 0b 0110 0xxx X X X X X X X [-2^59, -1] - * 9 bytes: 0b 0111 1000 X X X X X X X X [0, 2^64 - 1] - * 0b 0111 0000 X X X X X X X X [-2^64, -1] - * - * NOTE: 0b 0111 1111 is used by 128 bits UUID - * 0b 0111 1110 is used by EdgeId - */ - int positive = val >= 0 ? 0x08 : 0x00; - if (~0x7ffL <= val && val <= 0x7ffL) { - int high3bits = (int) (val >> 8) & 0x07; - this.writeUInt8(0x00 | positive | high3bits); - this.writeUInt8((byte) val); - } else if (~0x7ffffL <= val && val <= 0x7ffffL) { - int high3bits = (int) (val >> 16) & 0x07; - this.writeUInt8(0x10 | positive | high3bits); - this.writeShort((short) val); - } else if (~0x7ffffffL <= val && val <= 0x7ffffffL) { - int high3bits = (int) (val >> 24 & 0x07); - this.writeUInt8(0x20 | positive | high3bits); - this.write((byte) (val >> 16)); - this.writeShort((short) val); - } else if (~0x7ffffffffL <= val && val <= 0x7ffffffffL) { - int high3bits = (int) (val >> 32) & 0x07; - this.writeUInt8(0x30 | positive | high3bits); - this.writeInt((int) val); - } else if (~0x7ffffffffffL <= val && val <= 0x7ffffffffffL) { - int high3bits = (int) (val >> 40) & 0x07; - this.writeUInt8(0x40 | positive | high3bits); - this.write((byte) (val >> 32)); - this.writeInt((int) val); - } else if (~0x7ffffffffffffL <= val && val <= 0x7ffffffffffffL) { - int high3bits = (int) (val >> 48) & 0x07; - this.writeUInt8(0x50 | positive | high3bits); - this.writeShort((short) (val >> 32)); - this.writeInt((int) val); - } else if (~0x7ffffffffffffffL <= val && val <= 0x7ffffffffffffffL) { - int high3bits = (int) (val >> 56) & 0x07; - this.writeUInt8(0x60 | positive | high3bits); - this.write((byte) (val >> 48)); - this.writeShort((short) (val >> 32)); - this.writeInt((int) val); - } else { - // high3bits is always 0b000 for 9 bytes number - this.writeUInt8(0x70 | positive); - this.writeLong(val); - } - } - - private long readNumber(byte b) { - if ((b & 0x80) != 0) { - E.checkArgument(false, - "Not a number type with prefix byte '0x%s'", - Bytes.toHex(b)); - } - // Parse the kind from byte 0kkksxxx - int kind = b >>> 4; - boolean positive = (b & 0x08) > 0; - long high3bits = b & 0x07; - long value = high3bits << ((kind + 1) * 8); - switch (kind) { - case 0: - value |= this.readUInt8(); - break; - case 1: - value |= this.readUInt16(); - break; - case 2: - value |= (long) this.readUInt8() << 16 | this.readUInt16(); - break; - case 3: - value |= this.readUInt32(); - break; - case 4: - value |= (long) this.readUInt8() << 32 | this.readUInt32(); - break; - case 5: - value |= (long) this.readUInt16() << 32 | this.readUInt32(); - break; - case 6: - value |= (long) this.readUInt8() << 48 | - (long) this.readUInt16() << 32 | - this.readUInt32(); - break; - case 7: - assert high3bits == 0L; - value |= this.readLong(); - break; - default: - throw new AssertionError("Invalid length of number: " + kind); - } - if (!positive && kind < 7) { - // Restore the bits of the original negative number - long mask = Long.MIN_VALUE >> (52 - kind * 8); - value |= mask; - } - return value; - } - - private byte[] readBytesWithEnding() { - int start = this.buffer.position(); - boolean foundEnding = false; - int remaining = this.remaining(); - for (int i = 0; i < remaining; i++) { - byte current = this.read(); - if (current == STRING_ENDING_BYTE) { - foundEnding = true; - break; - } - } - if (!foundEnding) { - E.checkArgument(false, - "Not found ending '0x%s'", - Bytes.toHex(STRING_ENDING_BYTE)); - } - int end = this.buffer.position() - 1; - int len = end - start; - if (len <= 0) { - return BYTES_EMPTY; - } - byte[] bytes = new byte[len]; - System.arraycopy(this.array(), start, bytes, 0, len); - return bytes; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/GraphSerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/GraphSerializer.java index 2d14df1c84..548ddba7ad 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/GraphSerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/GraphSerializer.java @@ -18,14 +18,14 @@ package org.apache.hugegraph.backend.serializer; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.iterator.CIter; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeEdgeProperty; -import org.apache.hugegraph.structure.HugeIndex; +import org.apache.hugegraph.structure.Index; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.structure.HugeVertexProperty; import org.apache.hugegraph.type.HugeType; @@ -49,9 +49,9 @@ public interface GraphSerializer { CIter readEdges(HugeGraph graph, BackendEntry bytesEntry); - BackendEntry writeIndex(HugeIndex index); + BackendEntry writeIndex(Index index); - HugeIndex readIndex(HugeGraph graph, ConditionQuery query, BackendEntry entry); + Index readIndex(HugeGraph graph, ConditionQuery query, BackendEntry entry); BackendEntry writeId(HugeType type, Id id); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/SchemaSerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/SchemaSerializer.java index 74e2dcc09f..aea85495b7 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/SchemaSerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/SchemaSerializer.java @@ -19,10 +19,10 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; public interface SchemaSerializer { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/SerializerFactory.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/SerializerFactory.java index db5fe0a8cd..42962d5757 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/SerializerFactory.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/SerializerFactory.java @@ -20,7 +20,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.config.HugeConfig; public class SerializerFactory { @@ -72,13 +72,11 @@ public static void register(String name, String classPath) { "AbstractSerializer: '%s'", classPath); } - // Check exists - if (serializers.containsKey(name)) { + // Register atomically: identical re-registration is a no-op + Class registered = serializers.putIfAbsent(name, (Class) clazz); + if (registered != null && !registered.equals(clazz)) { throw new BackendException("Exists serializer: %s(Class '%s')", - name, serializers.get(name).getName()); + name, registered.getName()); } - - // Register class - serializers.put(name, (Class) clazz); } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TableBackendEntry.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TableBackendEntry.java index 918a95b511..34d15e2b11 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TableBackendEntry.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TableBackendEntry.java @@ -27,7 +27,8 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.lang3.NotImplementedException; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; @@ -252,17 +253,17 @@ public int columnsSize() { } @Override - public Collection columns() { + public Collection columns() { throw new NotImplementedException("Not supported by table backend"); } @Override - public void columns(Collection bytesColumns) { + public void columns(Collection bytesColumns) { throw new NotImplementedException("Not supported by table backend"); } @Override - public void columns(BackendEntry.BackendColumn bytesColumn) { + public void columns(BackendColumn bytesColumn) { throw new NotImplementedException("Not supported by table backend"); } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TableSerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TableSerializer.java index a2e3279976..e322e90d11 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TableSerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TableSerializer.java @@ -23,26 +23,26 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.id.IdUtil; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.IdUtil; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.config.HugeConfig; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.SchemaLabel; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeEdgeProperty; import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.structure.HugeIndex; +import org.apache.hugegraph.structure.Index; import org.apache.hugegraph.structure.HugeProperty; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.structure.HugeVertexProperty; @@ -83,7 +83,7 @@ protected TableBackendEntry newBackendEntry(SchemaElement e) { return newBackendEntry(e.type(), e.id()); } - protected TableBackendEntry newBackendEntry(HugeIndex index) { + protected TableBackendEntry newBackendEntry(Index index) { return newBackendEntry(index.type(), index.id()); } @@ -305,7 +305,7 @@ public HugeEdge readEdge(HugeGraph graph, BackendEntry backendEntry) { } @Override - public BackendEntry writeIndex(HugeIndex index) { + public BackendEntry writeIndex(Index index) { TableBackendEntry entry = newBackendEntry(index); /* * When field-values is null and elementIds size is 0, it is @@ -327,7 +327,7 @@ public BackendEntry writeIndex(HugeIndex index) { } @Override - public HugeIndex readIndex(HugeGraph graph, ConditionQuery query, + public Index readIndex(HugeGraph graph, ConditionQuery query, BackendEntry backendEntry) { E.checkNotNull(graph, "serializer graph"); if (backendEntry == null) { @@ -342,7 +342,7 @@ public HugeIndex readIndex(HugeGraph graph, ConditionQuery query, Number expiredTime = entry.column(HugeKeys.EXPIRED_TIME); IndexLabel indexLabel = graph.indexLabel(this.toId(indexLabelId)); - HugeIndex index = new HugeIndex(graph, indexLabel); + Index index = new Index(graph, indexLabel); index.fieldValues(indexValues); long expired = index.hasTtl() ? expiredTime.longValue() : 0L; for (Object elemId : elemIds) { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TableSerializerV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TableSerializerV2.java index 54f9e4cf54..b2ec49bde6 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TableSerializerV2.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TableSerializerV2.java @@ -18,13 +18,13 @@ package org.apache.hugegraph.backend.serializer; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.config.HugeConfig; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeEdgeProperty; import org.apache.hugegraph.structure.HugeVertex; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextBackendEntry.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextBackendEntry.java index 2dfdb2cba9..f1878fde35 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextBackendEntry.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextBackendEntry.java @@ -27,8 +27,9 @@ import java.util.Set; import java.util.concurrent.ConcurrentSkipListMap; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.HugeKeys; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextSerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextSerializer.java index 2d5cb81ec1..0d66a76446 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextSerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextSerializer.java @@ -24,32 +24,32 @@ import java.util.Map; import org.apache.commons.lang.NotImplementedException; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.id.IdUtil; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdPrefixQuery; -import org.apache.hugegraph.backend.query.IdRangeQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.IdUtil; +import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdPrefixQuery; +import org.apache.hugegraph.query.IdRangeQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.iterator.CIter; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeEdgeProperty; import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.structure.HugeIndex; -import org.apache.hugegraph.structure.HugeIndex.IdWithExpiredTime; +import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.structure.Index.IdWithExpiredTime; import org.apache.hugegraph.structure.HugeProperty; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.structure.HugeVertexProperty; @@ -363,7 +363,7 @@ public CIter readEdges(HugeGraph graph, BackendEntry bytesEntry) { } @Override - public BackendEntry writeIndex(HugeIndex index) { + public BackendEntry writeIndex(Index index) { TextBackendEntry entry = newBackendEntry(index.type(), index.id()); if (index.fieldValues() == null && index.elementIds().isEmpty()) { /* @@ -386,7 +386,7 @@ public BackendEntry writeIndex(HugeIndex index) { } @Override - public HugeIndex readIndex(HugeGraph graph, ConditionQuery query, + public Index readIndex(HugeGraph graph, ConditionQuery query, BackendEntry backendEntry) { E.checkNotNull(graph, "serializer graph"); if (backendEntry == null) { @@ -402,7 +402,7 @@ public HugeIndex readIndex(HugeGraph graph, ConditionQuery query, formatSyspropName(HugeKeys.ELEMENT_IDS)); IndexLabel indexLabel = IndexLabel.label(graph, readId(indexLabelId)); - HugeIndex index = new HugeIndex(graph, indexLabel); + Index index = new Index(graph, indexLabel); index.fieldValues(JsonUtil.fromJson(indexValues, Object.class)); for (IdWithExpiredTime elemId : readElementIds(elemIds)) { long expiredTime = elemId.expiredTime(); @@ -457,7 +457,7 @@ private Query writeQueryEdgeRangeCondition(ConditionQuery cq) { if (direction == null) { direction = Directions.OUT; } - Object label = cq.condition(HugeKeys.LABEL); + Object label = this.edgeIdConditionValue(cq, HugeKeys.LABEL); List start = new ArrayList<>(cq.conditionsSize()); start.add(writeEntryId((Id) vertex)); @@ -491,7 +491,7 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) { List condParts = new ArrayList<>(cq.conditionsSize()); for (HugeKeys key : EdgeId.KEYS) { - Object value = cq.condition(key); + Object value = this.edgeIdConditionValue(cq, key); if (value == null) { break; } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/AbstractBackendStore.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/AbstractBackendStore.java index adb3c8c400..89e9fd8ffc 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/AbstractBackendStore.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/AbstractBackendStore.java @@ -18,10 +18,10 @@ package org.apache.hugegraph.backend.store; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.query.ConditionQueryFlatten; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.exception.ConnectionException; import org.apache.hugegraph.iterator.ExtendableIterator; import org.apache.hugegraph.iterator.FlatMapperIterator; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/AbstractBackendStoreProvider.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/AbstractBackendStoreProvider.java index ff9ff00d4d..2ec8662375 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/AbstractBackendStoreProvider.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/AbstractBackendStoreProvider.java @@ -21,7 +21,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.store.raft.StoreSnapshotFile; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendEntry.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendEntry.java index b1a5f89dc1..3e40668b76 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendEntry.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendEntry.java @@ -21,60 +21,16 @@ import java.util.Iterator; import java.util.NoSuchElementException; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.iterator.WrappedIterator; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.Idfiable; import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.StringEncoding; public interface BackendEntry extends Idfiable { - class BackendColumn implements Comparable { - - public byte[] name; - public byte[] value; - - public static BackendColumn of(byte[] name, byte[] value) { - BackendColumn col = new BackendColumn(); - col.name = name; - col.value = value; - return col; - } - - @Override - public String toString() { - return String.format("%s=%s", - StringEncoding.decode(name), - StringEncoding.decode(value)); - } - - @Override - public int compareTo(BackendColumn other) { - if (other == null) { - return 1; - } - return Bytes.compare(this.name, other.name); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof BackendColumn)) { - return false; - } - BackendColumn other = (BackendColumn) obj; - return Bytes.equals(this.name, other.name) && - Bytes.equals(this.value, other.value); - } - - public int hashCode() { - return this.name.hashCode() ^ - this.value.hashCode(); - } - - } - HugeType type(); @Override diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendEntryIterator.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendEntryIterator.java index 20469f77aa..e5d2acaa40 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendEntryIterator.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendEntryIterator.java @@ -19,10 +19,10 @@ import java.util.NoSuchElementException; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.page.PageInfo; import org.apache.hugegraph.backend.page.PageState; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.exception.LimitExceedException; import org.apache.hugegraph.exception.NotSupportException; import org.apache.hugegraph.iterator.CIter; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendMutation.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendMutation.java index 5b17fdf8e9..9d301ac700 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendMutation.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendMutation.java @@ -23,8 +23,8 @@ import java.util.Map; import java.util.Set; -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.iterator.ExtendableIterator; import org.apache.hugegraph.perf.PerfUtil.Watched; import org.apache.hugegraph.type.HugeType; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendProviderFactory.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendProviderFactory.java index ac1e0018ce..c8e94d145f 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendProviderFactory.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendProviderFactory.java @@ -23,7 +23,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.store.memory.InMemoryDBStoreProvider; import org.apache.hugegraph.backend.store.raft.RaftBackendStoreProvider; import org.apache.hugegraph.config.CoreOptions; @@ -114,12 +114,10 @@ public static void register(String name, String classPath) { BackendException.check(subclass, "Class '%s' is not a subclass of " + "class BackendStoreProvider", classPath); - // Check exists - BackendException.check(!providers.containsKey(name), + // Register atomically: identical re-registration is a no-op + Class registered = providers.putIfAbsent(name, (Class) clazz); + BackendException.check(registered == null || registered.equals(clazz), "Exists BackendStoreProvider: %s (%s)", - name, providers.get(name)); - - // Register class - providers.put(name, (Class) clazz); + name, registered); } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendSessionPool.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendSessionPool.java index 521f44556f..518247159b 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendSessionPool.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendSessionPool.java @@ -25,6 +25,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; import org.slf4j.Logger; @@ -56,14 +57,18 @@ public HugeConfig config() { public final BackendSession getOrNewSession() { BackendSession session = this.threadLocalSession.get(); if (session == null) { - session = this.newSession(); - assert session != null; - this.threadLocalSession.set(session); - assert !this.sessions.containsKey(Thread.currentThread().getId()); - this.sessions.put(Thread.currentThread().getId(), session); - int sessionCount = this.sessionCount.incrementAndGet(); - LOG.debug("Now(after connect({})) session count is: {}", - this, sessionCount); + // Serialize new borrowers with the last-session native close. + synchronized (this) { + E.checkState(this.opened(), "Backend session pool is closed"); + session = this.newSession(); + assert session != null; + this.threadLocalSession.set(session); + assert !this.sessions.containsKey(Thread.currentThread().getId()); + this.sessions.put(Thread.currentThread().getId(), session); + int sessionCount = this.sessionCount.incrementAndGet(); + LOG.debug("Now(after connect({})) session count is: {}", + this, sessionCount); + } } else { this.detectSession(session); } @@ -131,7 +136,7 @@ public void forceResetSessions() { } } - public boolean close() { + public synchronized boolean close() { Pair result = Pair.of(-1, -1); try { result = this.closeSession(); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendStore.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendStore.java index 6486d353c1..a1a28b285e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendStore.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendStore.java @@ -22,9 +22,9 @@ import java.util.function.Function; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendTable.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendTable.java index a9ee4b5c72..91b6ef7922 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendTable.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/BackendTable.java @@ -24,9 +24,10 @@ import java.util.List; import org.apache.commons.lang3.NotImplementedException; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.backend.Shard; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.type.define.HugeKeys; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/Shard.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/Shard.java deleted file mode 100644 index 3f1d2bf9e8..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/Shard.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.backend.store; - -/** - * Shard is used for distributed backend storage scanning. - * operations. Each shard represents a range of tokens for a node. - * Reading data from a given shard does not cross multiple nodes. - */ -public class Shard { - - // token range start - private String start; - // token range end - private String end; - // partitions count in this range - private long length; - - public Shard(String start, String end, long length) { - this.start = start; - this.end = end; - this.length = length; - } - - public String start() { - return this.start; - } - - public void start(String start) { - this.start = start; - } - - public String end() { - return this.end; - } - - public void end(String end) { - this.end = end; - } - - public long length() { - return this.length; - } - - public void length(long length) { - this.length = length; - } - - @Override - public String toString() { - return String.format("Shard{start=%s, end=%s, length=%s}", - this.start, this.end, this.length); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/SystemSchemaStore.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/SystemSchemaStore.java index 1ce7700ad0..d32cc3514c 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/SystemSchemaStore.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/SystemSchemaStore.java @@ -19,8 +19,8 @@ import java.util.Map; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.schema.SchemaElement; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.struct.schema.SchemaElement; import org.apache.hugegraph.type.define.CollectionType; import org.apache.hugegraph.util.collection.CollectionFactory; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/memory/InMemoryDBStore.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/memory/InMemoryDBStore.java index c482749744..3368256482 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/memory/InMemoryDBStore.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/memory/InMemoryDBStore.java @@ -22,10 +22,10 @@ import java.util.Iterator; import java.util.Map; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.LocalCounter; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.serializer.TextBackendEntry; import org.apache.hugegraph.backend.store.AbstractBackendStore; import org.apache.hugegraph.backend.store.BackendAction; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/memory/InMemoryDBTable.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/memory/InMemoryDBTable.java index 4768080905..ce8d5ec4f3 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/memory/InMemoryDBTable.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/memory/InMemoryDBTable.java @@ -25,21 +25,21 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Aggregate; -import org.apache.hugegraph.backend.query.Aggregate.AggregateFunc; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdPrefixQuery; -import org.apache.hugegraph.backend.query.IdRangeQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Aggregate; +import org.apache.hugegraph.query.Aggregate.AggregateFunc; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdPrefixQuery; +import org.apache.hugegraph.query.IdRangeQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.serializer.TextBackendEntry; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendSession; import org.apache.hugegraph.backend.store.BackendTable; -import org.apache.hugegraph.backend.store.Shard; +import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.exception.NotSupportException; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/memory/InMemoryDBTables.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/memory/InMemoryDBTables.java index 445c28a5d6..8ce4a8e86f 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/memory/InMemoryDBTables.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/memory/InMemoryDBTables.java @@ -29,21 +29,21 @@ import java.util.SortedMap; import java.util.concurrent.ConcurrentSkipListMap; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.Condition.RangeConditions; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.Condition.RangeConditions; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.serializer.TextBackendEntry; import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendSession; import org.apache.hugegraph.iterator.ExtendableIterator; -import org.apache.hugegraph.structure.HugeIndex; +import org.apache.hugegraph.structure.Index; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.util.E; @@ -429,7 +429,7 @@ public Iterator query(BackendSession session, } assert fieldValue != null && indexLabelId != null; - Id id = HugeIndex.formatIndexId(query.resultType(), + Id id = Index.formatIndexId(query.resultType(), indexLabelId, fieldValue); IdQuery q = new IdQuery(query, id); q.offset(query.offset()); @@ -502,7 +502,7 @@ public Iterator query(BackendSession session, RangeConditions range = new RangeConditions(relations); if (range.keyEq() != null) { - Id id = HugeIndex.formatIndexId(query.resultType(), + Id id = Index.formatIndexId(query.resultType(), indexLabelId, range.keyEq()); IdQuery q = new IdQuery(query, id); q.offset(query.offset()); @@ -529,7 +529,7 @@ private Iterator betweenQuery(Id indexLabelId, // Field value < keyMax keyMin = NumericUtil.minValueOf(keyMax.getClass()); } - Id min = HugeIndex.formatIndexId(type, indexLabelId, keyMin); + Id min = Index.formatIndexId(type, indexLabelId, keyMin); if (keyMax == null) { // Field value > keyMin @@ -537,7 +537,7 @@ private Iterator betweenQuery(Id indexLabelId, indexLabelId = IdGenerator.of(indexLabelId.asLong() + 1L); keyMax = NumericUtil.minValueOf(keyMin.getClass()); } - Id max = HugeIndex.formatIndexId(type, indexLabelId, keyMax); + Id max = Index.formatIndexId(type, indexLabelId, keyMax); max = keyMaxEq ? rs.floorKey(max) : rs.lowerKey(max); if (max == null) { @@ -569,9 +569,9 @@ public void delete(BackendSession session, TextBackendEntry entry) { return; } Id indexLabelId = IdGenerator.of(Long.parseLong(indexLabel)); - Id min = HugeIndex.formatIndexId(entry.type(), indexLabelId, 0L); + Id min = Index.formatIndexId(entry.type(), indexLabelId, 0L); indexLabelId = IdGenerator.of(indexLabelId.asLong() + 1L); - Id max = HugeIndex.formatIndexId(entry.type(), indexLabelId, 0L); + Id max = Index.formatIndexId(entry.type(), indexLabelId, 0L); SortedMap subStore; subStore = this.store().subMap(min, max); Iterator> iter; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftBackendStore.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftBackendStore.java index 1765562127..cbdd5c5bd1 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftBackendStore.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftBackendStore.java @@ -23,8 +23,8 @@ import java.util.function.Function; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendFeatures; import org.apache.hugegraph.backend.store.BackendMutation; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftBackendStoreProvider.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftBackendStoreProvider.java index ecdff19bd5..b704efdaf4 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftBackendStoreProvider.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftBackendStoreProvider.java @@ -21,7 +21,7 @@ import java.util.concurrent.Future; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.store.BackendStore; import org.apache.hugegraph.backend.store.BackendStoreProvider; import org.apache.hugegraph.backend.store.raft.rpc.RaftRequests.StoreAction; @@ -29,6 +29,7 @@ import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.event.EventHub; import org.apache.hugegraph.event.EventListener; +import org.apache.hugegraph.util.CloseFailure; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Events; import org.apache.hugegraph.util.Log; @@ -44,6 +45,9 @@ public class RaftBackendStoreProvider implements BackendStoreProvider { private final BackendStoreProvider provider; private final RaftContext context; + private boolean providerClosed; + private boolean contextClosed; + private RaftBackendStore schemaStore; private RaftBackendStore graphStore; private RaftBackendStore systemStore; @@ -158,10 +162,29 @@ public void waitReady(RpcServer rpcServer) { } @Override - public void close() { - this.provider.close(); - if (this.context != null) { - this.context.close(); + public synchronized void close() { + Throwable failure = null; + try { + if (!this.providerClosed) { + this.provider.close(); + this.providerClosed = true; + } + } catch (RuntimeException | Error error) { + failure = error; + } + try { + if (!this.contextClosed && this.context != null) { + this.context.close(); + this.contextClosed = true; + } + } catch (RuntimeException | Error error) { + failure = CloseFailure.collect(failure, error); + } + if (failure instanceof Error) { + throw (Error) failure; + } + if (failure != null) { + throw (RuntimeException) failure; } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftClosure.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftClosure.java index bd4e45da42..eb6be8cd0b 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftClosure.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftClosure.java @@ -23,7 +23,7 @@ import java.util.concurrent.TimeoutException; import java.util.function.Supplier; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.util.Log; import org.slf4j.Logger; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftContext.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftContext.java index 3928a4127f..5545da83b4 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftContext.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftContext.java @@ -28,13 +28,14 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import org.apache.commons.io.FileUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.backend.cache.Cache; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.BackendAction; import org.apache.hugegraph.backend.store.BackendMutation; import org.apache.hugegraph.backend.store.BackendStore; @@ -53,6 +54,7 @@ import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.GraphMode; import org.apache.hugegraph.util.Bytes; +import org.apache.hugegraph.util.CloseFailure; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Events; import org.apache.hugegraph.util.Log; @@ -102,6 +104,9 @@ public final class RaftContext { private final ExecutorService backendExecutor; private RpcServer raftRpcServer; + private boolean nodeClosed; + private boolean rpcClosed; + private boolean sharedRpcServer; private PeerId endpoint; private RaftNode raftNode; @@ -169,15 +174,61 @@ public void waitRaftNodeStarted() { node.waitRaftLogSynced(RaftContext.NO_TIMEOUT); } - public void close() { + public synchronized void close() { LOG.info("Stop raft server: {}", this.endpoint()); - RaftNode node = this.node(); - if (node != null) { - node.shutdown(); + Throwable failure = null; + try { + RaftNode node = this.node(); + if (!this.nodeClosed) { + if (node != null) { + node.shutdown(); + } + this.nodeClosed = true; + } + } catch (RuntimeException | Error error) { + failure = error; + } + try { + this.shutdownRpcServer(); + } catch (RuntimeException | Error error) { + failure = CloseFailure.collect(failure, error); + } + // Only a joined node has stopped producing work for these per-context executors. + if (this.nodeClosed) { + for (ExecutorService executor : new ExecutorService[]{this.readIndexExecutor, + this.snapshotExecutor, + this.backendExecutor}) { + try { + this.closeExecutor(executor); + } catch (RuntimeException | Error error) { + failure = CloseFailure.collect(failure, error); + } + } + } + if (failure instanceof Error) { + throw (Error) failure; + } + if (failure != null) { + throw (RuntimeException) failure; } + } - this.shutdownRpcServer(); + private void closeExecutor(ExecutorService executor) { + if (executor == null || executor.isTerminated()) { + return; + } + // Keep queued work: shutdownNow() would discard pending apply/snapshot operations. + executor.shutdown(); + try { + long timeout = this.config().get(CoreOptions.TASK_WAIT_TIMEOUT); + if (!executor.awaitTermination(timeout, TimeUnit.SECONDS)) { + throw new RaftException("Raft executor is still active, please retry close later"); + } + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new RaftException("Interrupted while draining raft executor", interrupted); + } } public RaftNode node() { @@ -372,6 +423,7 @@ private HugeConfig config() { @SuppressWarnings("unused") private RpcServer initAndStartRpcServer() { + this.sharedRpcServer = false; Integer lowWaterMark = this.config().get( CoreOptions.RAFT_RPC_BUF_LOW_WATER_MARK); System.setProperty("bolt.channel_write_buf_low_water_mark", @@ -390,6 +442,8 @@ private RpcServer initAndStartRpcServer() { } private RpcServer wrapRpcServer(com.alipay.remoting.rpc.RpcServer rpcServer) { + // GraphManager owns this listener, which is shared by all local graph contexts. + this.sharedRpcServer = true; // TODO: pass ServerOptions instead of CoreOptions, to share by graphs Integer lowWaterMark = this.config().get( CoreOptions.RAFT_RPC_BUF_LOW_WATER_MARK); @@ -408,9 +462,17 @@ private RpcServer wrapRpcServer(com.alipay.remoting.rpc.RpcServer rpcServer) { } private void shutdownRpcServer() { - this.raftRpcServer.shutdown(); + if (this.sharedRpcServer) { + return; + } + if (!this.rpcClosed && this.raftRpcServer != null) { + this.raftRpcServer.shutdown(); + this.rpcClosed = true; + } PeerId endpoint = this.endpoint(); - NodeManager.getInstance().removeAddress(endpoint.getEndpoint()); + if (endpoint != null) { + NodeManager.getInstance().removeAddress(endpoint.getEndpoint()); + } } private void registerRpcRequestProcessors() { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftException.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftException.java index 85e6915bf8..4610d09a77 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftException.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftException.java @@ -17,7 +17,7 @@ package org.apache.hugegraph.backend.store.raft; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; public class RaftException extends HugeException { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftGroupManagerImpl.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftGroupManagerImpl.java index 0f231d734c..c0a144030c 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftGroupManagerImpl.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftGroupManagerImpl.java @@ -20,7 +20,7 @@ import java.util.List; import java.util.stream.Collectors; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.store.raft.rpc.RaftRequests.AddPeerRequest; import org.apache.hugegraph.backend.store.raft.rpc.RaftRequests.ListPeersRequest; import org.apache.hugegraph.backend.store.raft.rpc.RaftRequests.ListPeersResponse; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftNode.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftNode.java index a8e07b4114..956fd18594 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftNode.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/RaftNode.java @@ -24,7 +24,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.util.LZ4Util; import org.apache.hugegraph.util.Log; import org.slf4j.Logger; @@ -99,15 +99,15 @@ public void shutdown() { LOG.info("Shutdown raft node: {}", this); this.node.shutdown(); - if (this.raftGroupService != null) { - this.raftGroupService.shutdown(); - try { - this.raftGroupService.join(); - } catch (final InterruptedException e) { - throw new RaftException( - "Interrupted while shutdown raftGroupService"); - } + // RaftGroupService.shutdown() also removes the shared server address. + // Close only this node; GraphManager owns the shared listener and address. + try { + this.node.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RaftException("Interrupted while shutting down raft node", e); } + this.raftGroupService = null; } public RaftClosure snapshot() { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/StoreCommand.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/StoreCommand.java index f49f94348f..3f25aaf5e1 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/StoreCommand.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/StoreCommand.java @@ -17,7 +17,7 @@ package org.apache.hugegraph.backend.store.raft; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.backend.store.raft.rpc.RaftRequests.StoreAction; import org.apache.hugegraph.backend.store.raft.rpc.RaftRequests.StoreType; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/StoreSerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/StoreSerializer.java index 69937737da..c76afb9d64 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/StoreSerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/StoreSerializer.java @@ -21,13 +21,13 @@ import java.util.Iterator; import java.util.List; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.backend.store.BackendAction; import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendMutation; import org.apache.hugegraph.backend.store.raft.RaftBackendStore.IncrCounter; import org.apache.hugegraph.type.HugeType; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/StoreStateMachine.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/StoreStateMachine.java index df54a8276d..a6237ce8fb 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/StoreStateMachine.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/StoreStateMachine.java @@ -23,8 +23,8 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.backend.store.BackendMutation; import org.apache.hugegraph.backend.store.BackendStore; import org.apache.hugegraph.backend.store.raft.RaftBackendStore.IncrCounter; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/rpc/RpcForwarder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/rpc/RpcForwarder.java index 0944f5cc39..ba4973d69d 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/rpc/RpcForwarder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/raft/rpc/RpcForwarder.java @@ -19,7 +19,7 @@ import java.util.concurrent.ExecutionException; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.store.raft.RaftClosure; import org.apache.hugegraph.backend.store.raft.RaftContext; import org.apache.hugegraph.backend.store.raft.RaftStoreClosure; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/IntIntMap.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/IntIntMap.java index 1b2a4293ea..02d3261590 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/IntIntMap.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/IntIntMap.java @@ -22,7 +22,7 @@ import java.io.IOException; import java.util.Arrays; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; public final class IntIntMap implements RamMap { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/IntLongMap.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/IntLongMap.java index bc980db5a5..ccf7db5a30 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/IntLongMap.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/IntLongMap.java @@ -22,7 +22,7 @@ import java.io.IOException; import java.util.Arrays; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; public final class IntLongMap implements RamMap { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/RamTable.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/RamTable.java index 0e2c58bddc..c9d5edfce1 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/RamTable.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/RamTable.java @@ -31,28 +31,30 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import org.apache.commons.io.FileUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.query.ConditionQueryFlatten; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.iterator.FlatMapperIterator; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.util.Consumers; +import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Vertex; @@ -269,7 +271,18 @@ public boolean matched(Query query) { int conditionsSize = cq.conditionsSize(); Object owner = cq.condition(HugeKeys.OWNER_VERTEX); Directions direction = cq.condition(HugeKeys.DIRECTION); - Id label = cq.condition(HugeKeys.LABEL); + // query() expands one nonempty LABEL IN relation into EQ branches. + // Do not mistake an empty/conflicting set for the wildcard label zero. + Set labels = cq.conditionValues(HugeKeys.LABEL); + for (Object value : labels) { + if (!(value instanceof Id)) { + return false; + } + Id label = (Id) value; + if (!label.number() || label.asLong() <= 0L || label.asLong() > Integer.MAX_VALUE) { + return false; + } + } if (direction == null && conditionsSize > 1) { for (Condition cond : cq.conditions()) { @@ -289,7 +302,7 @@ public boolean matched(Query query) { if (direction != null) { matchedConds++; } - if (label != null) { + if (!labels.isEmpty()) { matchedConds++; } return matchedConds == cq.conditionsSize(); @@ -316,9 +329,12 @@ private Iterator query(ConditionQuery query) { if (dir == null) { dir = Directions.BOTH; } - Id label = query.condition(HugeKeys.LABEL); - if (label == null) { - label = IdGenerator.ZERO; + Id label = IdGenerator.ZERO; + if (query.containsConditionValues(HugeKeys.LABEL)) { + // Only an absent EQ/IN label condition means all labels. Each + // flattened branch must otherwise resolve to exactly one label. + label = query.conditionValue(HugeKeys.LABEL); + E.checkState(label != null, "Expect one label value for query: %s", query); } return this.query(owner.asLong(), dir, (int) label.asLong()); } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/AbstractTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/AbstractTransaction.java index 43393252d2..0a5d527832 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/AbstractTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/AbstractTransaction.java @@ -20,14 +20,14 @@ import java.util.Set; import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.Transaction; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.serializer.AbstractSerializer; import org.apache.hugegraph.backend.store.BackendEntry; @@ -37,7 +37,7 @@ import org.apache.hugegraph.backend.store.BackendStore; import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Action; import org.apache.hugegraph.type.define.CollectionType; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java index 6faace9671..9935644324 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java @@ -31,52 +31,54 @@ import java.util.function.Function; import java.util.stream.Collectors; -import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.analyzer.Analyzer; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.page.IdHolder; import org.apache.hugegraph.backend.page.IdHolder.BatchIdHolder; import org.apache.hugegraph.backend.page.IdHolder.FixedIdHolder; import org.apache.hugegraph.backend.page.IdHolder.PagingIdHolder; +import org.apache.hugegraph.backend.page.IdHolder; import org.apache.hugegraph.backend.page.IdHolderList; import org.apache.hugegraph.backend.page.PageIds; import org.apache.hugegraph.backend.page.PageInfo; import org.apache.hugegraph.backend.page.PageState; import org.apache.hugegraph.backend.page.SortByCountIdHolderList; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.Condition.RangeConditions; -import org.apache.hugegraph.backend.query.Condition.Relation; -import org.apache.hugegraph.backend.query.Condition.RelationType; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.ConditionQuery.OptimizedType; import org.apache.hugegraph.backend.query.ConditionQueryFlatten; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.backend.query.QueryBatch; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.serializer.AbstractSerializer; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendStore; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.exception.NotSupportException; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.iterator.Metadatable; import org.apache.hugegraph.job.EphemeralJob; import org.apache.hugegraph.job.system.DeleteExpiredJob; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaLabel; +import org.apache.hugegraph.query.Condition.RangeConditions; +import org.apache.hugegraph.query.Condition.Relation; +import org.apache.hugegraph.query.Condition.RelationType; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery.OptimizedType; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.MatchedIndex; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.structure.BaseProperty; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.structure.HugeIndex; -import org.apache.hugegraph.structure.HugeIndex.IdWithExpiredTime; import org.apache.hugegraph.structure.HugeProperty; import org.apache.hugegraph.structure.HugeVertex; +import org.apache.hugegraph.structure.Index.IdWithExpiredTime; +import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.structure.builder.IndexBuilder; import org.apache.hugegraph.task.EphemeralJobQueue; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Action; @@ -97,18 +99,17 @@ public class GraphIndexTransaction extends AbstractTransaction { - public static final String START_SYMBOL = "("; - public static final String END_SYMBOL = ")"; - public static final String WORD_DELIMITER = "|"; + public static final String START_SYMBOL = IndexBuilder.START_SYMBOL; + public static final String END_SYMBOL = IndexBuilder.END_SYMBOL; + public static final String WORD_DELIMITER = IndexBuilder.WORD_DELIMITER; - private final Analyzer textAnalyzer; + private final IndexBuilder indexBuilder; private final int indexIntersectThresh; public GraphIndexTransaction(HugeGraphParams graph, BackendStore store) { super(graph, store); - this.textAnalyzer = graph.analyzer(); - assert this.textAnalyzer != null; + this.indexBuilder = new IndexBuilder(this.graph(), graph.analyzer()); final HugeConfig conf = graph.configuration(); this.indexIntersectThresh = @@ -137,27 +138,8 @@ public void updateLabelIndex(HugeElement element, boolean removed) { return; } - // Update label index if backend store not supports label-query - HugeIndex index = new HugeIndex(this.graph(), - IndexLabel.label(element.type())); - index.fieldValues(element.schemaLabel().id()); - index.elementIds(element.id(), element.expiredTime()); - - if (removed) { - this.doEliminate(this.serializer.writeIndex(index)); - } else { - this.doAppend(this.serializer.writeIndex(index)); - } - - if (element instanceof HugeEdge && ((EdgeLabel) label).hasFather()) { - HugeIndex fatherIndex = new HugeIndex(this.graph(), IndexLabel.label(element.type())); - fatherIndex.fieldValues(((EdgeLabel) label).fatherId()); - fatherIndex.elementIds(element.id(), element.expiredTime()); - if (removed) { - this.doEliminate(this.serializer.writeIndex(fatherIndex)); - } else { - this.doAppend(this.serializer.writeIndex(fatherIndex)); - } + for (Index index : this.indexBuilder.buildLabelIndex(element.element())) { + this.writeIndex(index, removed); } } @@ -168,7 +150,7 @@ public void updateVertexIndex(HugeVertex vertex, boolean removed) { return; } // Update index(only property, no edge) of a vertex - for (Id id : vertex.schemaLabel().indexLabels()) { + for (Id id : this.indexLabelIds(vertex.schemaLabel())) { this.updateIndex(id, vertex, removed); } } @@ -176,7 +158,7 @@ public void updateVertexIndex(HugeVertex vertex, boolean removed) { @Watched(prefix = "index") public void updateEdgeIndex(HugeEdge edge, boolean removed) { // Update index of an edge - for (Id id : edge.schemaLabel().indexLabels()) { + for (Id id : this.indexLabelIds(edge.schemaLabel())) { this.updateIndex(id, edge, removed); } @@ -189,18 +171,9 @@ public void updateEdgeIndex(HugeEdge edge, boolean removed) { } private void updateVertexOlapIndex(HugeVertex vertex, boolean removed) { - Set propKeys = vertex.getPropertyKeys(); - E.checkArgument(propKeys.size() == 1, - "Expect only 1 property for olap vertex, but got %s", - propKeys.size()); - Id pkId = propKeys.iterator().next(); - List indexLabels = this.params().schemaTransaction() - .getIndexLabels(); - for (IndexLabel il : indexLabels) { - if (il.indexFields().contains(pkId)) { - this.updateIndex(il.id(), vertex, removed); - } - } + IndexBuilder.forEachOlapIndexLabelId(vertex.element(), + () -> this.params().schemaTransaction().getIndexLabels().iterator(), + id -> this.updateIndex(id, vertex, removed)); } /** @@ -216,110 +189,21 @@ protected void updateIndex(Id ilId, HugeElement element, boolean removed) { E.checkArgument(indexLabel != null, "Not exist index label with id '%s'", ilId); - // Collect property values of index fields - List allPropValues = new ArrayList<>(); - int fieldsNum = indexLabel.indexFields().size(); - int firstNullField = fieldsNum; - for (Id fieldId : indexLabel.indexFields()) { - HugeProperty property = element.getProperty(fieldId); - if (property == null) { - E.checkState(hasNullableProp(element, fieldId), - "Non-null property '%s' is null for '%s'", - this.graph().propertyKey(fieldId), element); - if (firstNullField == fieldsNum) { - firstNullField = allPropValues.size(); - } - allPropValues.add(ConditionQuery.INDEX_VALUE_NULL); - } else { - allPropValues.add(property.value()); - } - } - - if (firstNullField == 0 && !indexLabel.indexType().isUnique()) { - // The property value of first index field is null - return; - } - // Not build index for record with nullable field (except unique index) - List nnPropValues = allPropValues.subList(0, firstNullField); - - // Expired time - long expiredTime = element.expiredTime(); - - // Update index for each index type - switch (indexLabel.indexType()) { - case RANGE_INT: - case RANGE_FLOAT: - case RANGE_LONG: - case RANGE_DOUBLE: - E.checkState(nnPropValues.size() == 1, - "Expect only one property in range index"); - Object value = NumericUtil.convertToNumber(nnPropValues.get(0)); - this.updateIndex(indexLabel, value, element.id(), - expiredTime, removed); - break; - case SEARCH: - E.checkState(nnPropValues.size() == 1, - "Expect only one property in search index"); - value = nnPropValues.get(0); - Set words = - this.segmentWords(propertyValueToString(value)); - for (String word : words) { - this.updateIndex(indexLabel, word, element.id(), - expiredTime, removed); - } - break; - case SECONDARY: - // Secondary index maybe include multi prefix index - if (isCollectionIndex(nnPropValues)) { - /* - * Property value is a collection - * we should create index for each item - */ - for (Object propValue : (Collection) nnPropValues.get(0)) { - value = ConditionQuery.concatValues(propValue); - this.updateIndex(indexLabel, value, element.id(), - expiredTime, removed); - } - } else { - for (int i = 0, n = nnPropValues.size(); i < n; i++) { - List prefixValues = - nnPropValues.subList(0, i + 1); - value = ConditionQuery.concatValues(prefixValues); - this.updateIndex(indexLabel, value, element.id(), - expiredTime, removed); - } - } - break; - case SHARD: - value = ConditionQuery.concatValues(nnPropValues); - this.updateIndex(indexLabel, value, element.id(), - expiredTime, removed); - break; - case UNIQUE: - value = ConditionQuery.concatValues(allPropValues); - assert !"".equals(value); - Id id = element.id(); - // TODO: add lock for updating unique index - if (!removed && this.existUniqueValue(indexLabel, value, id)) { - throw new IllegalArgumentException(String.format( - "Unique constraint %s conflict is found for %s", - indexLabel, element)); - } - this.updateIndex(indexLabel, value, element.id(), - expiredTime, removed); - break; - default: - throw new AssertionError(String.format( - "Unknown index type '%s'", indexLabel.indexType())); + if (!(element instanceof HugeVertex && ((HugeVertex) element).olap())) { + element.element().schemaLabel(this.currentSchemaLabel(element.schemaLabel())); } + this.indexBuilder.forEachIndex(element.element(), indexLabel, index -> { + Object value = index.fieldValues(); + if (indexLabel.indexType().isUnique() && !removed && + this.existUniqueValue(indexLabel, value, element.id())) { + throw new IllegalArgumentException(String.format( + "Unique constraint %s conflict is found for %s", indexLabel, element)); + } + this.writeIndex(index, removed); + }); } - private void updateIndex(IndexLabel indexLabel, Object propValue, - Id elementId, long expiredTime, boolean removed) { - HugeIndex index = new HugeIndex(this.graph(), indexLabel); - index.fieldValues(propValue); - index.elementIds(elementId, expiredTime); - + private void writeIndex(Index index, boolean removed) { if (removed) { this.doEliminate(this.serializer.writeIndex(index)); } else { @@ -335,7 +219,7 @@ private boolean existUniqueValue(IndexLabel indexLabel, private boolean hasEliminateInTx(IndexLabel indexLabel, Object value, Id elementId) { - HugeIndex index = new HugeIndex(this.graph(), indexLabel); + Index index = new Index(this.graph(), indexLabel); index.fieldValues(value); index.elementIds(elementId); BackendEntry entry = this.serializer.writeIndex(index); @@ -351,7 +235,7 @@ private boolean existUniqueValueInStore(IndexLabel indexLabel, Object value) { try { exist = iterator.hasNext(); if (exist) { - HugeIndex index = this.serializer.readIndex(graph(), query, + Index index = this.serializer.readIndex(graph(), query, iterator.next()); this.removeExpiredIndexIfNeeded(index, query.showExpired()); // Memory backend might return empty BackendEntry @@ -400,10 +284,11 @@ public IdHolderList queryIndex(ConditionQuery query) { // Query by index query.optimized(OptimizedType.INDEX); + Id label = query.singleConditionValueOrNull(HugeKeys.LABEL); if (query.allSysprop() && conds.size() == 1 && - query.containsCondition(HugeKeys.LABEL)) { - // Query only by label - return this.queryByLabel(query); + label != null) { + // Query only by one EQ/IN-resolved label + return this.queryByLabel(query, label); } else { // Query by userprops (or userprops + label) return this.queryByUserprop(query); @@ -411,13 +296,10 @@ public IdHolderList queryIndex(ConditionQuery query) { } @Watched(prefix = "index") - private IdHolderList queryByLabel(ConditionQuery query) { + private IdHolderList queryByLabel(ConditionQuery query, Id label) { HugeType queryType = query.resultType(); IndexLabel il = IndexLabel.label(queryType); validateIndexLabel(il); - Id label = query.condition(HugeKeys.LABEL); - assert label != null; - HugeType indexType; SchemaLabel schemaLabel; if (queryType.isVertex()) { @@ -480,20 +362,50 @@ private IdHolderList queryByUserprop(ConditionQuery query) { } } } - Set indexes = this.collectMatchedIndexes(query); + boolean paging = query.paging(); + if (query.containsConditionValues(HugeKeys.LABEL) && + query.conditionValues(HugeKeys.LABEL).isEmpty()) { + return IdHolderList.empty(paging); + } + List schemaLabels = this.querySchemaLabels(query); + // Use the outer request's visibility, including flattened queries. + Query visibility = query.rootOriginQuery(); + List visibleLabels = schemaLabels.stream().filter(label -> + (visibility.showHidden() || !label.hidden()) && + (visibility.showDeleting() || !label.status().deleting())) + .collect(Collectors.toList()); + if (visibleLabels.size() != schemaLabels.size()) { + Set properties = this.requiredIndexProperties(query); + boolean hadCandidates = schemaLabels.stream().anyMatch(label -> + isIndexQueryCandidate(label, properties)); + boolean hasVisibleCandidates = visibleLabels.stream().anyMatch(label -> + isIndexQueryCandidate(label, properties)); + // Only visibility exclusions justify an empty result here. Visible + // candidates without usable indexes must still fail below. + if (hadCandidates && !hasVisibleCandidates) { + IdHolderList empty = new IdHolderList(paging); + if (paging) { + // Keep a page slot so an empty first page is valid. + empty.add(new PagingIdHolder(query, q -> PageIds.EMPTY)); + } + return empty; + } + } + Set indexes = this.collectMatchedIndexes(query, visibleLabels); if (indexes.isEmpty()) { - Id label = query.condition(HugeKeys.LABEL); + Id label = query.singleConditionValueOrNull(HugeKeys.LABEL); throw noIndexException(this.graph(), query, label); } // Value type of Condition not matched - boolean paging = query.paging(); if (!validQueryConditionValues(this.graph(), query)) { return IdHolderList.empty(paging); } - // Do index query - IdHolderList holders = new IdHolderList(paging); + this.checkIndexCoverage(query, visibleLabels, indexes); + + // Validate the whole plan before opening any backend iterators. A later + // label's unavailable index must not leave earlier iterators unowned. for (MatchedIndex index : indexes) { for (IndexLabel il : index.indexLabels()) { validateIndexLabel(il); @@ -501,28 +413,59 @@ private IdHolderList queryByUserprop(ConditionQuery query) { if (paging && index.indexLabels().size() > 1) { throw new NotSupportException("joint index query in paging"); } + } - if (index.containsSearchIndex()) { - // Do search-index query - holders.addAll(this.doSearchIndex(query, index)); - } else { - // Do secondary-index, range-index or shard-index query - IndexQueries queries = index.constructIndexQueries(query); - assert !paging || queries.size() <= 1; - IdHolder holder = this.doSingleOrJointIndex(queries); - holders.add(holder); + // Do index query + IdHolderList holders = new IdHolderList(paging); + try { + for (MatchedIndex index : indexes) { + ConditionQuery indexQuery = prepareIndexQuery(query, index); + if (index.containsSearchIndex()) { + // Do search-index query + holders.addAll(this.doSearchIndex(indexQuery, index)); + } else { + // Do secondary-index, range-index or shard-index query + IndexQueries queries = constructIndexQueries(indexQuery, index); + assert !paging || queries.size() <= 1; + IdHolder holder = this.doSingleOrJointIndex(queries); + holders.add(holder); + } + + /* + * NOTE: need to skip the offset if offset > 0, but can't handle + * it here because the query may a sub-query after flatten, + * so the offset will be handle in QueryList.IndexQuery + * + * TODO: finish early here if records exceeds required limit with + * FixedIdHolder. + */ } + return holders; + } catch (Throwable failure) { + throw closeIndexHolders(failure, holders); + } + } - /* - * NOTE: need to skip the offset if offset > 0, but can't handle - * it here because the query may a sub-query after flatten, - * so the offset will be handle in QueryList.IndexQuery - * - * TODO: finish early here if records exceeds required limit with - * FixedIdHolder. - */ + private static ConditionQuery prepareIndexQuery(ConditionQuery query, MatchedIndex index) { + Id label = query.singleConditionValueOrNull(HugeKeys.LABEL); + if (!query.resultType().isEdge() || label == null || + index.schemaLabel().id().equals(label)) { + return query; + } + // BaseEdge exposes the parent as LABEL and the actual child as SUB_LABEL. + // Filter on the child after fetching from the shared parent index. + ConditionQuery filtered = query.copy(); + filtered.unsetCondition(HugeKeys.LABEL); + filtered.eq(HugeKeys.SUB_LABEL, label); + filtered.optimized(OptimizedType.INDEX_FILTER); + if (!query.paging()) { + filtered.limit(Query.NO_LIMIT); } - return holders; + if (!index.containsSearchIndex()) { + // Search queries install their word-aware filter after rewriting. + filtered.registerResultsFilter(element -> filtered.test(element, null)); + } + return filtered; } @Watched(prefix = "index") @@ -532,18 +475,22 @@ private IdHolderList doSearchIndex(ConditionQuery query, // Sorted by matched count IdHolderList holders = new SortByCountIdHolderList(query.paging()); List flatten = ConditionQueryFlatten.flatten(query); - for (ConditionQuery q : flatten) { - if (!q.noLimit() && flatten.size() > 1) { - // Increase limit for union operation - increaseLimit(q); + try { + for (ConditionQuery q : flatten) { + if (!q.noLimit() && flatten.size() > 1) { + // Increase limit for union operation + increaseLimit(q); + } + IndexQueries queries = constructIndexQueries(q, index); + assert !query.paging() || queries.size() <= 1; + IdHolder holder = this.doSingleOrJointIndex(queries); + // NOTE: ids will be merged into one IdHolder if not in paging + holders.add(holder); } - IndexQueries queries = index.constructIndexQueries(q); - assert !query.paging() || queries.size() <= 1; - IdHolder holder = this.doSingleOrJointIndex(queries); - // NOTE: ids will be merged into one IdHolder if not in paging - holders.add(holder); + return holders; + } catch (Throwable failure) { + throw closeIndexHolders(failure, holders); } - return holders; } @Watched(prefix = "index") @@ -575,61 +522,79 @@ private IdHolder doJointIndex(IndexQueries queries) { Set intersectIds = null; boolean filtering = false; IdHolder resultHolder = null; - for (Map.Entry e : queries.entrySet()) { - IndexLabel indexLabel = e.getKey(); - ConditionQuery query = e.getValue(); - assert !query.paging(); - if (!query.noLimit() && queries.size() > 1) { - // Unset limit for intersection operation - query.limit(Query.NO_LIMIT); - } - /* - * Try to query by joint indexes: - * 1 If there is any index exceeded the threshold, transform into - * partial index query, then filter after back-table. - * 1.1 Return the holder of the first index that not exceeded the - * threshold if there exists one index, this holder will be used - * as the only query condition. - * 1.2 Return the holder of the first index if all indexes exceeded - * the threshold. - * 2 Else intersect holders for all indexes, and return intersection - * ids of all indexes. - */ - IdHolder holder = this.doIndexQuery(indexLabel, query); - if (resultHolder == null) { - resultHolder = holder; - this.storeSelectedIndexField(indexLabel, query); - } - assert this.indexIntersectThresh > 0; // default value is 1000 - Set ids = ((BatchIdHolder) holder).peekNext( - this.indexIntersectThresh).ids(); - if (ids.size() >= this.indexIntersectThresh) { - // Transform into filtering - filtering = true; - query.optimized(OptimizedType.INDEX_FILTER); - } else if (filtering) { - assert ids.size() < this.indexIntersectThresh; - resultHolder = holder; - this.storeSelectedIndexField(indexLabel, query); - break; - } else { - if (intersectIds == null) { - intersectIds = ids; - } else { - CollectionUtil.intersectWithModify(intersectIds, ids); + List holders = new ArrayList<>(queries.size()); + try { + for (Map.Entry e : queries.entrySet()) { + IndexLabel indexLabel = e.getKey(); + ConditionQuery query = e.getValue(); + assert !query.paging(); + if (!query.noLimit() && queries.size() > 1) { + // Unset limit for intersection operation + query.limit(Query.NO_LIMIT); + } + /* + * Try to query by joint indexes: + * 1 If there is any index exceeded the threshold, transform into + * partial index query, then filter after back-table. + * 1.1 Return the holder of the first index that not exceeded the + * threshold if there exists one index, this holder will be used + * as the only query condition. + * 1.2 Return the holder of the first index if all indexes exceeded + * the threshold. + * 2 Else intersect holders for all indexes, and return intersection + * ids of all indexes. + */ + IdHolder holder = this.doIndexQuery(indexLabel, query); + holders.add(holder); + if (resultHolder == null) { + resultHolder = holder; + this.storeSelectedIndexField(indexLabel, query); } - if (intersectIds.isEmpty()) { + assert this.indexIntersectThresh > 0; // default value is 1000 + Set ids = ((BatchIdHolder) holder).peekNext( + this.indexIntersectThresh).ids(); + if (ids.size() >= this.indexIntersectThresh) { + // Transform into filtering + filtering = true; + query.optimized(OptimizedType.INDEX_FILTER); + } else if (filtering) { + assert ids.size() < this.indexIntersectThresh; + resultHolder = holder; + this.storeSelectedIndexField(indexLabel, query); break; + } else { + if (intersectIds == null) { + intersectIds = ids; + } else { + CollectionUtil.intersectWithModify(intersectIds, ids); + } + if (intersectIds.isEmpty()) { + break; + } } } + + IdHolder result = filtering ? resultHolder : + new FixedIdHolder(queries.asJointQuery(), intersectIds); + // Transfer only the selected holder. Other lazy cursors may still + // be open when an index exceeded the intersection threshold. + QueryBatch.closeAll(holders.stream().filter(holder -> holder != result).toArray()); + return result; + } catch (Throwable failure) { + throw closeIndexHolders(failure, holders); } + } - if (filtering) { - return resultHolder; - } else { - assert intersectIds != null; - return new FixedIdHolder(queries.asJointQuery(), intersectIds); + private static RuntimeException closeIndexHolders(Throwable failure, + Collection holders) { + try { + QueryBatch.closeAll(holders.toArray()); + } catch (Throwable closing) { + if (closing != failure) { + failure.addSuppressed(closing); + } } + return QueryBatch.propagate(failure); } private void storeSelectedIndexField(IndexLabel indexLabel, @@ -694,7 +659,7 @@ private IdHolder doIndexQueryBatch(IndexLabel indexLabel, Set ids = InsertionOrderUtil.newSet(); while ((batch == Query.NO_LIMIT || ids.size() < batch) && entries.hasNext()) { - HugeIndex index = this.serializer.readIndex(graph(), query, + Index index = this.serializer.readIndex(graph(), query, entries.next()); this.removeExpiredIndexIfNeeded(index, query.showExpired()); ids.addAll(index.elementIds()); @@ -708,7 +673,7 @@ private IdHolder doIndexQueryBatch(IndexLabel indexLabel, }, this.keepBackendIndexOrder(indexLabel, query)); } - private void recordIndexValue(ConditionQuery query, HugeIndex index) { + private void recordIndexValue(ConditionQuery query, Index index) { if (!shouldRecordIndexValue(query, index)) { return; } @@ -736,7 +701,7 @@ private PageIds doIndexQueryOnce(IndexLabel indexLabel, Set ids = InsertionOrderUtil.newSet(); entries = super.query(query).iterator(); while (entries.hasNext()) { - HugeIndex index = this.serializer.readIndex(graph(), query, + Index index = this.serializer.readIndex(graph(), query, entries.next()); this.removeExpiredIndexIfNeeded(index, query.showExpired()); ids.addAll(index.elementIds()); @@ -765,14 +730,18 @@ private PageIds doIndexQueryOnce(IndexLabel indexLabel, } } - @Watched(prefix = "index") - private Set collectMatchedIndexes(ConditionQuery query) { + private List querySchemaLabels(ConditionQuery query) { ISchemaTransaction schema = this.params().schemaTransaction(); - Id label = query.condition(HugeKeys.LABEL); + Set labels = query.conditionValues(HugeKeys.LABEL); List schemaLabels; - if (label != null) { - // Query has LABEL condition + if (query.containsConditionValues(HugeKeys.LABEL) && labels.isEmpty()) { + // An empty intersection is not an unrestricted label query. + return Collections.emptyList(); + } + if (labels.size() == 1) { + Id label = (Id) labels.iterator().next(); + // Query has one resolved LABEL condition SchemaLabel schemaLabel; if (query.resultType().isVertex()) { schemaLabel = schema.getVertexLabel(label); @@ -784,8 +753,15 @@ private Set collectMatchedIndexes(ConditionQuery query) { query.resultType())); } schemaLabels = ImmutableList.of(schemaLabel); + if (schemaLabel instanceof EdgeLabel && ((EdgeLabel) schemaLabel).hasFather()) { + // Include the parent in visibility filtering and index matching; + // the original query label still filters out sibling edges. + EdgeLabel father = schema.getEdgeLabel(((EdgeLabel) schemaLabel).fatherId()); + schemaLabels = ImmutableList.of(schemaLabel, father); + } } else { - // Query doesn't have LABEL condition + // Query doesn't have LABEL condition or it doesn't resolve + // to a single label, so keep the conservative fallback. if (query.resultType().isVertex()) { schemaLabels = schema.getVertexLabels(); } else if (query.resultType().isEdge()) { @@ -797,17 +773,86 @@ private Set collectMatchedIndexes(ConditionQuery query) { } } + return schemaLabels; + } + + private Set collectMatchedIndexes(ConditionQuery query) { + // Cleanup callers must still inspect invisible labels' indexes. + return this.collectMatchedIndexes(query, this.querySchemaLabels(query)); + } + + @Watched(prefix = "index") + private Set collectMatchedIndexes(ConditionQuery query, + List schemaLabels) { // Collect MatchedIndex for each SchemaLabel Set matchedIndexes = InsertionOrderUtil.newSet(); for (SchemaLabel schemaLabel : schemaLabels) { MatchedIndex index = this.collectMatchedIndex(schemaLabel, query); if (index != null) { matchedIndexes.add(index); + // Prefer a compatible direct index for an explicit sub-label. + // Its parent, when present, follows it in querySchemaLabels(). + if (schemaLabel.id().equals(query.singleConditionValueOrNull(HugeKeys.LABEL))) { + break; + } } } return matchedIndexes; } + private static boolean isIndexQueryCandidate(SchemaLabel label, Set properties) { + // Base edge labels describe sub-labels and have no direct records. + return label.properties().containsAll(properties) && + !(label instanceof EdgeLabel && ((EdgeLabel) label).isFather()); + } + + private Set requiredIndexProperties(ConditionQuery query) { + Set requiredProperties = query.userpropKeys(); + if (this.graph().readMode().showOlap()) { + // OLAP properties are shared rather than declared on every label. + requiredProperties.removeIf(id -> this.graph().propertyKey(id).olap()); + } + return requiredProperties; + } + + private void checkIndexCoverage(ConditionQuery query, + List labels, + Set indexes) { + // Check at execution, before creating any lazy index iterator. Cleanup + // callers only need matching indexes and must not require full coverage. + Set requiredProperties = this.requiredIndexProperties(query); + if (requiredProperties.isEmpty()) { + // A shared OLAP index can match several labels, while MatchedIndex + // deduplicates those matches by index labels alone. + return; + } + Set covered = new HashSet<>(); + for (MatchedIndex index : indexes) { + covered.add(index.schemaLabel().id()); + } + for (SchemaLabel label : labels) { + if (covered.contains(label.id()) || + !label.properties().containsAll(requiredProperties)) { + continue; + } + if (label instanceof EdgeLabel) { + EdgeLabel edgeLabel = (EdgeLabel) label; + // Base labels have no direct edges; their property indexes + // include edges of their sub-labels (see updateEdgeIndex()). + if (edgeLabel.isFather() || + edgeLabel.hasFather() && covered.contains(edgeLabel.fatherId())) { + continue; + } + } + // Authorization filters elements after the backend query. Do not + // disclose an implicitly discovered schema label in this error. + throw new NoIndexException("Incomplete index coverage for properties %s; " + + "specify a label or create compatible indexes " + + "for every candidate label", + this.graph().mapPkId2Name(query.userpropKeys())); + } + } + /** * Collect matched IndexLabel(s) in a SchemaLabel for a query * @@ -882,8 +927,8 @@ private ConditionQuery constructSearchQuery(ConditionQuery query, MatchedIndex i if (key instanceof Id && indexFields.contains(key)) { // This is an index field of search index Id field = (Id) key; - HugeProperty property = element.getProperty(field); - String propValue = propertyValueToString(property.value()); + BaseProperty property = element.getProperty(field); + String propValue = IndexBuilder.propertyValueToString(property.value()); String fieldValue = (String) query.userpropValue(field); if (this.matchSearchIndexWords(propValue, fieldValue)) { continue; @@ -901,52 +946,24 @@ private ConditionQuery constructSearchQuery(ConditionQuery query, MatchedIndex i } private boolean matchSearchIndexWords(String propValue, String fieldValue) { - Set propValues = this.segmentWords(propValue); - Set words = this.segmentWords(fieldValue); - return CollectionUtil.hasIntersection(propValues, words); + return IndexBuilder.searchPredicate(this.params().analyzer(), fieldValue).test(propValue); } private Set segmentWords(String text) { - /* - Support 3 kinds of query: - - Text.contains("(word)"): query by user-specified word; - - Text.contains("(word1|word2|word3)"): query by user-specified words; - - Text.contains("words"): query by words splitted from analyzer; - Note: all kinds support words exact match - */ - if (text.startsWith(START_SYMBOL) && text.endsWith(END_SYMBOL)) { - String subText = text.substring(1, text.length() - 1); - if (subText.contains(WORD_DELIMITER)) { - String[] texts = StringUtils.split(subText, WORD_DELIMITER); - return ImmutableSet.copyOf(texts); - } else { - return ImmutableSet.of(subText); - } - } - Set segments = this.textAnalyzer.segment(text); - - /* - * Add original text to segments at the insertion stage, - * in order to can match fully words at the query stage. - */ - segments.add(text); - - // Ignore unicode \u0000 to \u0003 - segments.removeAll(ConditionQuery.IGNORE_SYM_SET); - return segments; + return this.indexBuilder.segmentWords(text); } private boolean needIndexForLabel() { return !this.store().features().supportsQueryByLabel(); } - private void removeExpiredIndexIfNeeded(HugeIndex index, + private void removeExpiredIndexIfNeeded(Index index, boolean showExpired) { if (this.store().features().supportsTtl() || showExpired) { return; } for (IdWithExpiredTime id : index.expiredElementIds()) { - HugeIndex removeIndex = index.clone(); + Index removeIndex = index.clone(); removeIndex.resetElementIds(); removeIndex.elementIds(id.id(), id.expiredTime()); DeleteExpiredJob.asyncDeleteExpiredObject(this.graph(), @@ -1183,7 +1200,7 @@ private static boolean cmn(List all, int m, int n, } private static boolean shouldRecordIndexValue(ConditionQuery query, - HugeIndex index) { + Index index) { // Currently, only range index has problems return query.originQuery() instanceof ConditionQuery && index.indexLabel().indexType().isRange(); @@ -1506,20 +1523,6 @@ private static Object toCollectionIfNeeded(PropertyKey pk, Object value) { return value; } - private static boolean isCollectionIndex(List propValues) { - return propValues.size() == 1 && - propValues.get(0) instanceof Collection; - } - - private static String propertyValueToString(Object value) { - /* - * Join collection items with white space if the value is Collection, - * or else keep the origin value. - */ - return value instanceof Collection ? - StringUtils.join(((Iterable) value), " ") : value.toString(); - } - private static NoIndexException noIndexException(HugeGraph graph, ConditionQuery query, Id label) { @@ -1557,13 +1560,23 @@ private static void validateIndexLabel(IndexLabel indexLabel) { indexLabel, indexLabel.status()); } - private static boolean hasNullableProp(HugeElement element, Id key) { - return element.schemaLabel().nullableKeys().contains(key); + Set indexLabelIds(SchemaLabel label) { + return this.currentSchemaLabel(label).indexLabels(); + } + + private SchemaLabel currentSchemaLabel(SchemaLabel label) { + // Elements can outlive schema cache eviction. Resolve the current index + // membership rather than using the schema object retained by an element. + ISchemaTransaction schema = this.params().schemaTransaction(); + SchemaLabel current = label.type() == HugeType.VERTEX_LABEL ? + schema.getVertexLabel(label.id()) : schema.getEdgeLabel(label.id()); + // Missing schema is represented by an undefined label so orphan records remain removable. + return current != null ? current : label; } - private static Set relatedIndexLabels(HugeElement element) { + private Set relatedIndexLabels(HugeElement element) { Set indexLabels = InsertionOrderUtil.newSet(); - Set indexLabelIds = element.schemaLabel().indexLabels(); + Set indexLabelIds = this.indexLabelIds(element.schemaLabel()); for (Id id : indexLabelIds) { IndexLabel indexLabel = element.graph().indexLabel(id); @@ -1586,72 +1599,20 @@ private static void increaseLimit(Query query) { } protected void removeIndex(IndexLabel indexLabel) { - HugeIndex index = new HugeIndex(this.graph(), indexLabel); + Index index = new Index(this.graph(), indexLabel); this.doRemove(this.serializer.writeIndex(index)); } - private static class MatchedIndex { - - private final SchemaLabel schemaLabel; - private final Set indexLabels; - - public MatchedIndex(SchemaLabel schemaLabel, - Set indexLabels) { - this.schemaLabel = schemaLabel; - this.indexLabels = indexLabels; - } - - @SuppressWarnings("unused") - public SchemaLabel schemaLabel() { - return this.schemaLabel; - } - - public Set indexLabels() { - return Collections.unmodifiableSet(this.indexLabels); - } - - public IndexQueries constructIndexQueries(ConditionQuery query) { - // Condition query => Index Queries - if (this.indexLabels().size() == 1) { - /* - * Query by single index or composite index - */ - IndexLabel il = this.indexLabels().iterator().next(); - ConditionQuery indexQuery = constructQuery(query, il); - assert indexQuery != null; - return IndexQueries.of(il, indexQuery); - } else { - /* - * Query by joint indexes - */ - IndexQueries queries = buildJointIndexesQueries(query, this); - assert !queries.isEmpty(); - return queries; - } - } - - public boolean containsSearchIndex() { - for (IndexLabel il : this.indexLabels) { - if (il.indexType().isSearch()) { - return true; - } - } - return false; - } - - @Override - public int hashCode() { - return this.indexLabels.hashCode(); - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof MatchedIndex)) { - return false; - } - Set indexLabels = ((MatchedIndex) other).indexLabels; - return Objects.equals(this.indexLabels, indexLabels); + private static IndexQueries constructIndexQueries(ConditionQuery query, MatchedIndex index) { + if (index.indexLabels().size() == 1) { + IndexLabel label = index.indexLabels().iterator().next(); + ConditionQuery indexQuery = constructQuery(query, label); + assert indexQuery != null; + return IndexQueries.of(label, indexQuery); } + IndexQueries queries = buildJointIndexesQueries(query, index); + assert !queries.isEmpty(); + return queries; } private static class IndexQueries @@ -1793,7 +1754,7 @@ protected long removeIndexLeft(ConditionQuery query, } // Check label is matched - Id label = query.condition(HugeKeys.LABEL); + Id label = query.singleConditionValueOrNull(HugeKeys.LABEL); // NOTE: original condition query may not have label condition, // which means possibly label == null. if (label != null && !element.schemaLabel().id().equals(label)) { @@ -1828,11 +1789,14 @@ private long processRangeIndexLeft(ConditionQuery query, Set indexValues = leftIndex.indexFieldValues(); IndexLabel indexLabel = this.findMatchedIndexLabel(query, leftIndex); - assert indexLabel != null; + if (indexLabel == null) { + // No matching schema index remains for this stale entry. + continue; + } AbstractSerializer serializer = this.tx.serializer; for (Object value : indexValues) { - HugeIndex index = new HugeIndex(this.graph(), indexLabel); + Index index = new Index(this.graph(), indexLabel); index.elementIds(element.id()); index.fieldValues(value); this.tx.doEliminate(serializer.writeIndex(index)); @@ -1870,7 +1834,7 @@ private long processSecondaryOrSearchIndexLeft(ConditionQuery query, // Delete unused index long count = 0; Set incorrectPkIds; - for (IndexLabel il : relatedIndexLabels(deletion)) { + for (IndexLabel il : this.tx.relatedIndexLabels(deletion)) { incorrectPkIds = incorrectPKs.keySet().stream() .map(PropertyKey::id) .collect(Collectors.toSet()); @@ -1947,7 +1911,7 @@ private boolean deletedByError(ConditionQuery query, if (elem == null) { return false; } - return query.test(elem); + return query.test(elem.element()); } private boolean deletedByError(HugeElement element, diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java index 3999b3d0c2..3b55a141bb 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java @@ -33,24 +33,24 @@ import java.util.stream.Collectors; import org.apache.commons.collections.CollectionUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.SplicingIdGenerator; import org.apache.hugegraph.backend.page.IdHolderList; import org.apache.hugegraph.backend.page.PageInfo; import org.apache.hugegraph.backend.page.QueryList; -import org.apache.hugegraph.backend.query.Aggregate; -import org.apache.hugegraph.backend.query.Aggregate.AggregateFunc; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.ConditionQuery.OptimizedType; +import org.apache.hugegraph.query.Aggregate; +import org.apache.hugegraph.query.Aggregate.AggregateFunc; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.ConditionQuery.OptimizedType; import org.apache.hugegraph.backend.query.ConditionQueryFlatten; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryBatch; import org.apache.hugegraph.backend.query.QueryResultContext; import org.apache.hugegraph.backend.query.QueryResults; @@ -64,21 +64,22 @@ import org.apache.hugegraph.iterator.BatchMapperIterator; import org.apache.hugegraph.iterator.ExtendableIterator; import org.apache.hugegraph.iterator.FilterIterator; +import org.apache.hugegraph.iterator.FlatMapperIterator; import org.apache.hugegraph.iterator.LimitIterator; import org.apache.hugegraph.iterator.MapperIterator; import org.apache.hugegraph.job.system.DeleteExpiredJob; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.SchemaLabel; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeEdgeProperty; import org.apache.hugegraph.structure.HugeElement; import org.apache.hugegraph.structure.HugeFeatures.HugeVertexFeatures; -import org.apache.hugegraph.structure.HugeIndex; +import org.apache.hugegraph.structure.Index; import org.apache.hugegraph.structure.HugeProperty; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.structure.HugeVertexProperty; @@ -369,7 +370,12 @@ protected void prepareDeletions(Map removedVertices, Map removedEdges) { // Remove related edges of each vertex for (HugeVertex v : removedVertices.values()) { - if (!v.schemaLabel().existsLinkLabel()) { + // System edge labels may be absent from schema enumeration after + // request cleanup, so always scan auth/system vertex relations. + // OLAP shares its ID with a base vertex and must keep its original + // path rather than scan that base vertex's ordinary edges. + VertexLabel label = v.schemaLabel(); + if ((!label.system() || label.olap()) && !label.existsLinkLabel()) { continue; } // Query all edges of the vertex and remove them @@ -546,26 +552,29 @@ public QueryResults query(Query query) { public Number queryNumber(Query query) { boolean isConditionQuery = query instanceof ConditionQuery; boolean hasUpdate = this.hasUpdate(); - Aggregate aggregate = query.aggregateNotNull(); - - // TODO: we can concat index-query results and tx uncommitted records. - if (hasUpdate) { - E.checkArgument(!isConditionQuery, - "It's not allowed to query by index when " + - "there are uncommitted records."); + Aggregate aggregate = query.aggregateNotNull(); + + if (hasUpdate && (query.resultType() == HugeType.VERTEX || + query.resultType() == HugeType.EDGE)) { + E.checkArgument(aggregate.func() == AggregateFunc.COUNT, + "The %s operator with uncommitted records " + + "is not supported", + aggregate.func().string()); + Query queryWithoutAggregate = query.copy(); + queryWithoutAggregate.aggregate(null); + Iterator results = queryWithoutAggregate.resultType().isVertex() ? + this.queryVertices(queryWithoutAggregate) : + this.queryEdges(queryWithoutAggregate); + return countAndClose(results); } QueryList queries = this.optimizeQueries(query, q -> { boolean isIndexQuery = q instanceof IdQuery; assert isIndexQuery || isConditionQuery || q == query; - // Need to fall back if there are uncommitted records - boolean fallback = hasUpdate; + boolean fallback = false; Number result; - if (fallback) { - // Here just ignore it, and do fall back later - result = null; - } else if (!isIndexQuery || !isConditionQuery) { + if (!isIndexQuery || !isConditionQuery) { // It's a sysprop-query, let parent tx do it assert !fallback; result = super.queryNumber(q); @@ -591,9 +600,9 @@ public Number queryNumber(Query query) { assert q.resultType().isVertex() || q.resultType().isEdge(); // Reset aggregate to fallback and scan q.aggregate(null); - result = IteratorUtils.count(q.resultType().isVertex() ? - this.queryVertices(q) : - this.queryEdges(q)); + result = countAndClose(q.resultType().isVertex() ? + this.queryVertices(q) : + this.queryEdges(q)); } return new QueryResults<>(IteratorUtils.of(result), q); @@ -605,6 +614,19 @@ public Number queryNumber(Query query) { return aggregate.reduce(results.iterator()); } + private static long countAndClose(Iterator results) { + try { + long count = 0L; + while (results.hasNext()) { + results.next(); + count++; + } + return count; + } finally { + CloseableIterator.closeIterator(results); + } + } + @Watched(prefix = "graph") public HugeVertex addVertex(Object... keyValues) { return this.addVertex(this.constructVertex(true, keyValues)); @@ -621,7 +643,7 @@ public HugeVertex addVertex(HugeVertex vertex) { this.locksTable.lockReads(LockUtil.VERTEX_LABEL_DELETE, vertex.schemaLabel().id()); this.locksTable.lockReads(LockUtil.INDEX_LABEL_DELETE, - vertex.schemaLabel().indexLabels()); + this.indexTx.indexLabelIds(vertex.schemaLabel())); // Ensure vertex label still exists from vertex-construct to lock this.graph().vertexLabel(vertex.schemaLabel().id()); /* @@ -694,7 +716,7 @@ public void removeVertex(HugeVertex vertex) { // Override vertices in local `addedVertices` this.addedVertices.remove(vertex.id()); // Force load vertex to ensure all properties are loaded (refer to #2181) - if (!vertex.schemaLabel().indexLabels().isEmpty()) { + if (!this.indexTx.indexLabelIds(vertex.schemaLabel()).isEmpty()) { vertex.forceLoad(); } // Collect the removed vertex @@ -743,7 +765,7 @@ public Iterator queryTaskInfos(Query query) { } public Iterator queryTaskInfos(Object... vertexIds) { - if (this.graph().backendStoreFeatures().supportsTaskAndServerVertex()) { + if (this.storeFeatures().supportsTaskAndServerVertex()) { return this.queryVerticesByIds(vertexIds, false, false, HugeType.TASK); } @@ -751,18 +773,6 @@ public Iterator queryTaskInfos(Object... vertexIds) { HugeType.VERTEX); } - public Iterator queryServerInfos(Query query) { - return this.queryVertices(query); - } - - public Iterator queryServerInfos(Object... vertexIds) { - if (this.graph().backendStoreFeatures().supportsTaskAndServerVertex()) { - return this.queryVerticesByIds(vertexIds, false, false, - HugeType.SERVER); - } - return this.queryVerticesByIds(vertexIds, false, false, HugeType.VERTEX); - } - protected Iterator queryVerticesByIds(Object[] vertexIds, boolean adjacentVertex, boolean checkMustExist) { return this.queryVerticesByIds(vertexIds, adjacentVertex, checkMustExist, HugeType.VERTEX); @@ -831,7 +841,7 @@ public Iterator queryVertices() { public Iterator queryVertices(Query query) { if (this.hasUpdate()) { E.checkArgument(query.noLimitAndOffset(), - "It's not allowed to query with offser/limit " + + "It's not allowed to query with offset/limit " + "when there are uncommitted records."); // TODO: also add check: no SCAN, no OLAP E.checkArgument(!query.paging(), @@ -900,7 +910,7 @@ public HugeEdge addEdge(HugeEdge edge) { this.locksTable.lockReads(LockUtil.EDGE_LABEL_DELETE, edge.schemaLabel().id()); this.locksTable.lockReads(LockUtil.INDEX_LABEL_DELETE, - edge.schemaLabel().indexLabels()); + this.indexTx.indexLabelIds(edge.schemaLabel())); // Ensure edge label still exists from edge-construct to lock this.graph().edgeLabel(edge.schemaLabel().id()); /* @@ -1020,7 +1030,7 @@ public Iterator queryEdges() { public Iterator queryEdges(Query query) { if (this.hasUpdate()) { E.checkArgument(query.noLimitAndOffset(), - "It's not allowed to query with offser/limit " + + "It's not allowed to query with offset/limit " + "when there are uncommitted records."); // TODO: also add check: no SCAN, no OLAP E.checkArgument(!query.paging(), @@ -1083,7 +1093,7 @@ private QueryResults queryEdgeBatchesFromBackend(Query query) { List flattened = ConditionQueryFlatten.flatten( (ConditionQuery) query, supportIn); Function> fetcher = cq -> { - Id label = cq.condition(HugeKeys.LABEL); + Id label = cq.singleConditionValueOrNull(HugeKeys.LABEL); if (this.storeFeatures().supportsFatherAndSubEdgeLabel() && label != null && graph().edgeLabel(label).isFather() && cq.condition(HugeKeys.SUB_LABEL) == null && @@ -1404,7 +1414,7 @@ private static boolean matchEdgeSortKeys(ConditionQuery query, boolean matchAll, HugeGraph graph) { assert query.resultType().isEdge(); - Id label = query.condition(HugeKeys.LABEL); + Id label = query.singleConditionValueOrNull(HugeKeys.LABEL); if (label == null) { return false; } @@ -1513,23 +1523,35 @@ private QueryList optimizeQueries(Query query, return queries; } - boolean supportIn = this.storeFeatures().supportsQueryWithInCondition(); - for (ConditionQuery cq : ConditionQueryFlatten.flatten( - (ConditionQuery) query, supportIn)) { - // Optimize by sysprop - Query q = this.optimizeQuery(cq); - /* - * NOTE: There are two possibilities for the returned q: - * 1.sysprop-query, which would not be empty. - * 2.index-query result(ids after optimization), which may be empty. - */ - if (q == null) { - queries.add(this.indexQuery(cq), this.batchSize); - } else if (!q.empty()) { - queries.add(q); + try { + boolean supportIn = this.storeFeatures().supportsQueryWithInCondition(); + for (ConditionQuery cq : ConditionQueryFlatten.flatten( + (ConditionQuery) query, supportIn)) { + // Optimize by sysprop + Query q = this.optimizeQuery(cq); + /* + * NOTE: There are two possibilities for the returned q: + * 1.sysprop-query, which would not be empty. + * 2.index-query result(ids after optimization), which may be empty. + */ + if (q == null) { + queries.add(this.indexQuery(cq), this.batchSize); + } else if (!q.empty()) { + queries.add(q); + } } + return queries; + } catch (Throwable failure) { + // Earlier flattened branches may already own backend cursors. + try { + queries.close(); + } catch (Throwable closing) { + if (closing != failure) { + failure.addSuppressed(closing); + } + } + throw QueryBatch.propagate(failure); } - return queries; } private Query optimizeQuery(ConditionQuery query) { @@ -1537,7 +1559,7 @@ private Query optimizeQuery(ConditionQuery query) { throw new HugeException("Not supported querying by id and conditions: %s", query); } - Id label = query.condition(HugeKeys.LABEL); + Id label = query.singleConditionValueOrNull(HugeKeys.LABEL); // Optimize vertex query if (label != null && query.resultType().isVertex()) { @@ -1839,7 +1861,7 @@ private void lockForUpdateProperty(SchemaLabel schemaLabel, Id pkey = prop.propertyKey().id(); Set indexIds = new HashSet<>(); - for (Id il : schemaLabel.indexLabels()) { + for (Id il : this.indexTx.indexLabelIds(schemaLabel)) { if (graph().indexLabel(il).indexFields().contains(pkey)) { indexIds.add(il); } @@ -1927,7 +1949,8 @@ private boolean rightResultFromIndexQuery(QueryResultContext context, HugeElemen if (cq == null || cq.resultType().isVertex() != elem.type().isVertex()) { return true; } - if (cq.condition(HugeKeys.LABEL) != null && cq.resultType().isEdge()) { + if (cq.singleConditionValueOrNull(HugeKeys.LABEL) != null && + cq.resultType().isEdge()) { if (cq.conditions().size() == 1) { // g.E().hasLabel(xxx) return true; @@ -1946,7 +1969,7 @@ private boolean rightResultFromIndexQuery(QueryResultContext context, HugeElemen } } - if (!context.conditionFilterRequired() || cq.test(elem, context.resultsFilter())) { + if (!context.conditionFilterRequired() || cq.test(elem.element(), context.resultsFilter())) { if (cq.existLeftIndex(elem.id())) { /* * Both have correct and left index, wo should return true @@ -2026,7 +2049,7 @@ private Iterator joinTxVertices(Query query, return null; } // Filter vertices matched conditions - return q.test(v) ? v : null; + return q.test(v.element()) ? v : null; }; vertices = this.joinTxRecords(query, vertices, matchTxFunc, this.addedVertices, this.removedVertices, @@ -2044,11 +2067,27 @@ private Iterator joinTxEdges(Query query, Iterator edges, return null; } // Filter edges matched conditions - return q.test(e) ? e : q.test(e = e.switchOwner()) ? e : null; + return q.test(e.element()) ? e : q.test((e = e.switchOwner()).element()) ? e : null; }; edges = this.joinTxRecords(query, edges, matchTxFunc, this.addedEdges, this.removedEdges, this.updatedEdges); + if (query instanceof ConditionQuery && + ((ConditionQuery) query).containsCondition(HugeKeys.OWNER_VERTEX) && + !(this.addedEdges.isEmpty() && this.updatedEdges.isEmpty())) { + edges = new FlatMapperIterator<>(edges, edge -> { + // Rehydrated self-loops may hold distinct vertex objects. + if (edge.sourceVertex().id().equals(edge.targetVertex().id()) && + (this.addedEdges.containsKey(edge.id()) || + this.updatedEdges.containsKey(edge.id()))) { + HugeEdge opposite = edge.switchOwner(); + if (query.test(opposite.element())) { + return ImmutableList.of(edge, opposite).iterator(); + } + } + return ImmutableList.of(edge).iterator(); + }); + } if (removingVertices.isEmpty()) { return edges; } @@ -2085,6 +2124,9 @@ private Iterator joinTxRecords( * Records in memory have higher priority than a query from backend store */ for (V elem : addedTxRecords.values()) { + if (removedTxRecords.containsKey(elem.id())) { + continue; + } if (query.reachLimit(txResults.size())) { break; } @@ -2093,6 +2135,9 @@ private Iterator joinTxRecords( } } for (V elem : updatedTxRecords.values()) { + if (removedTxRecords.containsKey(elem.id()) || addedTxRecords.containsKey(elem.id())) { + continue; + } if (query.reachLimit(txResults.size())) { break; } @@ -2187,7 +2232,7 @@ public void updateIndex(Id ilId, HugeElement element, boolean removed) { this.indexTx.updateIndex(ilId, element, removed); } - public void removeIndex(HugeIndex index) { + public void removeIndex(Index index) { // TODO: use event to replace direct call this.checkOwnerThread(); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/ISchemaTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/ISchemaTransaction.java index dce1276ced..f7eb879152 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/ISchemaTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/ISchemaTransaction.java @@ -20,13 +20,13 @@ import java.util.List; import java.util.Set; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.SchemaLabel; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.GraphMode; import org.apache.hugegraph.type.define.SchemaStatus; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/IdCounter.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/IdCounter.java index 87a10ce2c8..9d1ee4c845 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/IdCounter.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/IdCounter.java @@ -17,16 +17,16 @@ package org.apache.hugegraph.backend.tx; +import java.util.AbstractMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.pd.client.PDClient; import org.apache.hugegraph.pd.grpc.Pdpb; -import org.apache.hugegraph.store.term.HgPair; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.E; @@ -35,7 +35,7 @@ public class IdCounter { private static final int TIMES = 10000; private static final int DELTA = 10000; private static final String DELIMITER = "/"; - private static final Map> ids = + private static final Map> ids = new ConcurrentHashMap<>(); private final PDClient pdClient; private final String graphName; @@ -68,7 +68,7 @@ public long getCounter(HugeType type) { public synchronized void increaseCounter(HugeType type, long lowest) { String key = toKey(this.graphName, type); getCounterFromPd(type); - HgPair idPair = ids.get(key); + Map.Entry idPair = ids.get(key); AtomicLong currentId = idPair.getKey(); AtomicLong maxId = idPair.getValue(); if (currentId.longValue() >= lowest) { @@ -97,7 +97,7 @@ protected String toKey(String graphName, HugeType type) { public long getCounterFromPd(HugeType type) { AtomicLong currentId; AtomicLong maxId; - HgPair idPair; + Map.Entry idPair; String key = toKey(this.graphName, type); if ((idPair = ids.get(key)) == null) { synchronized (ids) { @@ -105,7 +105,7 @@ public long getCounterFromPd(HugeType type) { try { currentId = new AtomicLong(0); maxId = new AtomicLong(0); - idPair = new HgPair<>(currentId, maxId); + idPair = new AbstractMap.SimpleImmutableEntry<>(currentId, maxId); ids.put(key, idPair); } catch (Exception e) { throw new BackendException(String.format( diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/IndexableTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/IndexableTransaction.java index 7c481b05c2..8a3963989e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/IndexableTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/IndexableTransaction.java @@ -18,7 +18,7 @@ package org.apache.hugegraph.backend.tx; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.store.BackendMutation; import org.apache.hugegraph.backend.store.BackendStore; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/SchemaIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/SchemaIndexTransaction.java index 1fbade3190..c6122537bf 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/SchemaIndexTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/SchemaIndexTransaction.java @@ -20,16 +20,16 @@ import java.util.Iterator; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendStore; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.structure.HugeIndex; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.structure.Index; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.util.E; @@ -49,7 +49,7 @@ public void updateNameIndex(SchemaElement element, boolean removed) { IndexLabel indexLabel = IndexLabel.label(element.type()); // Update name index if backend store not supports name-query - HugeIndex index = new HugeIndex(this.graph(), indexLabel); + Index index = new Index(this.graph(), indexLabel); index.fieldValues(element.name()); index.elementIds(element.id()); @@ -96,7 +96,7 @@ private QueryResults queryByName(ConditionQuery query) { Iterator entries = super.query(indexQuery).iterator(); try { while (entries.hasNext()) { - HugeIndex index = this.serializer.readIndex(graph(), indexQuery, + Index index = this.serializer.readIndex(graph(), indexQuery, entries.next()); idQuery.query(index.elementIds()); Query.checkForceCapacity(idQuery.idsSize()); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/SchemaTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/SchemaTransaction.java index f09c45cf71..af6e3dedc5 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/SchemaTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/SchemaTransaction.java @@ -25,12 +25,12 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.LocalCounter; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendStore; @@ -47,13 +47,13 @@ import org.apache.hugegraph.job.schema.SchemaJob; import org.apache.hugegraph.job.schema.VertexLabelRemoveJob; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.SchemaLabel; -import org.apache.hugegraph.schema.Userdata; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.task.HugeTask; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.GraphMode; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/SchemaTransactionV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/SchemaTransactionV2.java index 5eed063dae..5fcc35b46f 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/SchemaTransactionV2.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/SchemaTransactionV2.java @@ -23,8 +23,8 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.job.JobBuilder; @@ -41,13 +41,13 @@ import org.apache.hugegraph.meta.PdMetaDriver; import org.apache.hugegraph.meta.managers.SchemaMetaManager; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.SchemaLabel; -import org.apache.hugegraph.schema.Userdata; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.task.HugeTask; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.GraphMode; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/config/CoreOptions.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/config/CoreOptions.java index bbd634eb73..2f4e0d2cad 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/config/CoreOptions.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/config/CoreOptions.java @@ -17,13 +17,13 @@ package org.apache.hugegraph.config; -import org.apache.hugegraph.backend.query.Query; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.type.define.CollectionType; import org.apache.hugegraph.util.Bytes; -import static org.apache.hugegraph.backend.query.Query.COMMIT_BATCH; +import static org.apache.hugegraph.query.Query.COMMIT_BATCH; import static org.apache.hugegraph.config.OptionChecker.*; public class CoreOptions extends OptionHolder { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/ConnectionException.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/ConnectionException.java index e6dc311815..70ac1bd1b7 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/ConnectionException.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/ConnectionException.java @@ -17,7 +17,7 @@ package org.apache.hugegraph.exception; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; public class ConnectionException extends HugeException { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/ErrorCodeProvider.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/ErrorCodeProvider.java deleted file mode 100644 index a5777f9fd1..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/ErrorCodeProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2017 HugeGraph Authors - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -package org.apache.hugegraph.exception; - -public interface ErrorCodeProvider { - - String format(Object... args); - - String with(String message); -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/ExistedException.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/ExistedException.java index b121588f0f..f6f64edf5d 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/ExistedException.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/ExistedException.java @@ -17,7 +17,7 @@ package org.apache.hugegraph.exception; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.type.HugeType; public class ExistedException extends HugeException { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NoIndexException.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NoIndexException.java index 3c1c788aee..2f72997972 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NoIndexException.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NoIndexException.java @@ -17,7 +17,7 @@ package org.apache.hugegraph.exception; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; public class NoIndexException extends HugeException { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/GraphSONSchemaSerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/GraphSONSchemaSerializer.java index db8d874e8e..208c96d4e8 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/GraphSONSchemaSerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/GraphSONSchemaSerializer.java @@ -22,11 +22,11 @@ import com.google.common.collect.ImmutableList; -import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.HugeGraphSupplier; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.EdgeLabelType; import org.apache.hugegraph.type.define.HugeKeys; @@ -34,7 +34,7 @@ public class GraphSONSchemaSerializer { public Map writeVertexLabel(VertexLabel vertexLabel) { - HugeGraph graph = vertexLabel.graph(); + HugeGraphSupplier graph = vertexLabel.graph(); assert graph != null; Map map = new LinkedHashMap<>(); @@ -61,7 +61,7 @@ public Map writeVertexLabel(VertexLabel vertexLabel) { } public Map writeEdgeLabel(EdgeLabel edgeLabel) { - HugeGraph graph = edgeLabel.graph(); + HugeGraphSupplier graph = edgeLabel.graph(); assert graph != null; Map map = new LinkedHashMap<>(); @@ -110,7 +110,7 @@ public Map writeEdgeLabel(EdgeLabel edgeLabel) { } public Map writePropertyKey(PropertyKey propertyKey) { - HugeGraph graph = propertyKey.graph(); + HugeGraphSupplier graph = propertyKey.graph(); assert graph != null; Map map = new LinkedHashMap<>(); @@ -128,7 +128,7 @@ public Map writePropertyKey(PropertyKey propertyKey) { } public Map writeIndexLabel(IndexLabel indexLabel) { - HugeGraph graph = indexLabel.graph(); + HugeGraphSupplier graph = indexLabel.graph(); assert graph != null; Map map = new LinkedHashMap<>(); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java index ddb7c1a981..158efdb2c4 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java @@ -33,18 +33,18 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.id.IdGenerator.LongId; -import org.apache.hugegraph.backend.id.IdGenerator.StringId; -import org.apache.hugegraph.backend.id.IdGenerator.UuidId; -import org.apache.hugegraph.backend.store.Shard; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.IdGenerator.LongId; +import org.apache.hugegraph.id.IdGenerator.StringId; +import org.apache.hugegraph.id.IdGenerator.UuidId; +import org.apache.hugegraph.backend.Shard; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.space.GraphSpace; import org.apache.hugegraph.space.Service; import org.apache.hugegraph.structure.HugeEdge; @@ -57,7 +57,6 @@ import org.apache.hugegraph.util.SafeDateUtil; import org.apache.tinkerpop.gremlin.process.traversal.Path; import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree; -import org.apache.tinkerpop.gremlin.structure.Element; import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONIo; import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONTokens; import org.apache.tinkerpop.gremlin.structure.io.graphson.TinkerPopJacksonModule; @@ -103,12 +102,14 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule { TYPE_DEFINITIONS = new ConcurrentHashMap<>(); TYPE_DEFINITIONS.put(Optional.class, "Optional"); + TYPE_DEFINITIONS.put(File.class, "File"); TYPE_DEFINITIONS.put(Date.class, "Date"); TYPE_DEFINITIONS.put(UUID.class, "UUID"); // HugeGraph id serializer TYPE_DEFINITIONS.put(StringId.class, "StringId"); TYPE_DEFINITIONS.put(LongId.class, "LongId"); + TYPE_DEFINITIONS.put(UuidId.class, "UuidId"); TYPE_DEFINITIONS.put(EdgeId.class, "EdgeId"); // HugeGraph schema serializer @@ -171,6 +172,7 @@ public static void registerCommonSerializers(SimpleModule module) { module.addSerializer(Shard.class, new ShardSerializer()); module.addSerializer(File.class, new FileSerializer()); + module.addDeserializer(File.class, new FileDeserializer()); boolean useTimestamp = false; module.addSerializer(Date.class, @@ -222,8 +224,14 @@ public static void registerGraphSerializers(SimpleModule module) { */ module.addSerializer(HugeVertex.class, new HugeVertexSerializer()); module.addSerializer(HugeEdge.class, new HugeEdgeSerializer()); + } + public static void registerTraversalSerializers(SimpleModule module) { module.addSerializer(Path.class, new PathSerializer()); + registerLegacyTreeSerializer(module); + } + + public static void registerLegacyTreeSerializer(SimpleModule module) { module.addSerializer(Tree.class, new TreeSerializer()); } @@ -641,8 +649,8 @@ public T deserialize(JsonParser jsonParser, String idValue = ctxt.readValue(jsonParser, String.class); return (T) IdGenerator.of(idValue); } else if (clazz.equals(UuidId.class)) { - UUID idValue = ctxt.readValue(jsonParser, UUID.class); - return (T) IdGenerator.of(idValue); + String idValue = ctxt.readValue(jsonParser, String.class); + return (T) IdGenerator.of(UUID.fromString(idValue)); } else { assert clazz.equals(EdgeId.class); String idValue = ctxt.readValue(jsonParser, String.class); @@ -883,9 +891,8 @@ public TreeSerializer() { public void serialize(Tree tree, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { jsonGenerator.writeStartArray(); - @SuppressWarnings("unchecked") - Set> set = tree.entrySet(); - for (Map.Entry entry : set) { + for (Object item : tree.entrySet()) { + Map.Entry entry = (Map.Entry) item; jsonGenerator.writeStartObject(); jsonGenerator.writeObjectField(GraphSONTokens.KEY, entry.getKey()); @@ -924,9 +931,65 @@ public FileSerializer() { public void serialize(File file, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { jsonGenerator.writeStartObject(); - jsonGenerator.writeStringField("file", file.getName()); + this.writeFields(file, jsonGenerator); jsonGenerator.writeEndObject(); } + + @Override + public void serializeWithType(File file, + JsonGenerator jsonGenerator, + SerializerProvider provider, + TypeSerializer typeSer) + throws IOException { + WritableTypeId typeId = typeSer.typeId( + file, JsonToken.VALUE_EMBEDDED_OBJECT); + typeSer.writeTypePrefix(jsonGenerator, typeId); + this.serialize(file, jsonGenerator, provider); + typeSer.writeTypeSuffix(jsonGenerator, typeId); + } + + private void writeFields(File file, JsonGenerator jsonGenerator) + throws IOException { + jsonGenerator.writeStringField("file", file.getName()); + } + } + + private static class FileDeserializer extends StdDeserializer { + + public FileDeserializer() { + super(File.class); + } + + @Override + public File deserialize(JsonParser jsonParser, + DeserializationContext ctxt) + throws IOException { + JsonToken token = jsonParser.currentToken(); + if (token == null) { + token = jsonParser.nextToken(); + } + if (token == JsonToken.VALUE_STRING) { + return new File(jsonParser.getValueAsString()); + } + if (token == JsonToken.START_OBJECT) { + String file = null; + while (jsonParser.nextToken() != JsonToken.END_OBJECT) { + String field = jsonParser.currentName(); + jsonParser.nextToken(); + if ("file".equals(field)) { + file = jsonParser.getValueAsString(); + } else { + jsonParser.skipChildren(); + } + } + if (file == null) { + return (File) ctxt.handleUnexpectedToken(File.class, + jsonParser); + } + return new File(file); + } + return (File) ctxt.handleUnexpectedToken(File.class, jsonParser); + } } private static class BlobSerializer extends StdSerializer { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONV1MessageIoRegistry.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONV1MessageIoRegistry.java new file mode 100644 index 0000000000..9c79766ff4 --- /dev/null +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONV1MessageIoRegistry.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.hugegraph.io; + +import org.apache.tinkerpop.gremlin.structure.io.AbstractIoRegistry; +import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONIo; +import org.apache.tinkerpop.shaded.jackson.databind.module.SimpleModule; + +/** + * Legacy Tree response shape for the untyped GraphSON V1 message serializer. + * Standard graph IO and typed message serializers use HugeGraphIoRegistry. + */ +public final class HugeGraphSONV1MessageIoRegistry extends AbstractIoRegistry { + + private static final HugeGraphSONV1MessageIoRegistry INSTANCE = + new HugeGraphSONV1MessageIoRegistry(); + + public static HugeGraphSONV1MessageIoRegistry instance() { + return INSTANCE; + } + + private HugeGraphSONV1MessageIoRegistry() { + SimpleModule module = new SimpleModule("hugegraph-v1-message"); + HugeGraphSONModule.registerLegacyTreeSerializer(module); + this.register(GraphSONIo.class, null, module); + } +} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.java new file mode 100644 index 0000000000..d86eac92a6 --- /dev/null +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.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.hugegraph.io; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import org.apache.hugegraph.backend.Shard; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.schema.SchemaManager; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.hugegraph.util.Blob; +import org.apache.tinkerpop.gremlin.structure.io.Buffer; +import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader; +import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter; +import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializer; +import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry; +import org.apache.tinkerpop.gremlin.structure.io.binary.types.SimpleTypeSerializer; +import org.apache.tinkerpop.gremlin.structure.io.binary.types.TransformSerializer; + +import groovy.lang.GString; + +public final class HugeGraphTypeSerializerRegistryBuilder + extends TypeSerializerRegistry.Builder { + + private static final TypeSerializer ID_TRANSFORM_SERIALIZER = + new WireTransformSerializer<>(Id::asObject); + private static final TypeSerializer BLOB_TRANSFORM_SERIALIZER = + new WireTransformSerializer<>(blob -> ByteBuffer.wrap(blob.bytes())); + private static final TypeSerializer SHARD_TRANSFORM_SERIALIZER = + new WireTransformSerializer<>(HugeGraphTypeSerializerRegistryBuilder::shardMap); + private static final TypeSerializer SCHEMA_TRANSFORM_SERIALIZER = + new WireTransformSerializer<>(HugeGraphTypeSerializerRegistryBuilder::schemaMap); + private static final TypeSerializer SCHEMA_MANAGER_TRANSFORM_SERIALIZER = + new WireTransformSerializer<>(HugeGraphTypeSerializerRegistryBuilder::schemaManagerMap); + private static final TypeSerializer> ENUM_TRANSFORM_SERIALIZER = + new WireTransformSerializer<>(Enum::name); + + private static final TypeSerializer> OPTIONAL_TRANSFORM_SERIALIZER = + new WireTransformSerializer<>(optional -> optional.orElse(null)); + private static final TypeSerializer FILE_TRANSFORM_SERIALIZER = + new WireTransformSerializer<>(file -> Map.of("file", file.getName())); + + private static final TypeSerializer GSTRING_TRANSFORM_SERIALIZER = + new WireTransformSerializer<>(GString::toString); + + public HugeGraphTypeSerializerRegistryBuilder() { + this.withFallbackResolver(type -> { + if (Id.class.isAssignableFrom(type)) { + return ID_TRANSFORM_SERIALIZER; + } + if (Blob.class.isAssignableFrom(type)) { + return BLOB_TRANSFORM_SERIALIZER; + } + if (Shard.class.isAssignableFrom(type)) { + return SHARD_TRANSFORM_SERIALIZER; + } + if (SchemaElement.class.isAssignableFrom(type)) { + return SCHEMA_TRANSFORM_SERIALIZER; + } + if (SchemaManager.class.isAssignableFrom(type)) { + return SCHEMA_MANAGER_TRANSFORM_SERIALIZER; + } + if (isHugeGraphEnum(type)) { + return ENUM_TRANSFORM_SERIALIZER; + } + if (Optional.class.isAssignableFrom(type)) { + return OPTIONAL_TRANSFORM_SERIALIZER; + } + if (File.class.isAssignableFrom(type)) { + return FILE_TRANSFORM_SERIALIZER; + } + if (GString.class.isAssignableFrom(type)) { + return GSTRING_TRANSFORM_SERIALIZER; + } + return null; + }); + } + + private static boolean isHugeGraphEnum(Class type) { + // Scope the fallback to HugeGraph; native TinkerPop enums keep their serializers. + return Enum.class.isAssignableFrom(type) && + type.getName().startsWith("org.apache.hugegraph."); + } + + private static Map shardMap(Shard shard) { + // Match the existing GraphSON contract, including open range boundaries. + Map result = new LinkedHashMap<>(); + result.put("start", shard.start()); + result.put("end", shard.end()); + result.put("length", shard.length()); + return result; + } + + private static Map schemaManagerMap(SchemaManager schema) { + Map result = new LinkedHashMap<>(); + result.put("propertykeys", schemaValue(schema.getPropertyKeys())); + result.put("vertexlabels", schemaValue(schema.getVertexLabels())); + result.put("edgelabels", schemaValue(schema.getEdgeLabels())); + result.put("indexlabels", schemaValue(schema.getIndexLabels())); + return result; + } + + private static Map schemaMap(SchemaElement schema) { + // Use the same field names and values as the existing GraphSON schema API. + GraphSONSchemaSerializer serializer = new GraphSONSchemaSerializer(); + Map fields; + if (schema instanceof PropertyKey) { + fields = serializer.writePropertyKey((PropertyKey) schema); + } else if (schema instanceof VertexLabel) { + fields = serializer.writeVertexLabel((VertexLabel) schema); + } else if (schema instanceof EdgeLabel) { + fields = serializer.writeEdgeLabel((EdgeLabel) schema); + } else if (schema instanceof IndexLabel) { + fields = serializer.writeIndexLabel((IndexLabel) schema); + } else { + throw new IllegalArgumentException("Unsupported schema type: " + schema.getClass()); + } + Map result = new LinkedHashMap<>(); + fields.forEach((key, value) -> result.put(key.string(), schemaValue(value))); + return result; + } + + private static Object schemaValue(Object value) { + if (value instanceof Enum && isHugeGraphEnum(value.getClass())) { + return ((Enum) value).name(); + } + if (value instanceof SchemaElement) { + return schemaMap((SchemaElement) value); + } + if (value instanceof SchemaManager) { + return schemaManagerMap((SchemaManager) value); + } + if (value instanceof Map) { + Map result = new LinkedHashMap<>(); + ((Map) value).forEach((key, item) -> result.put(schemaValue(key), schemaValue(item))); + return result; + } + if (value instanceof Collection) { + List result = new ArrayList<>(((Collection) value).size()); + for (Object item : (Collection) value) { + result.add(schemaValue(item)); + } + return result; + } + return value; + } + + private static final class WireTransformSerializer + extends SimpleTypeSerializer + implements TransformSerializer { + + private final Function transform; + + private WireTransformSerializer(Function transform) { + super(null); + this.transform = transform; + } + + @Override + protected T readValue(Buffer buffer, GraphBinaryReader context) + throws IOException { + throw new IOException("HugeGraph value is written as a standard wire value"); + } + + @Override + protected void writeValue(T value, Buffer buffer, + GraphBinaryWriter context) + throws IOException { + throw new IOException("HugeGraph value is written as a standard wire value"); + } + + @Override + public Object transform(T value) { + return this.transform.apply(value); + } + } +} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGryoModule.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGryoModule.java index 0feee228f6..cc1cbe6040 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGryoModule.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGryoModule.java @@ -21,14 +21,14 @@ import java.util.Map; import java.util.Optional; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.Id.IdType; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.Id.IdType; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.util.StringEncoding; import org.apache.tinkerpop.gremlin.structure.io.gryo.GryoIo; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/EphemeralJobBuilder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/EphemeralJobBuilder.java index a8a0966fa7..785a2563ea 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/EphemeralJobBuilder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/EphemeralJobBuilder.java @@ -18,8 +18,8 @@ package org.apache.hugegraph.job; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.task.HugeTask; import org.apache.hugegraph.task.TaskScheduler; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/GremlinJob.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/GremlinJob.java index 8bde61feb4..01c03d18ed 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/GremlinJob.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/GremlinJob.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.Map; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.exception.LimitExceedException; import org.apache.hugegraph.traversal.optimize.HugeScriptTraversal; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/JobBuilder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/JobBuilder.java index fc7067b419..0ee870030f 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/JobBuilder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/JobBuilder.java @@ -20,7 +20,7 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.task.HugeTask; import org.apache.hugegraph.task.TaskCallable; import org.apache.hugegraph.task.TaskScheduler; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/AbstractAlgorithm.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/AbstractAlgorithm.java index b30d3ac83a..ce72882d03 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/AbstractAlgorithm.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/AbstractAlgorithm.java @@ -29,10 +29,10 @@ import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.mutable.MutableLong; -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.iterator.FilterIterator; import org.apache.hugegraph.iterator.FlatMapperIterator; import org.apache.hugegraph.job.UserJob; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/BfsTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/BfsTraverser.java index 4a6cda246e..ba73e2bd29 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/BfsTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/BfsTraverser.java @@ -23,7 +23,7 @@ import java.util.Map; import java.util.Stack; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.type.define.Directions; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/Consumers.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/Consumers.java index dc04bdf605..81680dbfa6 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/Consumers.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/Consumers.java @@ -25,7 +25,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.task.TaskManager.ContextCallable; import org.apache.hugegraph.util.ExecutorUtil; import org.apache.hugegraph.util.Log; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/SubgraphStatAlgorithm.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/SubgraphStatAlgorithm.java index d6a1be8aa2..6646fae82d 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/SubgraphStatAlgorithm.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/SubgraphStatAlgorithm.java @@ -23,7 +23,7 @@ import org.apache.commons.configuration2.PropertiesConfiguration; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.StandardHugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.job.UserJob; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/AbstractCentAlgorithm.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/AbstractCentAlgorithm.java index 8ab6ca81f7..d34c245639 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/AbstractCentAlgorithm.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/AbstractCentAlgorithm.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.job.algorithm.cent; +import org.apache.hugegraph.util.TinkerPopUtil; + import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -24,7 +26,7 @@ import java.util.Map; import org.apache.commons.lang3.tuple.Pair; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.iterator.MapperIterator; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.job.algorithm.AbstractAlgorithm; @@ -114,7 +116,7 @@ protected GraphTraversal constructPathUnit( if (dir == null) { dir = Directions.BOTH; } - Direction direction = dir.direction(); + Direction direction = TinkerPopUtil.direction(dir); String[] labels = {}; if (label != null) { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/BetweennessCentralityAlgorithm.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/BetweennessCentralityAlgorithm.java index 512d14a158..a3e267a146 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/BetweennessCentralityAlgorithm.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/BetweennessCentralityAlgorithm.java @@ -23,8 +23,8 @@ import java.util.Map; import org.apache.commons.lang3.mutable.MutableFloat; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.SplicingIdGenerator; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.structure.HugeElement; import org.apache.hugegraph.type.define.Directions; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/BetweennessCentralityAlgorithmV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/BetweennessCentralityAlgorithmV2.java index ca99fddf8d..0bace3503b 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/BetweennessCentralityAlgorithmV2.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/BetweennessCentralityAlgorithmV2.java @@ -22,8 +22,8 @@ import java.util.Map; import org.apache.commons.lang3.mutable.MutableFloat; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.job.algorithm.BfsTraverser; import org.apache.hugegraph.structure.HugeVertex; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/ClosenessCentralityAlgorithmV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/ClosenessCentralityAlgorithmV2.java index 1519a2a456..ce92864ff5 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/ClosenessCentralityAlgorithmV2.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/ClosenessCentralityAlgorithmV2.java @@ -20,8 +20,8 @@ import java.util.Iterator; import java.util.Map; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.exception.NotSupportException; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.job.algorithm.BfsTraverser; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/DegreeCentralityAlgorithm.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/DegreeCentralityAlgorithm.java index 19e68c0531..72d2ae7da2 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/DegreeCentralityAlgorithm.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/DegreeCentralityAlgorithm.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.Map; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.traversal.algorithm.steps.EdgeStep; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/StressCentralityAlgorithmV2.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/StressCentralityAlgorithmV2.java index d1c3da6d8d..e0614a42aa 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/StressCentralityAlgorithmV2.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/cent/StressCentralityAlgorithmV2.java @@ -22,8 +22,8 @@ import java.util.Map; import org.apache.commons.lang3.mutable.MutableLong; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.job.algorithm.BfsTraverser; import org.apache.hugegraph.structure.HugeVertex; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/KCoreAlgorithm.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/KCoreAlgorithm.java index eb334e5cbe..f0c9502109 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/KCoreAlgorithm.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/KCoreAlgorithm.java @@ -27,7 +27,7 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.mutable.MutableInt; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.traversal.algorithm.FusiformSimilarityTraverser; import org.apache.hugegraph.type.define.Directions; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/LouvainTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/LouvainTraverser.java index d30e798d41..6ac8022685 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/LouvainTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/LouvainTraverser.java @@ -38,17 +38,17 @@ import org.apache.commons.lang.mutable.MutableFloat; import org.apache.commons.lang3.mutable.MutableInt; import org.apache.commons.lang3.tuple.Pair; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.exception.ExistedException; import org.apache.hugegraph.iterator.ListIterator; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.job.algorithm.AbstractAlgorithm; import org.apache.hugegraph.job.algorithm.AbstractAlgorithm.AlgoTraverser; import org.apache.hugegraph.job.algorithm.Consumers; -import org.apache.hugegraph.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.SchemaLabel; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.define.Directions; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/LpaAlgorithm.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/LpaAlgorithm.java index 4b2e30db3e..e729d378e8 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/LpaAlgorithm.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/LpaAlgorithm.java @@ -26,10 +26,10 @@ import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.lang3.mutable.MutableInt; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.util.E; import org.apache.tinkerpop.gremlin.process.traversal.Scope; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/TriangleCountAlgorithm.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/TriangleCountAlgorithm.java index 3e5c6c6370..411a730008 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/TriangleCountAlgorithm.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/TriangleCountAlgorithm.java @@ -24,8 +24,8 @@ import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.lang.mutable.MutableLong; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.type.define.Directions; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/WeakConnectedComponent.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/WeakConnectedComponent.java index 674ec50235..25dab78af4 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/WeakConnectedComponent.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/comm/WeakConnectedComponent.java @@ -23,10 +23,10 @@ import java.util.List; import java.util.Map; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.define.Directions; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/path/RingsDetectAlgorithm.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/path/RingsDetectAlgorithm.java index f4e069c252..4bf53ae2bc 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/path/RingsDetectAlgorithm.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/path/RingsDetectAlgorithm.java @@ -20,7 +20,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicLong; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.job.algorithm.AbstractAlgorithm; import org.apache.hugegraph.job.algorithm.Consumers.StopExecution; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/rank/PageRankAlgorithm.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/rank/PageRankAlgorithm.java index a7a189ea31..0d70bfaae3 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/rank/PageRankAlgorithm.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/algorithm/rank/PageRankAlgorithm.java @@ -23,11 +23,11 @@ import java.util.List; import java.util.Map; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.job.UserJob; import org.apache.hugegraph.job.algorithm.comm.AbstractCommAlgorithm; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/computer/AbstractComputer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/computer/AbstractComputer.java index 5158ec7916..cb7e764f22 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/computer/AbstractComputer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/computer/AbstractComputer.java @@ -32,7 +32,7 @@ import org.apache.commons.configuration2.tree.ImmutableNode; import org.apache.commons.configuration2.tree.NodeHandler; import org.apache.commons.configuration2.tree.NodeModel; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.job.ComputerJob; import org.apache.hugegraph.job.Job; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/EdgeLabelRemoveJob.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/EdgeLabelRemoveJob.java index 00462b9d78..e9963805fe 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/EdgeLabelRemoveJob.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/EdgeLabelRemoveJob.java @@ -20,10 +20,10 @@ import java.util.Set; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.backend.tx.ISchemaTransaction; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.type.define.SchemaStatus; import org.apache.hugegraph.util.LockUtil; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/IndexLabelRebuildJob.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/IndexLabelRebuildJob.java index 9bf0142d76..0a0e198524 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/IndexLabelRebuildJob.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/IndexLabelRebuildJob.java @@ -22,14 +22,14 @@ import java.util.function.Consumer; import java.util.stream.Collectors; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.backend.tx.ISchemaTransaction; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.SchemaLabel; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeElement; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.SchemaStatus; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/IndexLabelRemoveJob.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/IndexLabelRemoveJob.java index a8d8e1578d..2331202562 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/IndexLabelRemoveJob.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/IndexLabelRemoveJob.java @@ -18,10 +18,10 @@ package org.apache.hugegraph.job.schema; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.backend.tx.ISchemaTransaction; -import org.apache.hugegraph.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; import org.apache.hugegraph.type.define.SchemaStatus; import org.apache.hugegraph.util.LockUtil; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/OlapPropertyKeyClearJob.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/OlapPropertyKeyClearJob.java index e82ca0c0a6..377b68a8be 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/OlapPropertyKeyClearJob.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/OlapPropertyKeyClearJob.java @@ -18,10 +18,10 @@ package org.apache.hugegraph.job.schema; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.backend.tx.ISchemaTransaction; -import org.apache.hugegraph.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; import org.apache.hugegraph.type.define.SchemaStatus; import org.apache.hugegraph.util.LockUtil; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/OlapPropertyKeyCreateJob.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/OlapPropertyKeyCreateJob.java index 483b9106f8..367960e1aa 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/OlapPropertyKeyCreateJob.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/OlapPropertyKeyCreateJob.java @@ -18,7 +18,7 @@ package org.apache.hugegraph.job.schema; import org.apache.hugegraph.backend.tx.ISchemaTransaction; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.PropertyKey; public class OlapPropertyKeyCreateJob extends SchemaJob { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/OlapPropertyKeyRemoveJob.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/OlapPropertyKeyRemoveJob.java index e6f5f20e0c..6b9b5471f4 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/OlapPropertyKeyRemoveJob.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/OlapPropertyKeyRemoveJob.java @@ -17,9 +17,9 @@ package org.apache.hugegraph.job.schema; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.tx.ISchemaTransaction; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.PropertyKey; public class OlapPropertyKeyRemoveJob extends OlapPropertyKeyClearJob { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/SchemaJob.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/SchemaJob.java index 3165351396..01d456f368 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/SchemaJob.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/SchemaJob.java @@ -20,11 +20,11 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.tx.ISchemaTransaction; import org.apache.hugegraph.job.SysJob; -import org.apache.hugegraph.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaElement; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/VertexLabelRemoveJob.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/VertexLabelRemoveJob.java index 90bc469124..4c95840321 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/VertexLabelRemoveJob.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/schema/VertexLabelRemoveJob.java @@ -20,13 +20,13 @@ import java.util.List; import java.util.Set; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.backend.tx.ISchemaTransaction; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.define.SchemaStatus; import org.apache.hugegraph.util.LockUtil; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/system/DeleteExpiredIndexJob.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/system/DeleteExpiredIndexJob.java index 5556febb89..459b8de465 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/system/DeleteExpiredIndexJob.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/system/DeleteExpiredIndexJob.java @@ -21,10 +21,10 @@ import java.util.Set; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.query.IdQuery; +import org.apache.hugegraph.query.IdQuery; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.structure.HugeIndex; +import org.apache.hugegraph.structure.Index; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.E; @@ -32,9 +32,9 @@ public class DeleteExpiredIndexJob extends DeleteExpiredJob { private static final String JOB_TYPE = "delete_expired_index"; - private final Set indexes; + private final Set indexes; - public DeleteExpiredIndexJob(Set indexes) { + public DeleteExpiredIndexJob(Set indexes) { E.checkArgument(indexes != null && !indexes.isEmpty(), "The indexes can't be null or empty"); this.indexes = indexes; @@ -52,7 +52,7 @@ public V execute() throws Exception { HugeGraphParams graph = this.params(); GraphTransaction tx = graph.graphTransaction(); try { - for (HugeIndex index : this.indexes) { + for (Index index : this.indexes) { this.deleteExpiredIndex(graph, index); } tx.commit(); @@ -70,7 +70,7 @@ public V execute() throws Exception { * Delete expired element(if exist) of the index, * otherwise just delete expired index only */ - private void deleteExpiredIndex(HugeGraphParams graph, HugeIndex index) { + private void deleteExpiredIndex(HugeGraphParams graph, Index index) { GraphTransaction tx = graph.graphTransaction(); HugeType type = index.indexLabel().queryType().isVertex() ? HugeType.VERTEX : HugeType.EDGE; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/system/DeleteExpiredJob.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/system/DeleteExpiredJob.java index 9ffa9e955b..28f4012c88 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/system/DeleteExpiredJob.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/system/DeleteExpiredJob.java @@ -23,7 +23,7 @@ import org.apache.hugegraph.job.EphemeralJobBuilder; import org.apache.hugegraph.job.system.JobCounters.JobCounter; import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.structure.HugeIndex; +import org.apache.hugegraph.structure.Index; import org.apache.hugegraph.task.HugeTask; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; @@ -80,7 +80,7 @@ public static EphemeralJob newDeleteExpiredElementJob( if (object instanceof HugeElement) { return new DeleteExpiredElementJob<>(jobCounter.elements()); } else { - assert object instanceof HugeIndex; + assert object instanceof Index; return new DeleteExpiredIndexJob<>(jobCounter.indexes()); } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/system/JobCounters.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/system/JobCounters.java index 6feed5699c..848a821058 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/system/JobCounters.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/job/system/JobCounters.java @@ -24,7 +24,7 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.structure.HugeIndex; +import org.apache.hugegraph.structure.Index; public class JobCounters { @@ -43,7 +43,7 @@ public static class JobCounter { private final AtomicInteger jobs; private Set elements; - private Set indexes; + private Set indexes; private final int batchSize; public JobCounter(int batchSize) { @@ -69,7 +69,7 @@ public Set elements() { return this.elements; } - public Set indexes() { + public Set indexes() { return this.indexes; } @@ -77,7 +77,7 @@ public void clear(Object object) { if (object instanceof HugeElement) { this.elements = ConcurrentHashMap.newKeySet(); } else { - assert object instanceof HugeIndex; + assert object instanceof Index; this.indexes = ConcurrentHashMap.newKeySet(); } } @@ -85,7 +85,7 @@ public void clear(Object object) { public boolean addAndTriggerDelete(Object object) { return object instanceof HugeElement ? addElementAndTriggerDelete((HugeElement) object) : - addIndexAndTriggerDelete((HugeIndex) object); + addIndexAndTriggerDelete((Index) object); } /** @@ -108,7 +108,7 @@ public boolean addElementAndTriggerDelete(HugeElement element) { * @param index * @return true if we should create a new delete job, false otherwise */ - public boolean addIndexAndTriggerDelete(HugeIndex index) { + public boolean addIndexAndTriggerDelete(Index index) { if (this.indexes.size() >= this.batchSize) { return true; } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/k8s/K8sDriver.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/k8s/K8sDriver.java index 47a34658fd..61b3340fdc 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/k8s/K8sDriver.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/k8s/K8sDriver.java @@ -31,7 +31,7 @@ import io.fabric8.kubernetes.client.dsl.ParameterNamespaceListVisitFromServerGetDeleteRecreateWaitApplicable; import io.fabric8.kubernetes.client.dsl.Resource; import org.apache.commons.io.FileUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.space.GraphSpace; import org.apache.hugegraph.space.Service; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/k8s/K8sManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/k8s/K8sManager.java index a6869b0341..1e3057c860 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/k8s/K8sManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/k8s/K8sManager.java @@ -19,15 +19,21 @@ import com.google.common.base.Strings; import io.fabric8.kubernetes.api.model.Namespace; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.space.GraphSpace; import org.apache.hugegraph.space.Service; import org.apache.hugegraph.util.Log; import org.slf4j.Logger; +import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; -import java.io.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.StringWriter; import java.util.Collections; import java.util.List; import java.util.Map; @@ -236,7 +242,7 @@ public void loadOperator(String namespace, String imagePath) @SuppressWarnings("unchecked") public void loadResourceQuota(String namespace, int cpuLimit, int memoryLimit) throws HugeException { - Yaml yaml = new Yaml(); + Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); FileInputStream inputStream = null; namespace = namespace.replace("_", "-").toLowerCase(); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/k8s/K8sRegister.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/k8s/K8sRegister.java index c1dd29abc8..73701f463e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/k8s/K8sRegister.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/k8s/K8sRegister.java @@ -23,7 +23,7 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/kvstore/KvStore.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/kvstore/KvStore.java index 569af417f4..94ed2a32a7 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/kvstore/KvStore.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/kvstore/KvStore.java @@ -23,7 +23,7 @@ import java.util.List; import java.util.Map; -import org.apache.hugegraph.backend.store.Shard; +import org.apache.hugegraph.backend.Shard; import org.apache.tinkerpop.gremlin.structure.Vertex; public interface KvStore { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/ClusterRole.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/ClusterRole.java deleted file mode 100644 index f85f29cc88..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/ClusterRole.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.masterelection; - -import java.util.Objects; - -public class ClusterRole { - - private final String node; - private long clock; - private final int epoch; - private final String url; - - public ClusterRole(String node, String url, int epoch) { - this(node, url, epoch, 1); - } - - public ClusterRole(String node, String url, int epoch, long clock) { - this.node = node; - this.url = url; - this.epoch = epoch; - this.clock = clock; - } - - public void increaseClock() { - this.clock++; - } - - public boolean isMaster(String node) { - return Objects.equals(this.node, node); - } - - public int epoch() { - return this.epoch; - } - - public long clock() { - return this.clock; - } - - public void clock(long clock) { - this.clock = clock; - } - - public String node() { - return this.node; - } - - public String url() { - return this.url; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof ClusterRole)) { - return false; - } - ClusterRole clusterRole = (ClusterRole) obj; - return clock == clusterRole.clock && - epoch == clusterRole.epoch && - Objects.equals(node, clusterRole.node); - } - - @Override - public int hashCode() { - return Objects.hash(node, clock, epoch); - } - - @Override - public String toString() { - return "RoleStateData{" + - "node='" + node + '\'' + - ", clock=" + clock + - ", epoch=" + epoch + - '}'; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/ClusterRoleStore.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/ClusterRoleStore.java deleted file mode 100644 index f8bee5c0be..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/ClusterRoleStore.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.masterelection; - -import java.util.Optional; - -public interface ClusterRoleStore { - - boolean updateIfNodePresent(ClusterRole clusterRole); - - Optional query(); -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/Config.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/Config.java deleted file mode 100644 index 31de004f97..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/Config.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.masterelection; - -public interface Config { - - String node(); - - String url(); - - int exceedsFailCount(); - - long randomTimeoutMillisecond(); - - long heartBeatIntervalSecond(); - - int masterDeadTimes(); - - long baseTimeoutMillisecond(); -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/GlobalMasterInfo.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/GlobalMasterInfo.java index 4856744459..668c0413f6 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/GlobalMasterInfo.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/GlobalMasterInfo.java @@ -17,55 +17,21 @@ package org.apache.hugegraph.masterelection; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.type.define.NodeRole; import org.apache.hugegraph.util.E; -// TODO: We need to completely delete the startup of master-worker public final class GlobalMasterInfo { - private static final NodeInfo NO_MASTER = new NodeInfo(false, ""); - - private volatile boolean supportElection; - private volatile NodeInfo masterNodeInfo; - private volatile Id nodeId; private volatile NodeRole nodeRole; public GlobalMasterInfo() { - this(NO_MASTER); - } - - public GlobalMasterInfo(NodeInfo masterInfo) { - this.supportElection = false; - this.masterNodeInfo = masterInfo; - this.nodeId = null; this.nodeRole = null; } - public void supportElection(boolean featureSupport) { - this.supportElection = featureSupport; - } - - public boolean supportElection() { - return this.supportElection; - } - - public void resetMasterInfo() { - this.masterNodeInfo = NO_MASTER; - } - - public void masterInfo(boolean isMaster, String nodeUrl) { - // final can avoid instruction rearrangement, visibility can be ignored - this.masterNodeInfo = new NodeInfo(isMaster, nodeUrl); - } - - public NodeInfo masterInfo() { - return this.masterNodeInfo; - } - public Id nodeId() { return this.nodeId; } @@ -91,29 +57,9 @@ public void changeNodeRole(NodeRole role) { } public static GlobalMasterInfo master(String nodeId) { - NodeInfo masterInfo = new NodeInfo(true, nodeId); - GlobalMasterInfo nodeInfo = new GlobalMasterInfo(masterInfo); + GlobalMasterInfo nodeInfo = new GlobalMasterInfo(); nodeInfo.nodeId = IdGenerator.of(nodeId); nodeInfo.nodeRole = NodeRole.MASTER; return nodeInfo; } - - public static class NodeInfo { - - private final boolean isMaster; - private final String nodeUrl; - - public NodeInfo(boolean isMaster, String url) { - this.isMaster = isMaster; - this.nodeUrl = url; - } - - public boolean isMaster() { - return this.isMaster; - } - - public String nodeUrl() { - return this.nodeUrl; - } - } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/RoleElectionConfig.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/RoleElectionConfig.java deleted file mode 100644 index e389530949..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/RoleElectionConfig.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.masterelection; - -public class RoleElectionConfig implements Config { - - private final String node; - private final String url; - private final int exceedsFailCount; - private final long randomTimeoutMillisecond; - private final long heartBeatIntervalSecond; - private final int masterDeadTimes; - private final long baseTimeoutMillisecond; - - public RoleElectionConfig(String node, String url, int exceedsFailCount, - long randomTimeoutMillisecond, long heartBeatIntervalSecond, - int masterDeadTimes, long baseTimeoutMillisecond) { - this.node = node; - this.url = url; - this.exceedsFailCount = exceedsFailCount; - this.randomTimeoutMillisecond = randomTimeoutMillisecond; - this.heartBeatIntervalSecond = heartBeatIntervalSecond; - this.masterDeadTimes = masterDeadTimes; - this.baseTimeoutMillisecond = baseTimeoutMillisecond; - } - - @Override - public String node() { - return this.node; - } - - @Override - public String url() { - return this.url; - } - - @Override - public int exceedsFailCount() { - return this.exceedsFailCount; - } - - @Override - public long randomTimeoutMillisecond() { - return this.randomTimeoutMillisecond; - } - - @Override - public long heartBeatIntervalSecond() { - return this.heartBeatIntervalSecond; - } - - @Override - public int masterDeadTimes() { - return this.masterDeadTimes; - } - - @Override - public long baseTimeoutMillisecond() { - return this.baseTimeoutMillisecond; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/RoleElectionOptions.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/RoleElectionOptions.java deleted file mode 100644 index 748ec36bd0..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/RoleElectionOptions.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.masterelection; - -import static org.apache.hugegraph.config.OptionChecker.disallowEmpty; -import static org.apache.hugegraph.config.OptionChecker.rangeInt; - -import org.apache.hugegraph.config.ConfigOption; -import org.apache.hugegraph.config.OptionHolder; - -public class RoleElectionOptions extends OptionHolder { - - private RoleElectionOptions() { - super(); - } - - private static volatile RoleElectionOptions instance; - - public static synchronized RoleElectionOptions instance() { - if (instance == null) { - instance = new RoleElectionOptions(); - // Should initialize all static members first, then register. - instance.registerOptions(); - } - return instance; - } - - public static final ConfigOption EXCEEDS_FAIL_COUNT = - new ConfigOption<>( - "server.role.fail_count", - "When the node failed count of update or query heartbeat " + - "is reaches this threshold, the node will become abdication state to guard" + - "safe property.", - rangeInt(0, Integer.MAX_VALUE), - 5 - ); - - public static final ConfigOption NODE_EXTERNAL_URL = - new ConfigOption<>( - "server.role.node_external_url", - "The url of external accessibility.", - disallowEmpty(), - "http://127.0.0.1:8080" - ); - - public static final ConfigOption RANDOM_TIMEOUT_MILLISECOND = - new ConfigOption<>( - "server.role.random_timeout", - "The random timeout in ms that be used when candidate node request " + - "to become master state to reduce competitive voting.", - rangeInt(0, Integer.MAX_VALUE), - 1000 - ); - - public static final ConfigOption HEARTBEAT_INTERVAL_SECOND = - new ConfigOption<>( - "server.role.heartbeat_interval", - "The role state machine heartbeat interval second time.", - rangeInt(0, Integer.MAX_VALUE), - 2 - ); - - public static final ConfigOption MASTER_DEAD_TIMES = - new ConfigOption<>( - "server.role.master_dead_times", - "When the worker node detects that the number of times " + - "the master node fails to update heartbeat reaches this threshold, " + - "the worker node will become to a candidate node.", - rangeInt(0, Integer.MAX_VALUE), - 10 - ); - - public static final ConfigOption BASE_TIMEOUT_MILLISECOND = - new ConfigOption<>( - "server.role.base_timeout", - "The role state machine candidate state base timeout time.", - rangeInt(0, Integer.MAX_VALUE), - 500 - ); -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/RoleListener.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/RoleListener.java deleted file mode 100644 index eba2aa1e5f..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/RoleListener.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.masterelection; - -public interface RoleListener { - - void onAsRoleMaster(StateMachineContext context); - - void onAsRoleWorker(StateMachineContext context); - - void onAsRoleCandidate(StateMachineContext context); - - void unknown(StateMachineContext context); - - void onAsRoleAbdication(StateMachineContext context); - - void error(StateMachineContext context, Throwable e); -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/StandardClusterRoleStore.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/StandardClusterRoleStore.java deleted file mode 100644 index fa1cc6a617..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/StandardClusterRoleStore.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.masterelection; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.auth.SchemaDefine; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.backend.tx.GraphTransaction; -import org.apache.hugegraph.schema.VertexLabel; -import org.apache.hugegraph.structure.HugeVertex; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.define.DataType; -import org.apache.hugegraph.type.define.HugeKeys; -import org.apache.hugegraph.util.Log; -import org.apache.tinkerpop.gremlin.structure.Graph; -import org.apache.tinkerpop.gremlin.structure.T; -import org.apache.tinkerpop.gremlin.structure.Vertex; -import org.slf4j.Logger; - -public class StandardClusterRoleStore implements ClusterRoleStore { - - private static final Logger LOG = Log.logger(StandardClusterRoleStore.class); - private static final int RETRY_QUERY_TIMEOUT = 200; - - private final HugeGraphParams graph; - - private boolean firstTime; - - public StandardClusterRoleStore(HugeGraphParams graph) { - this.graph = graph; - Schema schema = new Schema(graph); - schema.initSchemaIfNeeded(); - this.firstTime = true; - } - - @Override - public boolean updateIfNodePresent(ClusterRole clusterRole) { - // if epoch increase, update and return true - // if epoch equal, ignore different node, return false - Optional oldClusterRoleOpt = this.queryVertex(); - if (oldClusterRoleOpt.isPresent()) { - ClusterRole oldClusterRole = this.from(oldClusterRoleOpt.get()); - if (clusterRole.epoch() < oldClusterRole.epoch()) { - return false; - } - - if (clusterRole.epoch() == oldClusterRole.epoch() && - !Objects.equals(clusterRole.node(), oldClusterRole.node())) { - return false; - } - LOG.trace("Server {} epoch {} begin remove data old epoch {}, ", - clusterRole.node(), clusterRole.epoch(), oldClusterRole.epoch()); - this.graph.systemTransaction().removeVertex((HugeVertex) oldClusterRoleOpt.get()); - this.graph.systemTransaction().commitOrRollback(); - LOG.trace("Server {} epoch {} success remove data old epoch {}, ", - clusterRole.node(), clusterRole.epoch(), oldClusterRole.epoch()); - } - try { - GraphTransaction tx = this.graph.systemTransaction(); - tx.doUpdateIfAbsent(this.constructEntry(clusterRole)); - tx.commitOrRollback(); - LOG.trace("Server {} epoch {} success update data", - clusterRole.node(), clusterRole.epoch()); - } catch (Throwable ignore) { - LOG.trace("Server {} epoch {} fail update data", - clusterRole.node(), clusterRole.epoch()); - return false; - } - - return true; - } - - private BackendEntry constructEntry(ClusterRole clusterRole) { - List list = new ArrayList<>(8); - list.add(T.label); - list.add(P.ROLE_DATA); - - list.add(P.NODE); - list.add(clusterRole.node()); - - list.add(P.URL); - list.add(clusterRole.url()); - - list.add(P.CLOCK); - list.add(clusterRole.clock()); - - list.add(P.EPOCH); - list.add(clusterRole.epoch()); - - list.add(P.TYPE); - list.add("default"); - - HugeVertex vertex = this.graph.systemTransaction() - .constructVertex(false, list.toArray()); - - return this.graph.serializer().writeVertex(vertex); - } - - @Override - public Optional query() { - Optional vertex = this.queryVertex(); - if (!vertex.isPresent() && !this.firstTime) { - // If query nothing, retry once - try { - Thread.sleep(RETRY_QUERY_TIMEOUT); - } catch (InterruptedException ignored) { - } - - vertex = this.queryVertex(); - } - this.firstTime = false; - return vertex.map(this::from); - } - - private ClusterRole from(Vertex vertex) { - String node = (String) vertex.property(P.NODE).value(); - String url = (String) vertex.property(P.URL).value(); - Long clock = (Long) vertex.property(P.CLOCK).value(); - Integer epoch = (Integer) vertex.property(P.EPOCH).value(); - - return new ClusterRole(node, url, epoch, clock); - } - - private Optional queryVertex() { - GraphTransaction tx = this.graph.systemTransaction(); - ConditionQuery query; - if (this.graph.backendStoreFeatures().supportsTaskAndServerVertex()) { - query = new ConditionQuery(HugeType.SERVER); - } else { - query = new ConditionQuery(HugeType.VERTEX); - } - VertexLabel vl = this.graph.graph().vertexLabel(P.ROLE_DATA); - query.eq(HugeKeys.LABEL, vl.id()); - query.query(Condition.eq(vl.primaryKeys().get(0), "default")); - query.showHidden(true); - Iterator vertexIterator = tx.queryVertices(query); - if (vertexIterator.hasNext()) { - return Optional.of(vertexIterator.next()); - } - - return Optional.empty(); - } - - public static final class P { - - public static final String ROLE_DATA = Graph.Hidden.hide("role_data"); - - public static final String LABEL = T.label.getAccessor(); - - public static final String NODE = Graph.Hidden.hide("role_node"); - - public static final String CLOCK = Graph.Hidden.hide("role_clock"); - - public static final String EPOCH = Graph.Hidden.hide("role_epoch"); - - public static final String URL = Graph.Hidden.hide("role_url"); - - public static final String TYPE = Graph.Hidden.hide("role_type"); - } - - public static final class Schema extends SchemaDefine { - - public Schema(HugeGraphParams graph) { - super(graph, P.ROLE_DATA); - } - - @Override - public void initSchemaIfNeeded() { - if (this.existVertexLabel(this.label)) { - return; - } - - String[] properties = this.initProperties(); - - VertexLabel label = this.schema() - .vertexLabel(this.label) - .enableLabelIndex(true) - .usePrimaryKeyId() - .primaryKeys(P.TYPE) - .properties(properties) - .build(); - this.graph.schemaTransaction().addVertexLabel(label); - } - - private String[] initProperties() { - List props = new ArrayList<>(); - - props.add(createPropertyKey(P.NODE, DataType.TEXT)); - props.add(createPropertyKey(P.URL, DataType.TEXT)); - props.add(createPropertyKey(P.CLOCK, DataType.LONG)); - props.add(createPropertyKey(P.EPOCH, DataType.INT)); - props.add(createPropertyKey(P.TYPE, DataType.TEXT)); - - return super.initProperties(props); - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/StandardRoleElectionStateMachine.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/StandardRoleElectionStateMachine.java deleted file mode 100644 index e813f9e6c3..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/StandardRoleElectionStateMachine.java +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.masterelection; - -import java.security.SecureRandom; -import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.locks.LockSupport; - -import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.Log; -import org.slf4j.Logger; - -public class StandardRoleElectionStateMachine implements RoleElectionStateMachine { - - private static final Logger LOG = Log.logger(StandardRoleElectionStateMachine.class); - - private final Config config; - private final ClusterRoleStore roleStore; - private final ExecutorService applyThread; - - private volatile boolean shutdown; - private volatile RoleState state; - - public StandardRoleElectionStateMachine(Config config, ClusterRoleStore roleStore) { - this.config = config; - this.roleStore = roleStore; - this.applyThread = Executors.newSingleThreadExecutor(); - this.state = new UnknownState(null); - this.shutdown = false; - } - - @Override - public void shutdown() { - if (this.shutdown) { - return; - } - this.shutdown = true; - this.applyThread.shutdown(); - } - - @Override - public void start(RoleListener stateMachineCallback) { - this.applyThread.execute(() -> this.apply(stateMachineCallback)); - } - - private void apply(RoleListener stateMachineCallback) { - int failCount = 0; - StateMachineContextImpl context = new StateMachineContextImpl(this); - while (!this.shutdown) { - E.checkArgumentNotNull(this.state, "State don't be null"); - try { - RoleState pre = this.state; - this.state = state.transform(context); - LOG.trace("server {} epoch {} role state change {} to {}", - context.node(), context.epoch(), pre.getClass().getSimpleName(), - this.state.getClass().getSimpleName()); - Callback runnable = this.state.callback(stateMachineCallback); - runnable.call(context); - failCount = 0; - } catch (Throwable e) { - stateMachineCallback.error(context, e); - failCount++; - if (failCount >= this.config.exceedsFailCount()) { - this.state = new AbdicationState(context.epoch()); - Callback runnable = this.state.callback(stateMachineCallback); - runnable.call(context); - } - } - } - } - - protected ClusterRoleStore roleStore() { - return this.roleStore; - } - - private interface RoleState { - - SecureRandom SECURE_RANDOM = new SecureRandom(); - - RoleState transform(StateMachineContext context); - - Callback callback(RoleListener callback); - - static void heartBeatPark(StateMachineContext context) { - long heartBeatIntervalSecond = context.config().heartBeatIntervalSecond(); - LockSupport.parkNanos(heartBeatIntervalSecond * 1_000_000_000); - } - - static void randomPark(StateMachineContext context) { - long randomTimeout = context.config().randomTimeoutMillisecond(); - long baseTime = context.config().baseTimeoutMillisecond(); - long timeout = (long) (baseTime + (randomTimeout / 10.0 * SECURE_RANDOM.nextInt(11))); - LockSupport.parkNanos(timeout * 1_000_000); - } - } - - @FunctionalInterface - private interface Callback { - - void call(StateMachineContext context); - } - - private static class UnknownState implements RoleState { - - final Integer epoch; - - public UnknownState(Integer epoch) { - this.epoch = epoch; - } - - @Override - public RoleState transform(StateMachineContext context) { - ClusterRoleStore adapter = context.roleStore(); - Optional clusterRoleOpt = adapter.query(); - if (!clusterRoleOpt.isPresent()) { - context.reset(); - Integer nextEpoch = this.epoch == null ? 1 : this.epoch + 1; - context.epoch(nextEpoch); - return new CandidateState(nextEpoch); - } - - ClusterRole clusterRole = clusterRoleOpt.get(); - if (this.epoch != null && clusterRole.epoch() < this.epoch) { - context.reset(); - Integer nextEpoch = this.epoch + 1; - context.epoch(nextEpoch); - return new CandidateState(nextEpoch); - } - - context.epoch(clusterRole.epoch()); - context.master(new MasterServerInfoImpl(clusterRole.node(), clusterRole.url())); - if (clusterRole.isMaster(context.node())) { - return new MasterState(clusterRole); - } else { - return new WorkerState(clusterRole); - } - } - - @Override - public Callback callback(RoleListener callback) { - return callback::unknown; - } - } - - private static class AbdicationState implements RoleState { - - private final Integer epoch; - - public AbdicationState(Integer epoch) { - this.epoch = epoch; - } - - @Override - public RoleState transform(StateMachineContext context) { - context.master(null); - RoleState.heartBeatPark(context); - return new UnknownState(this.epoch).transform(context); - } - - @Override - public Callback callback(RoleListener callback) { - return callback::onAsRoleAbdication; - } - } - - private static class MasterState implements RoleState { - - private final ClusterRole clusterRole; - - public MasterState(ClusterRole clusterRole) { - this.clusterRole = clusterRole; - } - - @Override - public RoleState transform(StateMachineContext context) { - this.clusterRole.increaseClock(); - RoleState.heartBeatPark(context); - if (context.roleStore().updateIfNodePresent(this.clusterRole)) { - return this; - } - context.reset(); - context.epoch(this.clusterRole.epoch()); - return new UnknownState(this.clusterRole.epoch()).transform(context); - } - - @Override - public Callback callback(RoleListener callback) { - return callback::onAsRoleMaster; - } - } - - private static class WorkerState implements RoleState { - - private ClusterRole clusterRole; - private int clock; - - public WorkerState(ClusterRole clusterRole) { - this.clusterRole = clusterRole; - this.clock = 0; - } - - @Override - public RoleState transform(StateMachineContext context) { - RoleState.heartBeatPark(context); - RoleState nextState = new UnknownState(this.clusterRole.epoch()).transform(context); - if (nextState instanceof WorkerState) { - this.merge((WorkerState) nextState); - if (this.clock > context.config().masterDeadTimes()) { - return new CandidateState(this.clusterRole.epoch() + 1); - } else { - return this; - } - } else { - return nextState; - } - } - - @Override - public Callback callback(RoleListener callback) { - return callback::onAsRoleWorker; - } - - public void merge(WorkerState state) { - if (state.clusterRole.epoch() > this.clusterRole.epoch()) { - this.clock = 0; - this.clusterRole = state.clusterRole; - } else if (state.clusterRole.epoch() < this.clusterRole.epoch()) { - throw new IllegalStateException("Epoch must increase"); - } else if (state.clusterRole.epoch() == this.clusterRole.epoch() && - state.clusterRole.clock() < this.clusterRole.clock()) { - throw new IllegalStateException("Clock must increase"); - } else if (state.clusterRole.epoch() == this.clusterRole.epoch() && - state.clusterRole.clock() > this.clusterRole.clock()) { - this.clock = 0; - this.clusterRole = state.clusterRole; - } else { - this.clock++; - } - } - } - - private static class CandidateState implements RoleState { - - private final Integer epoch; - - public CandidateState(Integer epoch) { - this.epoch = epoch; - } - - @Override - public RoleState transform(StateMachineContext context) { - RoleState.randomPark(context); - int epoch = this.epoch == null ? 1 : this.epoch; - ClusterRole clusterRole = new ClusterRole(context.config().node(), - context.config().url(), epoch); - // The master failover completed - context.epoch(clusterRole.epoch()); - if (context.roleStore().updateIfNodePresent(clusterRole)) { - context.master(new MasterServerInfoImpl(clusterRole.node(), clusterRole.url())); - return new MasterState(clusterRole); - } else { - return new UnknownState(epoch).transform(context); - } - } - - @Override - public Callback callback(RoleListener callback) { - return callback::onAsRoleCandidate; - } - } - - private static class StateMachineContextImpl implements StateMachineContext { - - private Integer epoch; - private final String node; - private final StandardRoleElectionStateMachine machine; - - private MasterServerInfo masterServerInfo; - - public StateMachineContextImpl(StandardRoleElectionStateMachine machine) { - this.node = machine.config.node(); - this.machine = machine; - } - - @Override - public void master(MasterServerInfo info) { - this.masterServerInfo = info; - } - - @Override - public Integer epoch() { - return this.epoch; - } - - @Override - public String node() { - return this.node; - } - - @Override - public void epoch(Integer epoch) { - this.epoch = epoch; - } - - @Override - public ClusterRoleStore roleStore() { - return this.machine.roleStore(); - } - - @Override - public Config config() { - return this.machine.config; - } - - @Override - public MasterServerInfo master() { - return this.masterServerInfo; - } - - @Override - public RoleElectionStateMachine stateMachine() { - return this.machine; - } - - @Override - public void reset() { - this.epoch = null; - } - } - - private static class MasterServerInfoImpl implements StateMachineContext.MasterServerInfo { - - private final String node; - private final String url; - - public MasterServerInfoImpl(String node, String url) { - this.node = node; - this.url = url; - } - - @Override - public String url() { - return this.url; - } - - @Override - public String node() { - return this.node; - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/StandardRoleListener.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/StandardRoleListener.java deleted file mode 100644 index ce91ca2d5f..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/StandardRoleListener.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.masterelection; - -import java.util.Objects; - -import org.apache.hugegraph.task.TaskManager; -import org.apache.hugegraph.util.Log; -import org.slf4j.Logger; - -public class StandardRoleListener implements RoleListener { - - private static final Logger LOG = Log.logger(StandardRoleListener.class); - - private final TaskManager taskManager; - - private final GlobalMasterInfo roleInfo; - - private volatile boolean selfIsMaster; - - public StandardRoleListener(TaskManager taskManager, - GlobalMasterInfo roleInfo) { - this.taskManager = taskManager; - this.roleInfo = roleInfo; - this.selfIsMaster = false; - } - - @Override - public void onAsRoleMaster(StateMachineContext context) { - if (!selfIsMaster) { - this.taskManager.onAsRoleMaster(); - LOG.info("Server {} change to master role", context.config().node()); - } - this.updateMasterInfo(context); - this.selfIsMaster = true; - } - - @Override - public void onAsRoleWorker(StateMachineContext context) { - if (this.selfIsMaster) { - this.taskManager.onAsRoleWorker(); - LOG.info("Server {} change to worker role", context.config().node()); - } - this.updateMasterInfo(context); - this.selfIsMaster = false; - } - - @Override - public void onAsRoleCandidate(StateMachineContext context) { - // pass - } - - @Override - public void onAsRoleAbdication(StateMachineContext context) { - if (this.selfIsMaster) { - this.taskManager.onAsRoleWorker(); - LOG.info("Server {} change to worker role", context.config().node()); - } - this.updateMasterInfo(context); - this.selfIsMaster = false; - } - - @Override - public void error(StateMachineContext context, Throwable e) { - LOG.error("Server {} exception occurred", context.config().node(), e); - } - - @Override - public void unknown(StateMachineContext context) { - if (this.selfIsMaster) { - this.taskManager.onAsRoleWorker(); - LOG.info("Server {} change to worker role", context.config().node()); - } - this.updateMasterInfo(context); - - this.selfIsMaster = false; - } - - public void updateMasterInfo(StateMachineContext context) { - StateMachineContext.MasterServerInfo master = context.master(); - if (master == null) { - this.roleInfo.resetMasterInfo(); - return; - } - - boolean isMaster = Objects.equals(context.node(), master.node()); - this.roleInfo.masterInfo(isMaster, master.url()); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/StateMachineContext.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/StateMachineContext.java deleted file mode 100644 index 9c0789a41e..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/StateMachineContext.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.masterelection; - -public interface StateMachineContext { - - Integer epoch(); - - String node(); - - RoleElectionStateMachine stateMachine(); - - void epoch(Integer epoch); - - Config config(); - - MasterServerInfo master(); - - void master(MasterServerInfo info); - - ClusterRoleStore roleStore(); - - void reset(); - - interface MasterServerInfo { - - String url(); - - String node(); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/factory/IdFactory.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/factory/IdFactory.java index 976b9cb373..e05d50326c 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/factory/IdFactory.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/factory/IdFactory.java @@ -17,13 +17,15 @@ package org.apache.hugegraph.memory.consumer.factory; +import org.apache.hugegraph.backend.BinaryId; + import java.util.UUID; import org.apache.hugegraph.backend.cache.CachedBackendStore; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; import org.apache.hugegraph.memory.MemoryManager; import org.apache.hugegraph.memory.consumer.OffHeapObject; @@ -50,7 +52,7 @@ private IdFactory() { /** * If using off-heap mode, param id must be OffHeapObject */ - public BinaryBackendEntry.BinaryId newBinaryId(byte[] bytes, Id id) { + public BinaryId newBinaryId(byte[] bytes, Id id) { switch (memoryMode) { case ENABLE_ON_HEAP_MANAGEMENT: case ENABLE_OFF_HEAP_MANAGEMENT: @@ -63,7 +65,7 @@ public BinaryBackendEntry.BinaryId newBinaryId(byte[] bytes, Id id) { (OffHeapObject) id); case DISABLE_MEMORY_MANAGEMENT: default: - return new BinaryBackendEntry.BinaryId(bytes, id); + return new BinaryId(bytes, id); } } @@ -238,8 +240,8 @@ public EdgeId newEdgeId(HugeVertex ownerVertex, ); case DISABLE_MEMORY_MANAGEMENT: default: - return new EdgeId(ownerVertex, direction, edgeLabelId, subLabelId, sortValues, - otherVertex); + return new EdgeId(ownerVertex.id(), direction, edgeLabelId, subLabelId, sortValues, + otherVertex.id()); } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/factory/PropertyFactory.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/factory/PropertyFactory.java index c628155eb1..896e25248a 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/factory/PropertyFactory.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/factory/PropertyFactory.java @@ -24,7 +24,7 @@ import org.apache.hugegraph.memory.consumer.impl.property.HugeEdgePropertyOffHeap; import org.apache.hugegraph.memory.consumer.impl.property.HugeVertexPropertyOffHeap; import org.apache.hugegraph.memory.pool.impl.TaskMemoryPool; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.structure.HugeEdgeProperty; import org.apache.hugegraph.structure.HugeElement; import org.apache.hugegraph.structure.HugeVertexProperty; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/BinaryIdOffHeap.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/BinaryIdOffHeap.java index e6444f3061..448c76aa08 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/BinaryIdOffHeap.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/BinaryIdOffHeap.java @@ -20,8 +20,8 @@ import java.util.Collections; import java.util.List; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.backend.BinaryId; import org.apache.hugegraph.memory.consumer.OffHeapObject; import org.apache.hugegraph.memory.pool.MemoryPool; import org.apache.hugegraph.util.Bytes; @@ -30,7 +30,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; -public class BinaryIdOffHeap extends BinaryBackendEntry.BinaryId implements OffHeapObject { +public class BinaryIdOffHeap extends BinaryId implements OffHeapObject { private final OffHeapObject originId; private ByteBuf bytesOffHeap; @@ -51,8 +51,8 @@ public void serializeSelfToByteBuf(MemoryPool memoryPool) { } @Override - public BinaryBackendEntry.BinaryId zeroCopyReadFromByteBuf() { - return new BinaryBackendEntry.BinaryId(ByteBufUtil.getBytes(bytesOffHeap), + public BinaryId zeroCopyReadFromByteBuf() { + return new BinaryId(ByteBufUtil.getBytes(bytesOffHeap), (Id) originId.zeroCopyReadFromByteBuf()); } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/EdgeIdOffHeap.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/EdgeIdOffHeap.java index b77de337a8..502e8286e0 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/EdgeIdOffHeap.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/EdgeIdOffHeap.java @@ -22,10 +22,8 @@ import java.util.List; import java.util.Objects; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdUtil; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.memory.consumer.OffHeapObject; import org.apache.hugegraph.memory.pool.MemoryPool; import org.apache.hugegraph.structure.HugeVertex; @@ -57,7 +55,7 @@ public EdgeIdOffHeap(HugeVertex ownerVertex, OffHeapObject edgeLabelIdOffHeap, OffHeapObject subLabelIdOffHeap, OffHeapObject otherVertexIdOffHeap) { - super(ownerVertex, direction, edgeLabelId, subLabelId, sortValues, otherVertex); + super(ownerVertex.id(), direction, edgeLabelId, subLabelId, sortValues, otherVertex.id()); this.memoryPool = memoryPool; this.ownerVertexIdOffHeap = ownerVertexIdOffHeap; this.edgeLabelIdOffHeap = edgeLabelIdOffHeap; @@ -220,23 +218,7 @@ public String asString() { this.cacheOffHeap.resetReaderIndex(); } } - String tmpCache; - if (this.directed) { - tmpCache = SplicingIdGenerator.concat( - IdUtil.writeString((Id) this.ownerVertexIdOffHeap.zeroCopyReadFromByteBuf()), - this.direction.type().string(), - IdUtil.writeLong((Id) this.edgeLabelIdOffHeap.zeroCopyReadFromByteBuf()), - IdUtil.writeLong((Id) this.subLabelIdOffHeap.zeroCopyReadFromByteBuf()), - this.sortValues(), - IdUtil.writeString((Id) this.otherVertexIdOffHeap.zeroCopyReadFromByteBuf())); - } else { - tmpCache = SplicingIdGenerator.concat( - IdUtil.writeString((Id) this.ownerVertexIdOffHeap.zeroCopyReadFromByteBuf()), - IdUtil.writeLong((Id) this.edgeLabelIdOffHeap.zeroCopyReadFromByteBuf()), - IdUtil.writeLong((Id) this.subLabelIdOffHeap.zeroCopyReadFromByteBuf()), - this.sortValues(), - IdUtil.writeString((Id) this.otherVertexIdOffHeap.zeroCopyReadFromByteBuf())); - } + String tmpCache = this.formatString(); byte[] tmpCacheBytes = tmpCache.getBytes(StandardCharsets.UTF_8); this.cacheOffHeap = (ByteBuf) memoryPool.requireMemory(tmpCacheBytes.length, memoryPool); this.cacheOffHeap.markReaderIndex(); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/LongIdOffHeap.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/LongIdOffHeap.java index 8d86f84a88..b7e34f28a1 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/LongIdOffHeap.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/LongIdOffHeap.java @@ -17,14 +17,14 @@ package org.apache.hugegraph.memory.consumer.impl.id; -import static org.apache.hugegraph.backend.id.IdGenerator.compareType; +import static org.apache.hugegraph.id.IdGenerator.compareType; import java.util.Collections; import java.util.List; import java.util.Objects; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.memory.consumer.OffHeapObject; import org.apache.hugegraph.memory.pool.MemoryPool; import org.apache.hugegraph.util.NumericUtil; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/ObjectIdOffHeap.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/ObjectIdOffHeap.java index d47d955a4d..16ab09a13b 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/ObjectIdOffHeap.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/ObjectIdOffHeap.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.Objects; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.memory.consumer.OffHeapObject; import org.apache.hugegraph.memory.pool.MemoryPool; import org.apache.hugegraph.memory.util.FurySerializationUtil; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/QueryIdOffHeap.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/QueryIdOffHeap.java index 8a8dd4d1ee..4b7c539b79 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/QueryIdOffHeap.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/QueryIdOffHeap.java @@ -22,8 +22,8 @@ import java.util.List; import org.apache.hugegraph.backend.cache.CachedBackendStore; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.memory.consumer.OffHeapObject; import org.apache.hugegraph.memory.pool.MemoryPool; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/StringIdOffHeap.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/StringIdOffHeap.java index d026583821..cb8295fe31 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/StringIdOffHeap.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/StringIdOffHeap.java @@ -17,14 +17,14 @@ package org.apache.hugegraph.memory.consumer.impl.id; -import static org.apache.hugegraph.backend.id.IdGenerator.compareType; +import static org.apache.hugegraph.id.IdGenerator.compareType; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.memory.consumer.OffHeapObject; import org.apache.hugegraph.memory.pool.MemoryPool; import org.jetbrains.annotations.TestOnly; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/UuidIdOffHeap.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/UuidIdOffHeap.java index ed4b0399b5..e2af7ea1c7 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/UuidIdOffHeap.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/id/UuidIdOffHeap.java @@ -17,16 +17,16 @@ package org.apache.hugegraph.memory.consumer.impl.id; -import static org.apache.hugegraph.backend.id.IdGenerator.compareType; +import static org.apache.hugegraph.id.IdGenerator.compareType; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.UUID; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.memory.consumer.OffHeapObject; import org.apache.hugegraph.memory.pool.MemoryPool; import org.apache.hugegraph.memory.util.FurySerializationUtil; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/property/HugeEdgePropertyOffHeap.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/property/HugeEdgePropertyOffHeap.java index d2f21a414c..bec26eb672 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/property/HugeEdgePropertyOffHeap.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/property/HugeEdgePropertyOffHeap.java @@ -24,10 +24,11 @@ import org.apache.hugegraph.memory.consumer.OffHeapObject; import org.apache.hugegraph.memory.pool.MemoryPool; import org.apache.hugegraph.memory.util.FurySerializationUtil; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeEdgeProperty; import org.apache.hugegraph.structure.HugeElement; +import org.apache.hugegraph.structure.BaseProperty; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -53,7 +54,7 @@ public Object zeroCopyReadFromByteBuf() { @Override public void serializeSelfToByteBuf(MemoryPool memoryPool) { - byte[] bytes = FurySerializationUtil.FURY.serialize(this.value); + byte[] bytes = FurySerializationUtil.FURY.serialize(this.property.value()); this.valueOffHeap = (ByteBuf) memoryPool.requireMemory(bytes.length, memoryPool); this.valueOffHeap.markReaderIndex(); this.valueOffHeap.writeBytes(bytes); @@ -61,7 +62,12 @@ public void serializeSelfToByteBuf(MemoryPool memoryPool) { @Override public void releaseOriginalVarsOnHeap() { - this.value = null; + this.property = new BaseProperty(this.pkey, null) { + @Override + public V value() { + return HugeEdgePropertyOffHeap.this.value(); + } + }; } @Override diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/property/HugeVertexPropertyOffHeap.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/property/HugeVertexPropertyOffHeap.java index 1beb759cf0..0c75e44608 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/property/HugeVertexPropertyOffHeap.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/consumer/impl/property/HugeVertexPropertyOffHeap.java @@ -24,8 +24,9 @@ import org.apache.hugegraph.memory.consumer.OffHeapObject; import org.apache.hugegraph.memory.pool.MemoryPool; import org.apache.hugegraph.memory.util.FurySerializationUtil; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.structure.HugeElement; +import org.apache.hugegraph.structure.BaseProperty; import org.apache.hugegraph.structure.HugeVertexProperty; import io.netty.buffer.ByteBuf; @@ -52,7 +53,7 @@ public Object zeroCopyReadFromByteBuf() { @Override public void serializeSelfToByteBuf(MemoryPool memoryPool) { - byte[] bytes = FurySerializationUtil.FURY.serialize(this.value); + byte[] bytes = FurySerializationUtil.FURY.serialize(this.property.value()); this.valueOffHeap = (ByteBuf) memoryPool.requireMemory(bytes.length, memoryPool); this.valueOffHeap.markReaderIndex(); this.valueOffHeap.writeBytes(bytes); @@ -60,7 +61,12 @@ public void serializeSelfToByteBuf(MemoryPool memoryPool) { @Override public void releaseOriginalVarsOnHeap() { - this.value = null; + this.property = new BaseProperty(this.pkey, null) { + @Override + public V value() { + return HugeVertexPropertyOffHeap.this.value(); + } + }; } @Override diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/util/OffHeapMemoryUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/util/OffHeapMemoryUtil.java index a63ddc91b7..3c726dea9a 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/util/OffHeapMemoryUtil.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/util/OffHeapMemoryUtil.java @@ -17,32 +17,16 @@ package org.apache.hugegraph.memory.util; -import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.hugegraph.util.UnsafeUtil; import sun.misc.Unsafe; // NOTE: due to different layout of klass in various versions of JDK, this class may easily crash! public class OffHeapMemoryUtil { - private static final Logger LOG = LoggerFactory.getLogger(OffHeapMemoryUtil.class); - private static final Unsafe UNSAFE; - - static { - Unsafe unsafe; - try { - Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); - unsafeField.setAccessible(true); - unsafe = (Unsafe) unsafeField.get(null); - } catch (Throwable cause) { - LOG.warn("sun.misc.Unsafe.theUnsafe: unavailable.", cause); - throw new UnsupportedOperationException("Unsafe is not supported in this platform."); - } - UNSAFE = unsafe; - } + private static final Unsafe UNSAFE = UnsafeUtil.getUnsafe(); public static long allocateObjectToOffHeap(Object obj) { long size = sizeOf(obj); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/util/OutOfMemoryException.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/util/OutOfMemoryException.java index c770752626..152e2f92cc 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/util/OutOfMemoryException.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/memory/util/OutOfMemoryException.java @@ -16,7 +16,7 @@ */ package org.apache.hugegraph.memory.util; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; public class OutOfMemoryException extends HugeException { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/EtcdMetaDriver.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/EtcdMetaDriver.java index 75042131c8..6af98e0a88 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/EtcdMetaDriver.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/EtcdMetaDriver.java @@ -31,7 +31,7 @@ import java.util.function.Consumer; import org.apache.commons.io.FileUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.meta.lock.EtcdDistributedLock; import org.apache.hugegraph.meta.lock.LockResult; import org.apache.hugegraph.type.define.CollectionType; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/MetaManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/MetaManager.java index b30b505d32..0bc917d78d 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/MetaManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/MetaManager.java @@ -36,8 +36,8 @@ import org.apache.hugegraph.auth.HugeRole; import org.apache.hugegraph.auth.HugeTarget; import org.apache.hugegraph.auth.HugeUser; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.meta.lock.LockResult; import org.apache.hugegraph.meta.managers.AuthMetaManager; import org.apache.hugegraph.meta.managers.ConfigMetaManager; @@ -49,10 +49,10 @@ import org.apache.hugegraph.meta.managers.ServiceMetaManager; import org.apache.hugegraph.meta.managers.SpaceMetaManager; import org.apache.hugegraph.meta.managers.TaskMetaManager; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.space.GraphSpace; import org.apache.hugegraph.space.SchemaTemplate; import org.apache.hugegraph.space.Service; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/PdMetaDriver.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/PdMetaDriver.java index 18d0ea3fc3..c744e02e8e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/PdMetaDriver.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/PdMetaDriver.java @@ -23,7 +23,7 @@ import java.util.Map; import java.util.function.Consumer; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.constant.ServiceConstant; import org.apache.hugegraph.meta.lock.LockResult; import org.apache.hugegraph.meta.lock.PdDistributedLock; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/lock/PdDistributedLock.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/lock/PdDistributedLock.java index 3e8c66ea14..2dbbdffdd0 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/lock/PdDistributedLock.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/lock/PdDistributedLock.java @@ -22,7 +22,7 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.pd.client.KvClient; import org.apache.hugegraph.pd.common.PDException; import org.apache.hugegraph.pd.grpc.kv.LockResponse; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/AbstractMetaManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/AbstractMetaManager.java index b7294a36e2..b1930144c0 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/AbstractMetaManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/AbstractMetaManager.java @@ -26,11 +26,11 @@ import java.util.function.Consumer; import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.auth.SchemaDefine; import org.apache.hugegraph.meta.MetaDriver; import org.apache.hugegraph.meta.lock.LockResult; -import org.apache.hugegraph.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaElement; import org.apache.hugegraph.util.JsonUtil; public class AbstractMetaManager { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManager.java index bf801c0849..03564ed0a6 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManager.java @@ -47,8 +47,8 @@ import org.apache.hugegraph.auth.HugeRole; import org.apache.hugegraph.auth.HugeTarget; import org.apache.hugegraph.auth.HugeUser; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.meta.MetaDriver; import org.apache.hugegraph.meta.MetaManager; import org.apache.hugegraph.util.E; @@ -855,8 +855,18 @@ public String targetFromAccess(String accessKey) { public void clearGraphAuth(String graphSpace) { E.checkArgument(StringUtils.isNotEmpty(graphSpace), "The graphSpace is empty"); - String prefix = this.authPrefix(graphSpace); - this.metaDriver.deleteWithPrefix(prefix); + // Graph keys sit beside AUTH under the graphspace, and a graph may be + // named AUTH or AUTHx. Delete each kind of auth key up to its + // delimiter, not the whole AUTH prefix, so the schema and task keys + // of such a graph are left alone + String[] prefixes = {this.roleListKey(graphSpace), + this.targetListKey(graphSpace), + this.belongListKey(graphSpace), + this.accessListKey(graphSpace), + this.projectListKey(graphSpace)}; + for (String prefix : prefixes) { + this.metaDriver.deleteWithPrefix(prefix + META_PATH_DELIMITER); + } } @SuppressWarnings("unchecked") @@ -1044,16 +1054,6 @@ private String userListKey() { META_PATH_USER); } - private String authPrefix(String graphSpace) { - // HUGEGRAPH/{cluster}/GRAPHSPACE/{graphSpace}/AUTH - return String.join(META_PATH_DELIMITER, - META_PATH_HUGEGRAPH, - this.cluster, - META_PATH_GRAPHSPACE, - graphSpace, - META_PATH_AUTH); - } - private String groupKey(String group) { // HUGEGRAPH/{cluster}/AUTH/GROUP/{group} return String.join(META_PATH_DELIMITER, diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/SchemaMetaManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/SchemaMetaManager.java index 57e2839837..e832fb50a6 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/SchemaMetaManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/SchemaMetaManager.java @@ -33,14 +33,14 @@ import java.util.Map; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.meta.MetaDriver; import org.apache.hugegraph.meta.PdMetaDriver; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.util.JsonUtil; public class SchemaMetaManager extends AbstractMetaManager { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/SchemaTemplateMetaManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/SchemaTemplateMetaManager.java index bc2f1448bc..0fa8a0328a 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/SchemaTemplateMetaManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/SchemaTemplateMetaManager.java @@ -27,7 +27,7 @@ import java.util.Set; import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.meta.MetaDriver; import org.apache.hugegraph.space.SchemaTemplate; import org.apache.hugegraph.util.JsonUtil; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/ServiceMetaManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/ServiceMetaManager.java index 3c03be3433..14a453aef8 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/ServiceMetaManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/managers/ServiceMetaManager.java @@ -50,12 +50,11 @@ private static String serviceName(String graphSpace, String name) { public Map serviceConfigs(String graphSpace) { Map serviceMap = new HashMap<>(); - Map keyValues = this.metaDriver.scanWithPrefix( - this.serviceConfPrefix(graphSpace)); + String prefix = this.serviceConfPrefix(graphSpace); + Map keyValues = this.metaDriver.scanWithPrefix(prefix); for (Map.Entry entry : keyValues.entrySet()) { String key = entry.getKey(); - String[] parts = key.split(META_PATH_DELIMITER); - serviceMap.put(parts[parts.length - 1], + serviceMap.put(key.substring(prefix.length()), JsonUtil.fromJson(entry.getValue(), Service.class)); } return serviceMap; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/plugin/HugeGraphGremlinPlugin.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/plugin/HugeGraphGremlinPlugin.java index 0b3b19ba7e..8ae5c80096 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/plugin/HugeGraphGremlinPlugin.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/plugin/HugeGraphGremlinPlugin.java @@ -22,7 +22,7 @@ import java.util.Iterator; import java.util.Set; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.util.ReflectionUtil; import org.apache.tinkerpop.gremlin.jsr223.AbstractGremlinPlugin; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Server.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Server.java deleted file mode 100644 index 4648244734..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Server.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.rpc; - -public interface RpcServiceConfig4Server { - - String addService(Class clazz, S serviceImpl); - - String addService(String graph, Class clazz, - S serviceImpl); - - void removeService(String serviceId); - - void removeAllService(); -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/EdgeLabel.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/EdgeLabel.java deleted file mode 100644 index da572321c1..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/EdgeLabel.java +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.schema; - -import java.util.ArrayList; -import java.util.Arrays; -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.stream.Collectors; - -import org.apache.commons.lang3.tuple.Pair; -import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.builder.SchemaBuilder; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.define.Directions; -import org.apache.hugegraph.type.define.EdgeLabelType; -import org.apache.hugegraph.type.define.Frequency; -import org.apache.hugegraph.type.define.SchemaStatus; -import org.apache.hugegraph.util.E; - -import com.google.common.base.Objects; - -public class EdgeLabel extends SchemaLabel { - - public static final EdgeLabel NONE = new EdgeLabel(null, NONE_ID, UNDEF); - - private Set> links = new HashSet<>(); - private Id sourceLabel = NONE_ID; - private Id targetLabel = NONE_ID; - private Frequency frequency; - private List sortKeys; - - private EdgeLabelType edgeLabelType = EdgeLabelType.NORMAL; - private Id fatherId; - - public EdgeLabel(final HugeGraph graph, Id id, String name) { - super(graph, id, name); - this.frequency = Frequency.DEFAULT; - this.sortKeys = new ArrayList<>(); - } - - @Override - public HugeType type() { - return HugeType.EDGE_LABEL; - } - - public boolean isFather() { - return this.edgeLabelType.parent(); - } - - public void edgeLabelType(EdgeLabelType type) { - this.edgeLabelType = type; - } - - public EdgeLabelType edgeLabelType() { - return this.edgeLabelType; - } - - public boolean hasFather() { - return this.edgeLabelType.sub(); - } - - public Id fatherId() { - return this.fatherId; - } - - public void fatherId(Id fatherId) { - this.fatherId = fatherId; - } - - public Frequency frequency() { - return this.frequency; - } - - public void frequency(Frequency frequency) { - this.frequency = frequency; - } - - public boolean directed() { - // TODO: implement (do we need this method?) - return true; - } - - public String sourceLabelName() { - E.checkState(this.links.size() == 1, - "Only edge label has single vertex label pair can call " + - "sourceLabelName(), but current edge label got %s", - this.links.size()); - return this.graph.vertexLabelOrNone(this.links.iterator().next().getLeft()).name(); - } - - public List linksIds() { - List ids = new ArrayList<>(this.links.size() * 2); - for (Pair link : this.links) { - ids.add(link.getLeft()); - ids.add(link.getRight()); - } - return ids; - } - - public void linksIds(Id[] ids) { - this.links = new HashSet<>(ids.length / 2); - for (int i = 0; i < ids.length; i += 2) { - this.links.add(Pair.of(ids[i], ids[i + 1])); - } - } - - public Id sourceLabel() { - if (links.size() == 1) { - return links.iterator().next().getLeft(); - } - return NONE_ID; - } - - public void sourceLabel(Id id) { - E.checkArgument(this.links.isEmpty(), - "Not allowed add source label to an edge label which " + - "already has links"); - if (this.targetLabel != NONE_ID) { - this.links.add(Pair.of(id, this.targetLabel)); - this.targetLabel = NONE_ID; - } else { - this.sourceLabel = id; - } - } - - public String targetLabelName() { - E.checkState(this.links.size() == 1, - "Only edge label has single vertex label pair can call " + - "sourceLabelName(), but current edge label got %s", - this.links.size()); - return this.graph.vertexLabelOrNone(this.links.iterator().next().getRight()).name(); - } - - public Id targetLabel() { - if (links.size() == 1) { - return links.iterator().next().getRight(); - } - return NONE_ID; - } - - public void targetLabel(Id id) { - E.checkArgument(this.links.isEmpty(), - "Not allowed add source label to an edge label which " + - "already has links"); - if (this.sourceLabel != NONE_ID) { - this.links.add(Pair.of(this.sourceLabel, id)); - this.sourceLabel = NONE_ID; - } else { - this.targetLabel = id; - } - } - - public boolean linkWithLabel(Id id) { - for (Pair link : this.links) { - if (link.getLeft().equals(id) || link.getRight().equals(id)) { - return true; - } - } - return false; - } - - public boolean linkWithVertexLabel(Id label, Directions dir) { - return this.links.stream().anyMatch(pair -> { - Id sourceLabel = pair.getLeft(); - Id targetLabel = pair.getRight(); - if (dir.equals(Directions.IN)) { - return targetLabel.equals(label); - } else if (dir.equals(Directions.OUT)) { - return sourceLabel.equals(label); - } else if (dir.equals(Directions.BOTH)) { - return targetLabel.equals(label) || sourceLabel.equals(label); - } - return false; - }); - } - - public boolean checkLinkEqual(Id sourceLabel, Id targetLabel) { - return this.links.contains(Pair.of(sourceLabel, targetLabel)); - } - - public Set> links() { - return this.links; - } - - public void links(Pair link) { - if (this.links == null) { - this.links = new HashSet<>(); - } - this.links.add(link); - } - - public boolean existSortKeys() { - return !this.sortKeys.isEmpty(); - } - - public List sortKeys() { - return Collections.unmodifiableList(this.sortKeys); - } - - public void sortKey(Id id) { - this.sortKeys.add(id); - } - - public void sortKeys(Id... ids) { - this.sortKeys.addAll(Arrays.asList(ids)); - } - - public boolean hasSameContent(EdgeLabel other) { - return super.hasSameContent(other) && - this.frequency == other.frequency && - Objects.equal(this.sourceLabelName(), other.sourceLabelName()) && - Objects.equal(this.targetLabelName(), other.targetLabelName()) && - Objects.equal(this.graph.mapPkId2Name(this.sortKeys), - other.graph.mapPkId2Name(other.sortKeys)); - } - - public static EdgeLabel undefined(HugeGraph graph, Id id) { - return new EdgeLabel(graph, id, UNDEF); - } - - public interface Builder extends SchemaBuilder { - - Id rebuildIndex(); - - Builder asBase(); - - Builder withBase(String fatherLabel); - - Builder link(String sourceLabel, String targetLabel); - - @Deprecated - Builder sourceLabel(String label); - - @Deprecated - Builder targetLabel(String label); - - Builder singleTime(); - - Builder multiTimes(); - - Builder sortKeys(String... keys); - - Builder properties(String... properties); - - Builder nullableKeys(String... keys); - - Builder frequency(Frequency frequency); - - Builder ttl(long ttl); - - Builder ttlStartTime(String ttlStartTime); - - Builder enableLabelIndex(boolean enable); - - Builder userdata(String key, Object value); - - Builder userdata(Map userdata); - } - - @Override - public Map asMap() { - Map map = new HashMap<>(); - - if (this.sourceLabel() != null && this.sourceLabel() != NONE_ID) { - map.put(P.SOURCE_LABEL, this.sourceLabel().asString()); - } - - if (this.targetLabel() != null && this.targetLabel() != NONE_ID) { - map.put(P.TARGET_LABEL, this.targetLabel().asString()); - } - - if (this.properties() != null) { - map.put(P.PROPERTIES, this.properties()); - } - - if (this.nullableKeys() != null) { - map.put(P.NULLABLE_KEYS, this.nullableKeys()); - } - - if (this.indexLabels() != null) { - map.put(P.INDEX_LABELS, this.indexLabels()); - } - - if (this.ttlStartTime() != null) { - map.put(P.TT_START_TIME, this.ttlStartTime().asString()); - } - - if (this.sortKeys() != null) { - map.put(P.SORT_KEYS, this.sortKeys); - } - - map.put(P.EDGELABEL_TYPE, this.edgeLabelType); - if (this.fatherId() != null) { - map.put(P.FATHER_ID, this.fatherId().asString()); - } - map.put(P.ENABLE_LABEL_INDEX, this.enableLabelIndex()); - map.put(P.TTL, String.valueOf(this.ttl())); - map.put(P.LINKS, this.links()); - map.put(P.FREQUENCY, this.frequency().toString()); - - return super.asMap(map); - } - - @SuppressWarnings("unchecked") - public static EdgeLabel fromMap(Map map, HugeGraph graph) { - Id id = IdGenerator.of((int) map.get(EdgeLabel.P.ID)); - String name = (String) map.get(EdgeLabel.P.NAME); - EdgeLabel edgeLabel = new EdgeLabel(graph, id, name); - for (Map.Entry entry : map.entrySet()) { - switch (entry.getKey()) { - case P.ID: - case P.NAME: - break; - case P.STATUS: - edgeLabel.status( - SchemaStatus.valueOf(((String) entry.getValue()).toUpperCase())); - break; - case P.USERDATA: - edgeLabel.userdata(new Userdata((Map) entry.getValue())); - break; - case P.PROPERTIES: - Set ids = ((List) entry.getValue()).stream().map( - IdGenerator::of).collect(Collectors.toSet()); - edgeLabel.properties(ids); - break; - case P.NULLABLE_KEYS: - ids = ((List) entry.getValue()).stream().map( - IdGenerator::of).collect(Collectors.toSet()); - edgeLabel.nullableKeys(ids); - break; - case P.INDEX_LABELS: - ids = ((List) entry.getValue()).stream().map( - IdGenerator::of).collect(Collectors.toSet()); - edgeLabel.addIndexLabels(ids.toArray(new Id[0])); - break; - case P.ENABLE_LABEL_INDEX: - boolean enableLabelIndex = (Boolean) entry.getValue(); - edgeLabel.enableLabelIndex(enableLabelIndex); - break; - case P.TTL: - long ttl = Long.parseLong((String) entry.getValue()); - edgeLabel.ttl(ttl); - break; - case P.TT_START_TIME: - long ttlStartTime = - Long.parseLong((String) entry.getValue()); - edgeLabel.ttlStartTime(IdGenerator.of(ttlStartTime)); - break; - case P.LINKS: - // TODO: serialize and deserialize - List list = (List) entry.getValue(); - for (Map m : list) { - for (Object key : m.keySet()) { - Id sid = IdGenerator.of(Long.parseLong((String) key)); - Id tid = IdGenerator.of(Long.parseLong(String.valueOf(m.get(key)))); - edgeLabel.links(Pair.of(sid, tid)); - } - } - break; - case P.SOURCE_LABEL: - long sourceLabel = - Long.parseLong((String) entry.getValue()); - edgeLabel.sourceLabel(IdGenerator.of(sourceLabel)); - break; - case P.TARGET_LABEL: - long targetLabel = - Long.parseLong((String) entry.getValue()); - edgeLabel.targetLabel(IdGenerator.of(targetLabel)); - break; - case P.FATHER_ID: - long fatherId = - Long.parseLong((String) entry.getValue()); - edgeLabel.fatherId(IdGenerator.of(fatherId)); - break; - case P.EDGELABEL_TYPE: - EdgeLabelType edgeLabelType = - EdgeLabelType.valueOf( - ((String) entry.getValue()).toUpperCase()); - edgeLabel.edgeLabelType(edgeLabelType); - break; - case P.FREQUENCY: - Frequency frequency = - Frequency.valueOf(((String) entry.getValue()).toUpperCase()); - edgeLabel.frequency(frequency); - break; - case P.SORT_KEYS: - ids = ((List) entry.getValue()).stream().map( - IdGenerator::of).collect(Collectors.toSet()); - edgeLabel.sortKeys(ids.toArray(new Id[0])); - break; - default: - throw new AssertionError(String.format( - "Invalid key '%s' for edge label", - entry.getKey())); - } - } - return edgeLabel; - } - - public static final class P { - - public static final String ID = "id"; - public static final String NAME = "name"; - - public static final String STATUS = "status"; - public static final String USERDATA = "userdata"; - - public static final String PROPERTIES = "properties"; - public static final String NULLABLE_KEYS = "nullableKeys"; - public static final String INDEX_LABELS = "indexLabels"; - - public static final String ENABLE_LABEL_INDEX = "enableLabelIndex"; - public static final String TTL = "ttl"; - public static final String TT_START_TIME = "ttlStartTime"; - public static final String LINKS = "links"; - public static final String SOURCE_LABEL = "sourceLabel"; - public static final String TARGET_LABEL = "targetLabel"; - public static final String EDGELABEL_TYPE = "edgeLabelType"; - public static final String FATHER_ID = "fatherId"; - public static final String FREQUENCY = "frequency"; - public static final String SORT_KEYS = "sortKeys"; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/IndexLabel.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/IndexLabel.java deleted file mode 100644 index e20a728c2b..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/IndexLabel.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.schema; - -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.stream.Collectors; - -import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.builder.SchemaBuilder; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.define.IndexType; -import org.apache.hugegraph.type.define.SchemaStatus; -import org.apache.hugegraph.util.E; - -import com.google.common.base.Objects; - -public class IndexLabel extends SchemaElement { - - private HugeType baseType; - private Id baseValue; - private IndexType indexType; - private List indexFields; - - public IndexLabel(final HugeGraph graph, Id id, String name) { - super(graph, id, name); - this.baseType = HugeType.SYS_SCHEMA; - this.baseValue = NONE_ID; - this.indexType = IndexType.SECONDARY; - this.indexFields = new ArrayList<>(); - } - - protected IndexLabel(long id, String name) { - this(null, IdGenerator.of(id), name); - } - - @Override - public HugeType type() { - return HugeType.INDEX_LABEL; - } - - public HugeType baseType() { - return this.baseType; - } - - public void baseType(HugeType baseType) { - this.baseType = baseType; - } - - public Id baseValue() { - return this.baseValue; - } - - public void baseValue(Id id) { - this.baseValue = id; - } - - public IndexType indexType() { - return this.indexType; - } - - public void indexType(IndexType indexType) { - this.indexType = indexType; - } - - public HugeType queryType() { - switch (this.baseType) { - case VERTEX_LABEL: - return HugeType.VERTEX; - case EDGE_LABEL: - return HugeType.EDGE; - case SYS_SCHEMA: - return HugeType.SYS_SCHEMA; - default: - throw new AssertionError(String.format( - "Query type of index label is either '%s' or '%s', " + - "but '%s' is used", - HugeType.VERTEX_LABEL, HugeType.EDGE_LABEL, - this.baseType)); - } - } - - public List indexFields() { - return Collections.unmodifiableList(this.indexFields); - } - - public void indexFields(Id... ids) { - this.indexFields.addAll(Arrays.asList(ids)); - } - - public void indexField(Id id) { - this.indexFields.add(id); - } - - public Id indexField() { - E.checkState(this.indexFields.size() == 1, - "There should be only one field in %s index label, " + - "but got: %s", this.indexType.string(), this.indexFields); - return this.indexFields.get(0); - } - - public SchemaLabel baseLabel() { - return getBaseLabel(this.graph, this.baseType, this.baseValue); - } - - public boolean hasSameContent(IndexLabel other) { - return super.hasSameContent(other) && - this.indexType == other.indexType && - this.baseType == other.baseType && - Objects.equal(this.graph.mapPkId2Name(this.indexFields), - other.graph.mapPkId2Name(other.indexFields)); - } - - public boolean olap() { - return VertexLabel.OLAP_VL.id().equals(this.baseValue); - } - - public Object validValue(Object value) { - if (!(value instanceof Number)) { - return value; - } - - Number number = (Number) value; - switch (this.indexType()) { - case RANGE_INT: - return number.intValue(); - case RANGE_LONG: - return number.longValue(); - case RANGE_FLOAT: - return number.floatValue(); - case RANGE_DOUBLE: - return number.doubleValue(); - default: - return value; - } - } - - // Label index - private static final IndexLabel VL_IL = new IndexLabel(VL_IL_ID, "~vli"); - private static final IndexLabel EL_IL = new IndexLabel(EL_IL_ID, "~eli"); - - // Schema name index - private static final IndexLabel PKN_IL = new IndexLabel(PKN_IL_ID, "~pkni"); - private static final IndexLabel VLN_IL = new IndexLabel(VLN_IL_ID, "~vlni"); - private static final IndexLabel ELN_IL = new IndexLabel(ELN_IL_ID, "~elni"); - private static final IndexLabel ILN_IL = new IndexLabel(ILN_IL_ID, "~ilni"); - - public static IndexLabel label(HugeType type) { - switch (type) { - case TASK: - case SERVER: - case VERTEX: - return VL_IL; - case EDGE: - case EDGE_OUT: - case EDGE_IN: - return EL_IL; - case PROPERTY_KEY: - return PKN_IL; - case VERTEX_LABEL: - return VLN_IL; - case EDGE_LABEL: - return ELN_IL; - case INDEX_LABEL: - return ILN_IL; - default: - throw new AssertionError(String.format( - "No primitive index label for '%s'", type)); - } - } - - public static IndexLabel label(HugeGraph graph, Id id) { - // Primitive IndexLabel first - if (id.asLong() < 0 && id.asLong() > -NEXT_PRIMITIVE_SYS_ID) { - switch ((int) id.asLong()) { - case VL_IL_ID: - return VL_IL; - case EL_IL_ID: - return EL_IL; - case PKN_IL_ID: - return PKN_IL; - case VLN_IL_ID: - return VLN_IL; - case ELN_IL_ID: - return ELN_IL; - case ILN_IL_ID: - return ILN_IL; - default: - throw new AssertionError(String.format( - "No primitive index label for '%s'", id)); - } - } - return graph.indexLabel(id); - } - - public static SchemaLabel getBaseLabel(HugeGraph graph, - HugeType baseType, - Object baseValue) { - E.checkNotNull(baseType, "base type", "index label"); - E.checkNotNull(baseValue, "base value", "index label"); - E.checkArgument(baseValue instanceof String || baseValue instanceof Id, - "The base value must be instance of String or Id, " + - "but got %s(%s)", baseValue, - baseValue.getClass().getSimpleName()); - - SchemaLabel label; - switch (baseType) { - case VERTEX_LABEL: - if (baseValue instanceof String) { - label = graph.vertexLabel((String) baseValue); - } else { - assert baseValue instanceof Id; - label = graph.vertexLabel((Id) baseValue); - } - break; - case EDGE_LABEL: - if (baseValue instanceof String) { - label = graph.edgeLabel((String) baseValue); - } else { - assert baseValue instanceof Id; - label = graph.edgeLabel((Id) baseValue); - } - break; - default: - throw new AssertionError(String.format( - "Unsupported base type '%s' of index label", - baseType)); - } - - E.checkArgumentNotNull(label, "Can't find the %s with name '%s'", - baseType.readableName(), baseValue); - return label; - } - - public interface Builder extends SchemaBuilder { - - TaskWithSchema createWithTask(); - - Id rebuild(); - - Builder onV(String baseValue); - - Builder onE(String baseValue); - - Builder by(String... fields); - - Builder secondary(); - - Builder range(); - - Builder search(); - - Builder shard(); - - Builder unique(); - - Builder on(HugeType baseType, String baseValue); - - Builder indexType(IndexType indexType); - - Builder userdata(String key, Object value); - - Builder userdata(Map userdata); - - Builder rebuild(boolean rebuild); - } - - @Override - public Map asMap() { - HashMap map = new HashMap<>(); - map.put(P.BASE_TYPE, this.baseType().name()); - map.put(P.BASE_VALUE, this.baseValue().asString()); - map.put(P.INDEX_TYPE, this.indexType().name()); - map.put(P.INDEX_FIELDS, this.indexFields()); - return super.asMap(map); - } - - @SuppressWarnings("unchecked") - public static IndexLabel fromMap(Map map, HugeGraph graph) { - Id id = IdGenerator.of((int) map.get(IndexLabel.P.ID)); - String name = (String) map.get(IndexLabel.P.NAME); - - IndexLabel indexLabel = new IndexLabel(graph, id, name); - for (Map.Entry entry : map.entrySet()) { - switch (entry.getKey()) { - case P.ID: - case P.NAME: - break; - case P.STATUS: - indexLabel.status( - SchemaStatus.valueOf(((String) entry.getValue()).toUpperCase())); - break; - case P.USERDATA: - indexLabel.userdata(new Userdata((Map) entry.getValue())); - break; - case P.BASE_TYPE: - HugeType hugeType = - HugeType.valueOf(((String) entry.getValue()).toUpperCase()); - indexLabel.baseType(hugeType); - break; - case P.BASE_VALUE: - long sourceLabel = - Long.parseLong((String) entry.getValue()); - indexLabel.baseValue(IdGenerator.of(sourceLabel)); - break; - case P.INDEX_TYPE: - IndexType indexType = - IndexType.valueOf(((String) entry.getValue()).toUpperCase()); - indexLabel.indexType(indexType); - break; - case P.INDEX_FIELDS: - List ids = ((List) entry.getValue()).stream().map( - IdGenerator::of).collect(Collectors.toList()); - indexLabel.indexFields(ids.toArray(new Id[0])); - break; - default: - throw new AssertionError(String.format( - "Invalid key '%s' for index label", - entry.getKey())); - } - } - return indexLabel; - } - - public static final class P { - - public static final String ID = "id"; - public static final String NAME = "name"; - - public static final String STATUS = "status"; - public static final String USERDATA = "userdata"; - - public static final String BASE_TYPE = "baseType"; - public static final String BASE_VALUE = "baseValue"; - public static final String INDEX_TYPE = "indexType"; - public static final String INDEX_FIELDS = "indexFields"; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/PropertyKey.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/PropertyKey.java deleted file mode 100644 index 5bf34ea530..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/PropertyKey.java +++ /dev/null @@ -1,514 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.schema; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.exception.NotSupportException; -import org.apache.hugegraph.schema.builder.SchemaBuilder; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.Propertiable; -import org.apache.hugegraph.type.define.AggregateType; -import org.apache.hugegraph.type.define.Cardinality; -import org.apache.hugegraph.type.define.DataType; -import org.apache.hugegraph.type.define.SchemaStatus; -import org.apache.hugegraph.type.define.WriteType; -import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.LongEncoding; - -public class PropertyKey extends SchemaElement implements Propertiable { - - private DataType dataType; - private Cardinality cardinality; - private AggregateType aggregateType; - private WriteType writeType; - - public PropertyKey(final HugeGraph graph, Id id, String name) { - super(graph, id, name); - this.dataType = DataType.TEXT; - this.cardinality = Cardinality.SINGLE; - this.aggregateType = AggregateType.NONE; - this.writeType = WriteType.OLTP; - } - - @Override - public HugeType type() { - return HugeType.PROPERTY_KEY; - } - - public DataType dataType() { - return this.dataType; - } - - public void dataType(DataType dataType) { - this.dataType = dataType; - } - - public Cardinality cardinality() { - return this.cardinality; - } - - public void cardinality(Cardinality cardinality) { - this.cardinality = cardinality; - } - - public AggregateType aggregateType() { - return this.aggregateType; - } - - public void aggregateType(AggregateType aggregateType) { - this.aggregateType = aggregateType; - } - - public void writeType(WriteType writeType) { - this.writeType = writeType; - } - - public WriteType writeType() { - return this.writeType; - } - - public boolean oltp() { - return this.writeType.oltp(); - } - - public boolean olap() { - return this.writeType.olap(); - } - - @Override - public Set properties() { - return Collections.emptySet(); - } - - public PropertyKey properties(Id... properties) { - if (properties.length > 0) { - throw new NotSupportException("PropertyKey.properties(Id)"); - } - return this; - } - - public void defineDefaultValue(Object value) { - // TODO add a field default_value - this.userdata().put(Userdata.DEFAULT_VALUE, value); - } - - public Object defaultValue() { - // TODO add a field default_value - Object value = this.userdata().get(Userdata.DEFAULT_VALUE); - if (value == null) { - return null; - } - - // Userdata is reloaded from JSON as a raw Map, so a typed default - // value (e.g. Date) comes back as a String. Normalize it to the - // runtime type expected by this property key's data type. Idempotent - // for values already of the expected type. - Object raw = value; - if (this.cardinality == Cardinality.SET && value instanceof Collection && - !(value instanceof Set)) { - raw = new LinkedHashSet<>((Collection) value); - } - - return this.validValueOrThrow(raw); - } - - public boolean hasSameContent(PropertyKey other) { - return super.hasSameContent(other) && - this.dataType == other.dataType() && - this.cardinality == other.cardinality() && - this.aggregateType == other.aggregateType() && - this.writeType == other.writeType(); - } - - public String clazz() { - String dataType = this.dataType().clazz().getSimpleName(); - switch (this.cardinality) { - case SINGLE: - return dataType; - // A set of values: Set - case SET: - return String.format("Set<%s>", dataType); - // A list of values: List - case LIST: - return String.format("List<%s>", dataType); - default: - throw new AssertionError(String.format( - "Unsupported cardinality: '%s'", this.cardinality)); - } - } - - public Class implementClazz() { - Class cls; - switch (this.cardinality) { - case SINGLE: - cls = this.dataType().clazz(); - break; - // A set of values: Set - case SET: - cls = LinkedHashSet.class; - break; - // A list of values: List - case LIST: - cls = ArrayList.class; - break; - default: - throw new AssertionError(String.format( - "Unsupported cardinality: '%s'", this.cardinality)); - } - return cls; - } - - @SuppressWarnings("unchecked") - public T newValue() { - switch (this.cardinality) { - case SET: - return (T) new LinkedHashSet<>(); - case LIST: - return (T) new ArrayList<>(); - default: - // pass - break; - } - - try { - return (T) this.implementClazz().newInstance(); - } catch (Exception e) { - throw new HugeException("Failed to new instance of %s: %s", - this.implementClazz(), e.toString()); - } - } - - /** - * Check property value valid - * - * @param value the property value to be checked data type and cardinality - * @param the property value class - * @return true if data type and cardinality satisfy requirements, - * otherwise false - */ - public boolean checkValueType(V value) { - boolean valid; - - switch (this.cardinality) { - case SINGLE: - valid = this.checkDataType(value); - break; - case SET: - valid = value instanceof Set; - valid = valid && this.checkDataType((Set) value); - break; - case LIST: - valid = value instanceof List; - valid = valid && this.checkDataType((List) value); - break; - default: - throw new AssertionError(String.format( - "Unsupported cardinality: '%s'", this.cardinality)); - } - return valid; - } - - /** - * Check type of the value valid - * - * @param value the property value to be checked data type - * @param the property value original data type - * @return true if the value is or can convert to the data type, - * otherwise false - */ - private boolean checkDataType(V value) { - return this.dataType().clazz().isInstance(value); - } - - /** - * Check type of all the values(maybe some list properties) valid - * - * @param values the property values to be checked data type - * @param the property value class - * @return true if all the values are or can convert to the data type, - * otherwise false - */ - private boolean checkDataType(Collection values) { - boolean valid = true; - for (Object o : values) { - if (!this.checkDataType(o)) { - valid = false; - break; - } - } - return valid; - } - - public Object serialValue(V value, boolean encodeNumber) { - V validValue = this.validValue(value); - E.checkArgument(validValue != null, - "Invalid property value '%s' for key '%s'", - value, this.name()); - E.checkArgument(this.cardinality.single(), - "The cardinality can't be '%s' for navigation key '%s'", - this.cardinality, this.name()); - if (this.dataType.isNumber() || this.dataType.isDate()) { - if (encodeNumber) { - return LongEncoding.encodeNumber(validValue); - } else { - return validValue.toString(); - } - } - return validValue; - } - - public V validValueOrThrow(V value) { - V validValue = this.validValue(value); - if (validValue == null) { - E.checkArgument(false, - "Invalid property value '%s' for key '%s', " + - "expect a value of type %s, actual type %s", - value, this.name(), this.clazz(), - value.getClass().getSimpleName()); - } - return validValue; - } - - public V validValue(V value) { - try { - return this.convValue(value); - } catch (RuntimeException e) { - throw new IllegalArgumentException(String.format( - "Invalid property value '%s' for key '%s': %s", - value, this.name(), e.getMessage())); - } - } - - @SuppressWarnings("unchecked") - private V convValue(V value) { - if (value == null) { - return null; - } - if (this.checkValueType(value)) { - // Same as expected type, no conversion required - return value; - } - - V validValue = null; - Collection validValues; - if (this.cardinality.single()) { - validValue = this.convSingleValue(value); - } else if (value instanceof Collection) { - assert this.cardinality.multiple(); - Collection collection = (Collection) value; - if (value instanceof Set) { - validValues = new LinkedHashSet<>(collection.size()); - } else { - assert value instanceof List; - validValues = new ArrayList<>(collection.size()); - } - for (T element : collection) { - element = this.convSingleValue(element); - if (element == null) { - validValues = null; - break; - } - validValues.add(element); - } - validValue = (V) validValues; - } else { - assert this.cardinality.multiple(); - E.checkArgument(false, - "Property value must be %s, but got '%s'(%s)", - this.cardinality, value, - value.getClass().getSimpleName()); - } - return validValue; - } - - private V convSingleValue(V value) { - if (value == null) { - return null; - } - if (this.dataType().isNumber()) { - @SuppressWarnings("unchecked") - V number = (V) this.dataType().valueToNumber(value); - return number; - } else if (this.dataType().isDate()) { - @SuppressWarnings("unchecked") - V date = (V) this.dataType().valueToDate(value); - return date; - } else if (this.dataType().isUUID()) { - @SuppressWarnings("unchecked") - V uuid = (V) this.dataType().valueToUUID(value); - return uuid; - } else if (this.dataType().isBlob()) { - @SuppressWarnings("unchecked") - V blob = (V) this.dataType().valueToBlob(value); - return blob; - } - - if (this.checkDataType(value)) { - return value; - } - return null; - } - - public interface Builder extends SchemaBuilder { - - TaskWithSchema createWithTask(); - - Builder asText(); - - Builder asInt(); - - Builder asDate(); - - Builder asUUID(); - - Builder asBoolean(); - - Builder asByte(); - - Builder asBlob(); - - Builder asDouble(); - - Builder asFloat(); - - Builder asLong(); - - Builder valueSingle(); - - Builder valueList(); - - Builder valueSet(); - - Builder calcMax(); - - Builder calcMin(); - - Builder calcSum(); - - Builder calcOld(); - - Builder calcSet(); - - Builder calcList(); - - Builder writeType(WriteType writeType); - - Builder cardinality(Cardinality cardinality); - - Builder dataType(DataType dataType); - - Builder aggregateType(AggregateType aggregateType); - - Builder userdata(String key, Object value); - - Builder userdata(Map userdata); - } - - @Override - public Map asMap() { - Map map = new HashMap<>(); - - if (this.dataType != null) { - map.put(P.DATA_TYPE, this.dataType.string()); - } - - if (this.cardinality != null) { - map.put(P.CARDINALITY, this.cardinality.string()); - } - - if (this.aggregateType != null) { - map.put(P.AGGREGATE_TYPE, this.aggregateType.string()); - } - - if (this.writeType != null) { - map.put(P.WRITE_TYPE, this.writeType.string()); - } - - return super.asMap(map); - } - - @SuppressWarnings("unchecked") - public static PropertyKey fromMap(Map map, HugeGraph graph) { - Id id = IdGenerator.of((int) map.get(P.ID)); - String name = (String) map.get(P.NAME); - - PropertyKey propertyKey = new PropertyKey(graph, id, name); - for (Map.Entry entry : map.entrySet()) { - switch (entry.getKey()) { - case P.ID: - case P.NAME: - break; - case P.STATUS: - propertyKey.status( - SchemaStatus.valueOf(((String) entry.getValue()).toUpperCase())); - break; - case P.USERDATA: - propertyKey.userdata(new Userdata((Map) entry.getValue())); - break; - case P.AGGREGATE_TYPE: - propertyKey.aggregateType( - AggregateType.valueOf(((String) entry.getValue()).toUpperCase())); - break; - case P.WRITE_TYPE: - propertyKey.writeType( - WriteType.valueOf(((String) entry.getValue()).toUpperCase())); - break; - case P.DATA_TYPE: - propertyKey.dataType( - DataType.valueOf(((String) entry.getValue()).toUpperCase())); - break; - case P.CARDINALITY: - propertyKey.cardinality( - Cardinality.valueOf(((String) entry.getValue()).toUpperCase())); - break; - default: - throw new AssertionError(String.format( - "Invalid key '%s' for property key", - entry.getKey())); - } - } - return propertyKey; - } - - public static final class P { - - public static final String ID = "id"; - public static final String NAME = "name"; - - public static final String STATUS = "status"; - public static final String USERDATA = "userdata"; - - public static final String DATA_TYPE = "data_type"; - public static final String CARDINALITY = "cardinality"; - - public static final String AGGREGATE_TYPE = "aggregate_type"; - public static final String WRITE_TYPE = "write_type"; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaElement.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaElement.java deleted file mode 100644 index c36eeaf632..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaElement.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.schema; - -import java.util.Collections; -import java.util.Map; - -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.type.Nameable; -import org.apache.hugegraph.type.Typeable; -import org.apache.hugegraph.type.define.SchemaStatus; -import org.apache.hugegraph.util.E; -import org.apache.tinkerpop.gremlin.structure.Graph; - -import com.google.common.base.Objects; - -public abstract class SchemaElement implements Nameable, Typeable, - Cloneable { - - public static final int MAX_PRIMITIVE_SYS_ID = 32; - public static final int NEXT_PRIMITIVE_SYS_ID = 8; - - // ABS of system schema id must be below MAX_PRIMITIVE_SYS_ID - protected static final int VL_IL_ID = -1; - protected static final int EL_IL_ID = -2; - protected static final int PKN_IL_ID = -3; - protected static final int VLN_IL_ID = -4; - protected static final int ELN_IL_ID = -5; - protected static final int ILN_IL_ID = -6; - protected static final int OLAP_VL_ID = -7; - - // OLAP_ID means all of vertex label ids - public static final Id OLAP_ID = IdGenerator.of(-7); - // OLAP means all of vertex label names - public static final String OLAP = "~olap"; - - public static final Id NONE_ID = IdGenerator.ZERO; - - public static final String UNDEF = "~undefined"; - - protected final HugeGraph graph; - - private final Id id; - private final String name; - private final Userdata userdata; - private SchemaStatus status; - - public SchemaElement(final HugeGraph graph, Id id, String name) { - E.checkArgumentNotNull(id, "SchemaElement id can't be null"); - E.checkArgumentNotNull(name, "SchemaElement name can't be null"); - this.graph = graph; - this.id = id; - this.name = name; - this.userdata = new Userdata(); - this.status = SchemaStatus.CREATED; - } - - public HugeGraph graph() { - E.checkState(this.graph != null, - "Graph is null of schema '%s'", this.name); - return this.graph; - } - - public Id id() { - return this.id; - } - - public long longId() { - return this.id.asLong(); - } - - @Override - public String name() { - return this.name; - } - - public Map userdata() { - return Collections.unmodifiableMap(this.userdata); - } - - /** - * Add userdata. String values of {@link Userdata#CREATE_TIME} are - * normalized to {@link java.util.Date} and malformed strings are rejected. - */ - public void userdata(String key, Object value) { - E.checkArgumentNotNull(key, "userdata key"); - E.checkArgumentNotNull(value, "userdata value"); - this.userdata.put(key, value); - } - - /** - * Add userdata in bulk. String values of {@link Userdata#CREATE_TIME} are - * normalized to {@link java.util.Date} and malformed strings are rejected. - */ - public void userdata(Userdata userdata) { - E.checkArgumentNotNull(userdata, "userdata"); - this.userdata.putAll(userdata); - } - - public void removeUserdata(String key) { - E.checkArgumentNotNull(key, "The userdata key can't be null"); - this.userdata.remove(key); - } - - public void removeUserdata(Userdata userdata) { - E.checkArgumentNotNull(userdata, "userdata"); - for (String key : userdata.keySet()) { - this.userdata.remove(key); - } - } - - public SchemaStatus status() { - return this.status; - } - - public void status(SchemaStatus status) { - this.status = status; - } - - public boolean system() { - return this.longId() < 0L; - } - - public boolean primitive() { - long id = this.longId(); - return -MAX_PRIMITIVE_SYS_ID <= id && id < 0L; - } - - public boolean hidden() { - return Graph.Hidden.isHidden(this.name()); - } - - public SchemaElement copy() { - try { - return (SchemaElement) super.clone(); - } catch (CloneNotSupportedException e) { - throw new HugeException("Failed to clone schema", e); - } - } - - public boolean hasSameContent(SchemaElement other) { - return Objects.equal(this.name(), other.name()) && - Objects.equal(this.userdata(), other.userdata()); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof SchemaElement)) { - return false; - } - - SchemaElement other = (SchemaElement) obj; - return this.type() == other.type() && this.id.equals(other.id()); - } - - @Override - public int hashCode() { - return this.type().hashCode() ^ this.id.hashCode(); - } - - @Override - public String toString() { - return String.format("%s(id=%s)", this.name, this.id); - } - - public static int schemaId(Id id) { - long l = id.asLong(); - // Currently we limit the schema id to within 4 bytes - E.checkArgument(Integer.MIN_VALUE <= l && l <= Integer.MAX_VALUE, - "Schema id is out of bound: %s", l); - return (int) l; - } - - public static class TaskWithSchema { - - private SchemaElement schemaElement; - private Id task; - - public TaskWithSchema(SchemaElement schemaElement, Id task) { - E.checkNotNull(schemaElement, "schema element"); - this.schemaElement = schemaElement; - this.task = task; - } - - public void propertyKey(PropertyKey propertyKey) { - E.checkNotNull(propertyKey, "property key"); - this.schemaElement = propertyKey; - } - - public void indexLabel(IndexLabel indexLabel) { - E.checkNotNull(indexLabel, "index label"); - this.schemaElement = indexLabel; - } - - public PropertyKey propertyKey() { - E.checkState(this.schemaElement instanceof PropertyKey, - "Expect property key, but actual schema type is " + - "'%s'", this.schemaElement.getClass()); - return (PropertyKey) this.schemaElement; - } - - public IndexLabel indexLabel() { - E.checkState(this.schemaElement instanceof IndexLabel, - "Expect index label, but actual schema type is " + - "'%s'", this.schemaElement.getClass()); - return (IndexLabel) this.schemaElement; - } - - public SchemaElement schemaElement() { - return this.schemaElement; - } - - public Id task() { - return this.task; - } - } - - public abstract Map asMap(); - - public Map asMap(Map map) { - E.checkState(this.id != null, - "Property key id can't be null"); - E.checkState(this.name != null, - "Property key name can't be null"); - E.checkState(this.status != null, - "Property status can't be null"); - - map.put(P.ID, this.id); - map.put(P.NAME, this.name); - map.put(P.STATUS, this.status.string()); - map.put(P.USERDATA, this.userdata); - - return map; - } - - public static final class P { - - public static final String ID = "id"; - public static final String NAME = "name"; - - public static final String STATUS = "status"; - public static final String USERDATA = "userdata"; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaLabel.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaLabel.java deleted file mode 100644 index 09de43e2d8..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaLabel.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.schema; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.Indexable; -import org.apache.hugegraph.type.Propertiable; -import org.apache.hugegraph.util.E; - -import com.google.common.base.Objects; - -public abstract class SchemaLabel extends SchemaElement - implements Indexable, Propertiable { - - private final Set properties; - private final Set nullableKeys; - private final Set indexLabels; - private boolean enableLabelIndex; - private long ttl; - private Id ttlStartTime; - - public SchemaLabel(final HugeGraph graph, Id id, String name) { - super(graph, id, name); - this.properties = new HashSet<>(); - this.nullableKeys = new HashSet<>(); - this.indexLabels = new HashSet<>(); - this.enableLabelIndex = true; - this.ttl = 0L; - this.ttlStartTime = NONE_ID; - } - - @Override - public Set properties() { - return Collections.unmodifiableSet(this.properties); - } - - public void properties(Set properties) { - this.properties.addAll(properties); - } - - public SchemaLabel properties(Id... ids) { - this.properties.addAll(Arrays.asList(ids)); - return this; - } - - public void property(Id id) { - this.properties.add(id); - } - - public Set nullableKeys() { - return Collections.unmodifiableSet(this.nullableKeys); - } - - public void nullableKey(Id id) { - this.nullableKeys.add(id); - } - - public void nullableKeys(Id... ids) { - this.nullableKeys.addAll(Arrays.asList(ids)); - } - - public void nullableKeys(Set nullableKeys) { - this.nullableKeys.addAll(nullableKeys); - } - - @Override - public Set indexLabels() { - return Collections.unmodifiableSet(this.indexLabels); - } - - public void addIndexLabel(Id id) { - this.indexLabels.add(id); - } - - public void addIndexLabels(Id... ids) { - this.indexLabels.addAll(Arrays.asList(ids)); - } - - public boolean existsIndexLabel() { - return !this.indexLabels().isEmpty(); - } - - public void removeIndexLabel(Id id) { - this.indexLabels.remove(id); - } - - public boolean enableLabelIndex() { - return this.enableLabelIndex; - } - - public void enableLabelIndex(boolean enable) { - this.enableLabelIndex = enable; - } - - public boolean undefined() { - return this.name() == UNDEF; - } - - public void ttl(long ttl) { - assert ttl >= 0L; - this.ttl = ttl; - } - - public long ttl() { - assert this.ttl >= 0L; - return this.ttl; - } - - public void ttlStartTime(Id id) { - this.ttlStartTime = id; - } - - public Id ttlStartTime() { - return this.ttlStartTime; - } - - public String ttlStartTimeName() { - return NONE_ID.equals(this.ttlStartTime) ? null : - this.graph.propertyKey(this.ttlStartTime).name(); - } - - public boolean hasSameContent(SchemaLabel other) { - return super.hasSameContent(other) && this.ttl == other.ttl && - this.enableLabelIndex == other.enableLabelIndex && - Objects.equal(this.graph.mapPkId2Name(this.properties), - other.graph.mapPkId2Name(other.properties)) && - Objects.equal(this.graph.mapPkId2Name(this.nullableKeys), - other.graph.mapPkId2Name(other.nullableKeys)) && - Objects.equal(this.graph.mapIlId2Name(this.indexLabels), - other.graph.mapIlId2Name(other.indexLabels)) && - Objects.equal(this.ttlStartTimeName(), other.ttlStartTimeName()); - } - - public static Id getLabelId(HugeGraph graph, HugeType type, Object label) { - E.checkNotNull(graph, "graph"); - E.checkNotNull(type, "type"); - E.checkNotNull(label, "label"); - if (label instanceof Number) { - return IdGenerator.of(((Number) label).longValue()); - } else if (label instanceof String) { - if (type.isVertex()) { - return graph.vertexLabel((String) label).id(); - } else if (type.isEdge()) { - return graph.edgeLabel((String) label).id(); - } else { - throw new HugeException( - "Not support query from '%s' with label '%s'", - type, label); - } - } else { - throw new HugeException( - "The label type must be number or string, but got '%s'", - label.getClass()); - } - } - - public static Id getVertexLabelId(HugeGraph graph, Object label) { - return SchemaLabel.getLabelId(graph, HugeType.VERTEX, label); - } - - public static Id getEdgeLabelId(HugeGraph graph, Object label) { - return SchemaLabel.getLabelId(graph, HugeType.EDGE, label); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaManager.java index 7ea46b0d7b..0663f194d9 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/SchemaManager.java @@ -27,6 +27,11 @@ import org.apache.hugegraph.schema.builder.IndexLabelBuilder; import org.apache.hugegraph.schema.builder.PropertyKeyBuilder; import org.apache.hugegraph.schema.builder.VertexLabelBuilder; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.E; import org.apache.tinkerpop.gremlin.structure.Graph; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/Userdata.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/Userdata.java deleted file mode 100644 index 9d2b9dbd19..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/Userdata.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.schema; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.hugegraph.exception.NotAllowException; -import org.apache.hugegraph.type.define.Action; -import org.apache.hugegraph.util.DateUtil; - -public class Userdata extends HashMap { - - private static final long serialVersionUID = -1235451175617197049L; - - public static final String CREATE_TIME = "~create_time"; - public static final String DEFAULT_VALUE = "~default_value"; - - public Userdata() { - } - - public Userdata(Map map) { - this.putAll(map); - } - - /** - * Normalizes the value before storing so the {@link #CREATE_TIME}-is-Date - * invariant holds regardless of how entries are added. - */ - @Override - public Object put(String key, Object value) { - return super.put(key, normalizeValue(key, value)); - } - - @Override - public void putAll(Map map) { - for (Map.Entry e : map.entrySet()) { - this.put(e.getKey(), e.getValue()); - } - } - - /** - * Normalize internal userdata values whose runtime type can diverge from - * their serialized form. The only such key today is {@link #CREATE_TIME}: - * it is written as a {@link java.util.Date} but persisted as a formatted - * JSON string by the backend serializers, and Jackson cannot re-type a - * value to {@code Date} when the target is a raw {@code Map}. This method - * restores the original type after deserialization. Idempotent for values - * already of the expected type. - *

- * An empty string is passed through unchanged: it is the key-only - * placeholder used by the {@code eliminate()}/{@code DELETE} builder flow - * (e.g. {@code .userdata(CREATE_TIME, "").eliminate()}), where the value is - * ignored and only the key drives {@code removeUserdata}. Parsing it would - * fail before the eliminate path can apply its key-only semantics. - */ - public static Object normalizeValue(String key, Object value) { - if (CREATE_TIME.equals(key) && value instanceof String && - !((String) value).isEmpty()) { - try { - return DateUtil.parse((String) value); - } catch (RuntimeException e) { - throw new IllegalArgumentException(String.format( - "Invalid userdata '%s' value: '%s'", - CREATE_TIME, value), e); - } - } - return value; - } - - public static void check(Userdata userdata, Action action) { - if (userdata == null) { - return; - } - switch (action) { - case INSERT: - case APPEND: - for (Map.Entry e : userdata.entrySet()) { - if (e.getValue() == null) { - throw new NotAllowException( - "Not allowed to pass null userdata value " + - "when create or append schema"); - } - } - break; - case ELIMINATE: - case DELETE: - // pass - break; - default: - throw new AssertionError(String.format( - "Unknown schema action '%s'", action)); - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/VertexLabel.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/VertexLabel.java deleted file mode 100644 index 3f62df8631..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/VertexLabel.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.schema; - -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.Set; -import java.util.stream.Collectors; - -import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.builder.SchemaBuilder; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.define.IdStrategy; -import org.apache.hugegraph.type.define.SchemaStatus; - -import com.google.common.base.Objects; - -public class VertexLabel extends SchemaLabel { - - public static final VertexLabel NONE = new VertexLabel(null, NONE_ID, UNDEF); - - // OLAP_VL_ID means all of vertex label ids - private static final Id OLAP_VL_ID = IdGenerator.of(SchemaLabel.OLAP_VL_ID); - // OLAP_VL_NAME means all of vertex label names - private static final String OLAP_VL_NAME = "*olap"; - // OLAP_VL means all of vertex labels - public static final VertexLabel OLAP_VL = new VertexLabel(null, OLAP_VL_ID, - OLAP_VL_NAME); - - private IdStrategy idStrategy; - private List primaryKeys; - - public VertexLabel(final HugeGraph graph, Id id, String name) { - super(graph, id, name); - this.idStrategy = IdStrategy.DEFAULT; - this.primaryKeys = new ArrayList<>(); - } - - @Override - public HugeType type() { - return HugeType.VERTEX_LABEL; - } - - public boolean olap() { - return VertexLabel.OLAP_VL.id().equals(this.id()); - } - - public IdStrategy idStrategy() { - return this.idStrategy; - } - - public void idStrategy(IdStrategy idStrategy) { - this.idStrategy = idStrategy; - } - - public List primaryKeys() { - return Collections.unmodifiableList(this.primaryKeys); - } - - public void primaryKey(Id id) { - this.primaryKeys.add(id); - } - - public void primaryKeys(Id... ids) { - this.primaryKeys.addAll(Arrays.asList(ids)); - } - - public boolean existsLinkLabel() { - return this.graph().existsLinkLabel(this.id()); - } - - public boolean hasSameContent(VertexLabel other) { - return super.hasSameContent(other) && - this.idStrategy == other.idStrategy && - Objects.equal(this.graph.mapPkId2Name(this.primaryKeys), - other.graph.mapPkId2Name(other.primaryKeys)); - } - - public static VertexLabel undefined(HugeGraph graph) { - return new VertexLabel(graph, NONE_ID, UNDEF); - } - - public static VertexLabel undefined(HugeGraph graph, Id id) { - return new VertexLabel(graph, id, UNDEF); - } - - public interface Builder extends SchemaBuilder { - - Id rebuildIndex(); - - Builder idStrategy(IdStrategy idStrategy); - - Builder useAutomaticId(); - - Builder usePrimaryKeyId(); - - Builder useCustomizeStringId(); - - Builder useCustomizeNumberId(); - - Builder useCustomizeUuidId(); - - Builder properties(String... properties); - - Builder primaryKeys(String... keys); - - Builder nullableKeys(String... keys); - - Builder ttl(long ttl); - - Builder ttlStartTime(String ttlStartTime); - - Builder enableLabelIndex(boolean enable); - - Builder userdata(String key, Object value); - - Builder userdata(Map userdata); - } - - @Override - public Map asMap() { - HashMap map = new HashMap(); - - map.put(P.PROPERTIES, this.properties()); - - map.put(P.NULLABLE_KEYS, this.nullableKeys()); - - map.put(P.INDEX_LABELS, this.indexLabels()); - - map.put(P.ENABLE_LABEL_INDEX, this.enableLabelIndex()); - - map.put(P.TTL, String.valueOf(this.ttl())); - - map.put(P.TT_START_TIME, this.ttlStartTime().asString()); - - map.put(P.ID_STRATEGY, this.idStrategy().string()); - - map.put(P.PRIMARY_KEYS, this.primaryKeys()); - - return super.asMap(map); - } - - @SuppressWarnings("unchecked") - public static VertexLabel fromMap(Map map, HugeGraph graph) { - Id id = IdGenerator.of((int) map.get(VertexLabel.P.ID)); - String name = (String) map.get(VertexLabel.P.NAME); - - VertexLabel vertexLabel = new VertexLabel(graph, id, name); - for (Map.Entry entry : map.entrySet()) { - switch (entry.getKey()) { - case P.ID: - case P.NAME: - break; - case P.STATUS: - vertexLabel.status( - SchemaStatus.valueOf(((String) entry.getValue()).toUpperCase())); - break; - case P.USERDATA: - vertexLabel.userdata(new Userdata((Map) entry.getValue())); - break; - case P.PROPERTIES: - Set ids = ((List) entry.getValue()).stream().map( - IdGenerator::of).collect(Collectors.toSet()); - vertexLabel.properties(ids); - break; - case P.NULLABLE_KEYS: - ids = ((List) entry.getValue()).stream().map( - IdGenerator::of).collect(Collectors.toSet()); - vertexLabel.nullableKeys(ids); - break; - case P.INDEX_LABELS: - ids = ((List) entry.getValue()).stream().map( - IdGenerator::of).collect(Collectors.toSet()); - vertexLabel.addIndexLabels(ids.toArray(new Id[0])); - break; - case P.ENABLE_LABEL_INDEX: - boolean enableLabelIndex = (Boolean) entry.getValue(); - vertexLabel.enableLabelIndex(enableLabelIndex); - break; - case P.TTL: - long ttl = Long.parseLong((String) entry.getValue()); - vertexLabel.ttl(ttl); - break; - case P.TT_START_TIME: - long ttlStartTime = - Long.parseLong((String) entry.getValue()); - vertexLabel.ttlStartTime(IdGenerator.of(ttlStartTime)); - break; - case P.ID_STRATEGY: - IdStrategy idStrategy = - IdStrategy.valueOf(((String) entry.getValue()).toUpperCase()); - vertexLabel.idStrategy(idStrategy); - break; - case P.PRIMARY_KEYS: - ids = ((List) entry.getValue()).stream().map( - IdGenerator::of).collect(Collectors.toSet()); - vertexLabel.primaryKeys(ids.toArray(new Id[0])); - break; - default: - throw new AssertionError(String.format( - "Invalid key '%s' for vertex label", - entry.getKey())); - } - } - return vertexLabel; - } - - public static final class P { - - public static final String ID = "id"; - public static final String NAME = "name"; - - public static final String STATUS = "status"; - public static final String USERDATA = "userdata"; - - public static final String PROPERTIES = "properties"; - public static final String NULLABLE_KEYS = "nullableKeys"; - public static final String INDEX_LABELS = "indexLabels"; - - public static final String ENABLE_LABEL_INDEX = "enableLabelIndex"; - public static final String TTL = "ttl"; - public static final String TT_START_TIME = "ttlStartTime"; - public static final String ID_STRATEGY = "idStrategy"; - public static final String PRIMARY_KEYS = "primaryKeys"; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/AbstractBuilder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/AbstractBuilder.java index 0e0e95256c..2baaf8130b 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/AbstractBuilder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/AbstractBuilder.java @@ -21,15 +21,15 @@ import java.util.function.Function; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; import org.apache.hugegraph.backend.tx.ISchemaTransaction; import org.apache.hugegraph.exception.ExistedException; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.GraphMode; import org.apache.hugegraph.type.define.SchemaStatus; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/EdgeLabelBuilder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/EdgeLabelBuilder.java index 32937a2cf0..e46d4c2f2a 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/EdgeLabelBuilder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/EdgeLabelBuilder.java @@ -28,16 +28,17 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.HugeGraphSupplier; import org.apache.hugegraph.backend.tx.ISchemaTransaction; import org.apache.hugegraph.exception.ExistedException; import org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.exception.NotFoundException; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.Userdata; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Action; import org.apache.hugegraph.type.define.EdgeLabelType; @@ -91,7 +92,7 @@ public EdgeLabelBuilder(ISchemaTransaction transaction, HugeGraph graph, EdgeLabel copy) { super(transaction, graph); E.checkNotNull(copy, "copy"); - HugeGraph origin = copy.graph(); + HugeGraphSupplier origin = copy.graph(); this.id = null; this.name = copy.name(); this.links = mapPairId2Name(origin, copy.links()); @@ -206,7 +207,6 @@ public EdgeLabel create() { this.checkNullableKeys(Action.INSERT); Userdata.check(this.userdata, Action.INSERT); this.checkTTL(); - this.checkUserdata(Action.INSERT); edgeLabel = this.build(); assert edgeLabel.name().equals(name); @@ -731,42 +731,26 @@ private void checkTTL() { "but got '%s(%s)'", this.ttlStartTime, pkey.dataType()); } - private void checkUserdata(Action action) { - switch (action) { - case INSERT: - case APPEND: - for (Map.Entry e : this.userdata.entrySet()) { - if (e.getValue() == null) { - throw new NotAllowException( - "Not allowed pass null userdata value when " + - "create or append edge label"); - } - } - break; - case ELIMINATE: - case DELETE: - // pass - break; - default: - throw new AssertionError(String.format( - "Unknown schema action '%s'", action)); - } - } - private static Set mapPkId2Name(HugeGraph graph, Set ids) { + private static Set mapPkId2Name(HugeGraphSupplier graph, Set ids) { return new HashSet<>(graph.mapPkId2Name(ids)); } - private static List mapPkId2Name(HugeGraph graph, List ids) { + private static List mapPkId2Name(HugeGraphSupplier graph, List ids) { return graph.mapPkId2Name(ids); } - private static String mapElId2Name(HugeGraph graph, Id fatherId) { - return graph.mapElId2Name(ImmutableList.of(fatherId)).get(0); + private static String mapElId2Name(HugeGraphSupplier graph, Id fatherId) { + return graph.edgeLabel(fatherId).name(); } - private static Set> mapPairId2Name(HugeGraph graph, + private static Set> mapPairId2Name(HugeGraphSupplier graph, Set> pairs) { - return graph.mapPairId2Name(pairs); + Set> names = new HashSet<>(); + for (Pair pair : pairs) { + names.add(Pair.of(graph.vertexLabel(pair.getLeft()).name(), + graph.vertexLabel(pair.getRight()).name())); + } + return names; } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/IndexLabelBuilder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/IndexLabelBuilder.java index 397df66229..abb4ca8fbd 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/IndexLabelBuilder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/IndexLabelBuilder.java @@ -25,21 +25,21 @@ import java.util.concurrent.TimeoutException; import java.util.function.BiPredicate; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; import org.apache.hugegraph.backend.tx.ISchemaTransaction; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.exception.ExistedException; import org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.exception.NotFoundException; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.SchemaLabel; -import org.apache.hugegraph.schema.Userdata; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Action; import org.apache.hugegraph.type.define.CollectionType; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/PropertyKeyBuilder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/PropertyKeyBuilder.java index a50b426d1d..d971fd5aeb 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/PropertyKeyBuilder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/PropertyKeyBuilder.java @@ -20,19 +20,19 @@ import java.util.Map; import java.util.concurrent.TimeoutException; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; import org.apache.hugegraph.backend.tx.ISchemaTransaction; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.exception.ExistedException; import org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.exception.NotSupportException; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.Userdata; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.Userdata; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Action; import org.apache.hugegraph.type.define.AggregateType; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/SchemaBuilder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/SchemaBuilder.java deleted file mode 100644 index 28ef0c15f5..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/SchemaBuilder.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.schema.builder; - -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.schema.SchemaElement; - -public interface SchemaBuilder { - - SchemaBuilder id(long id); - - T build(); - - T create(); - - T append(); - - T eliminate(); - - Id remove(); - - SchemaBuilder ifNotExist(); - - SchemaBuilder checkExist(boolean checkExist); -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/VertexLabelBuilder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/VertexLabelBuilder.java index 4962646209..95b14b2697 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/VertexLabelBuilder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/schema/builder/VertexLabelBuilder.java @@ -27,15 +27,16 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.HugeGraphSupplier; import org.apache.hugegraph.backend.tx.ISchemaTransaction; import org.apache.hugegraph.exception.ExistedException; import org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.exception.NotFoundException; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.Userdata; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Action; import org.apache.hugegraph.type.define.IdStrategy; @@ -79,7 +80,7 @@ public VertexLabelBuilder(ISchemaTransaction transaction, HugeGraph graph, VertexLabel copy) { super(transaction, graph); E.checkNotNull(copy, "copy"); - HugeGraph origin = copy.graph(); + HugeGraphSupplier origin = copy.graph(); this.id = null; this.name = copy.name(); this.idStrategy = copy.idStrategy(); @@ -138,7 +139,6 @@ public VertexLabel create() { this.checkNullableKeys(Action.INSERT); Userdata.check(this.userdata, Action.INSERT); this.checkTTL(); - this.checkUserdata(Action.INSERT); vertexLabel = this.build(); assert vertexLabel.name().equals(name); @@ -586,32 +586,12 @@ private void checkTTL() { "but got '%s(%s)'", this.ttlStartTime, pkey.dataType()); } - private void checkUserdata(Action action) { - switch (action) { - case INSERT: - case APPEND: - for (Map.Entry e : this.userdata.entrySet()) { - if (e.getValue() == null) { - throw new NotAllowException( - "Not allowed pass null userdata value when " + - "create or append edge label"); - } - } - break; - case ELIMINATE: - case DELETE: - // pass - break; - default: - throw new AssertionError(String.format("Unknown schema action '%s'", action)); - } - } - private static Set mapPkId2Name(HugeGraph graph, Set ids) { + private static Set mapPkId2Name(HugeGraphSupplier graph, Set ids) { return new HashSet<>(graph.mapPkId2Name(ids)); } - private static List mapPkId2Name(HugeGraph graph, List ids) { + private static List mapPkId2Name(HugeGraphSupplier graph, List ids) { return graph.mapPkId2Name(ids); } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/security/HugeSecurityManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/security/HugeSecurityManager.java index 204320d400..f435ea7b7f 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/security/HugeSecurityManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/security/HugeSecurityManager.java @@ -66,11 +66,16 @@ public class HugeSecurityManager extends SecurityManager { "java.specification.version" ); + private static final Set ASYNC_SCHEMA_METHODS = ImmutableSet.of( + "removeVertexLabel", "removeEdgeLabel", "removeIndexLabel", "rebuildIndex" + ); + private static final Map> ASYNC_TASKS = ImmutableMap.of( // Fixed https://github.com/apache/hugegraph/pull/892#issue-387202362 "org.apache.hugegraph.backend.tx.SchemaTransaction", - ImmutableSet.of("removeVertexLabel", "removeEdgeLabel", - "removeIndexLabel", "rebuildIndex"), + ASYNC_SCHEMA_METHODS, + "org.apache.hugegraph.backend.tx.SchemaTransactionV2", + ASYNC_SCHEMA_METHODS, "org.apache.hugegraph.backend.tx.GraphIndexTransaction", ImmutableSet.of("asyncRemoveIndexLeft") ); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/space/GraphSpace.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/space/GraphSpace.java index f821809b63..d78415c691 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/space/GraphSpace.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/space/GraphSpace.java @@ -24,7 +24,7 @@ import java.util.Map; import org.apache.commons.lang.StringUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.util.E; public class GraphSpace { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/GraphType.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/GraphType.java deleted file mode 100644 index 7cf5ee3b4d..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/GraphType.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.structure; - -import org.apache.hugegraph.type.Nameable; -import org.apache.hugegraph.type.Typeable; - -public interface GraphType extends Nameable, Typeable { - -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeEdge.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeEdge.java index 5bf7011a6f..9ba85a9baa 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeEdge.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeEdge.java @@ -22,24 +22,21 @@ import java.util.List; import org.apache.commons.lang3.tuple.Pair; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.query.QueryResults; -import org.apache.hugegraph.backend.serializer.BytesBuffer; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.util.E; -import org.apache.logging.log4j.util.Strings; import org.apache.tinkerpop.gremlin.structure.Direction; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Property; @@ -47,17 +44,14 @@ import org.apache.tinkerpop.gremlin.structure.util.StringFactory; import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyProperty; -import com.google.common.collect.ImmutableList; public class HugeEdge extends HugeElement implements Edge, Cloneable { - private Id id; - private final EdgeLabel label; - private String name; + private BaseEdge element; private HugeVertex sourceVertex; private HugeVertex targetVertex; - private boolean isOutEdge; + public HugeEdge(HugeVertex owner, Id id, EdgeLabel label, HugeVertex other) { @@ -70,52 +64,69 @@ public HugeEdge(final HugeGraph graph, Id id, EdgeLabel label) { super(graph); E.checkArgumentNotNull(label, "Edge label can't be null"); - this.label = label; - - this.id = id; - this.name = null; + this.element = new BaseEdge(id, label); + this.element.isOutEdge(true); this.sourceVertex = null; this.targetVertex = null; - this.isOutEdge = true; + } + + /** Adopt decoded shared state and attach engine endpoint wrappers. */ + public HugeEdge(final HugeGraph graph, BaseEdge element) { + this(graph, element, null); + } + + public HugeEdge(final HugeGraph graph, BaseEdge element, HugeVertex owner) { + super(graph); + E.checkArgumentNotNull(element, "Edge element can't be null"); + E.checkArgumentNotNull(element.schemaLabel(), "Edge label can't be null"); + this.element = element; + BaseVertex source = element.sourceVertex(); + BaseVertex target = element.targetVertex(); + this.sourceVertex = this.wrapVertex(graph, source, owner); + this.targetVertex = target == source ? this.sourceVertex : this.wrapVertex(graph, target, owner); + } + + private HugeVertex wrapVertex(HugeGraph graph, BaseVertex vertex, HugeVertex owner) { + if (vertex == null) { + return null; + } + return owner != null && owner.element() == vertex ? owner : new HugeVertex(graph, vertex); + } + + @Override + public BaseEdge element() { + return this.element; } @Override public HugeType type() { // NOTE: we optimize the edge type that let it include direction - return this.isOutEdge ? HugeType.EDGE_OUT : HugeType.EDGE_IN; + return this.element.isOutEdge() ? HugeType.EDGE_OUT : HugeType.EDGE_IN; } @Override public EdgeId id() { - return (EdgeId) this.id; + return (EdgeId) this.element.id(); } @Override public EdgeLabel schemaLabel() { - assert this.graph().sameAs(this.label.graph()); - return this.label; + assert this.graph().sameAs(this.element.schemaLabel().graph()); + return this.element.schemaLabel(); } @Override public String name() { - if (this.name == null) { - List sortValues = this.sortValues(); - if (sortValues.isEmpty()) { - this.name = Strings.EMPTY; - } else { - this.name = ConditionQuery.concatValues(sortValues); - } - } - return this.name; + return this.element.name(); } public void name(String name) { - this.name = name; + this.element.name(name); } @Override public String label() { - return this.label.name(); + return this.element.schemaLabel().name(); } public boolean selfLoop() { @@ -124,7 +135,7 @@ public boolean selfLoop() { } public Directions direction() { - return this.isOutEdge ? Directions.OUT : Directions.IN; + return this.element.isOutEdge() ? Directions.OUT : Directions.IN; } public boolean matchDirection(Directions direction) { @@ -135,60 +146,25 @@ public boolean matchDirection(Directions direction) { } public boolean isDirection(Directions direction) { - return this.isOutEdge && direction == Directions.OUT || - !this.isOutEdge && direction == Directions.IN; + return this.element.isOutEdge() && direction == Directions.OUT || + !this.element.isOutEdge() && direction == Directions.IN; } @Watched(prefix = "edge") public void assignId() { - // Generate an id and assign - if (this.schemaLabel().hasFather()) { - this.id = new EdgeId(this.ownerVertex(), this.direction(), - this.schemaLabel().fatherId(), - this.schemaLabel().id(), - this.name(), - this.otherVertex()); - } else { - this.id = new EdgeId(this.ownerVertex(), this.direction(), - this.schemaLabel().id(), - this.schemaLabel().id(), - this.name(), this.otherVertex()); - } - - if (this.fresh()) { - int len = this.id.length(); - E.checkArgument(len <= BytesBuffer.EID_LEN_MAX, - "The max length of edge id is %s, but got %s {%s}", - BytesBuffer.EID_LEN_MAX, len, this.id); - } + this.element.assignId(); } @Watched(prefix = "edge") public EdgeId idWithDirection() { - return ((EdgeId) this.id).directed(true); + return this.element.idWithDirection(); } @Watched(prefix = "edge") protected List sortValues() { - List sortKeys = this.schemaLabel().sortKeys(); - if (sortKeys.isEmpty()) { - return ImmutableList.of(); - } - List propValues = new ArrayList<>(sortKeys.size()); - for (Id sk : sortKeys) { - HugeProperty property = this.getProperty(sk); - E.checkState(property != null, - "The value of sort key '%s' can't be null", sk); - Object propValue = property.serialValue(true); - if (Strings.EMPTY.equals(propValue)) { - propValue = ConditionQuery.INDEX_VALUE_EMPTY; - } - propValues.add(propValue); - } - return propValues; + return this.element.sortValues(); } - @Watched(prefix = "edge") @Override public void remove() { this.removed(true); @@ -208,7 +184,7 @@ public void remove() { public Property property(String key, V value) { PropertyKey propertyKey = this.graph().propertyKey(key); // Check key in edge label - E.checkArgument(this.label.properties().contains(propertyKey.id()), + E.checkArgument(this.element.schemaLabel().properties().contains(propertyKey.id()), "Invalid property '%s' for edge label '%s'", key, this.label()); if (value == null) { @@ -238,6 +214,11 @@ protected HugeEdgeProperty newProperty(PropertyKey pkey, V val) { return new HugeEdgeProperty<>(this, pkey, val); } + @Override + protected HugeEdgeProperty wrapProperty(BaseProperty property) { + return new HugeEdgeProperty<>(this, property); + } + @Watched(prefix = "edge") @Override protected void onUpdateProperty(Cardinality cardinality, @@ -275,7 +256,7 @@ protected boolean ensureFilledProperties(boolean throwIfNotExist) { if (edge == null && !throwIfNotExist) { return false; } - E.checkState(edge != null, "Edge '%s' does not exist", this.id); + E.checkState(edge != null, "Edge '%s' does not exist", this.element.id()); this.copyProperties((HugeEdge) edge); this.updateToDefaultValueIfNone(); return true; @@ -319,31 +300,7 @@ public Iterator> properties(String... keys) { @Override public Object sysprop(HugeKeys key) { - switch (key) { - case ID: - return this.id(); - case OWNER_VERTEX: - return this.ownerVertex().id(); - case LABEL: - if (this.schemaLabel().hasFather()) { - return this.schemaLabel().fatherId(); - } else { - return this.schemaLabel().id(); - } - case DIRECTION: - return this.direction(); - case SUB_LABEL: - return this.schemaLabel().id(); - case OTHER_VERTEX: - return this.otherVertex().id(); - case SORT_VALUES: - return this.name(); - case PROPERTIES: - return this.getPropertiesMap(); - default: - E.checkArgument(false, "Invalid system property '%s' of Edge", key); - return null; - } + return this.element.sysprop(key); } @Override @@ -380,7 +337,7 @@ public Vertex inVertex() { public void vertices(HugeVertex owner, HugeVertex other) { Id ownerLabel = owner.schemaLabel().id(); Id otherLabel = other.schemaLabel().id(); - for (Pair link : this.label.links()) { + for (Pair link : this.element.schemaLabel().links()) { if (ownerLabel.equals(link.getLeft()) && otherLabel.equals(link.getRight())) { this.vertices(true, owner, other); @@ -392,8 +349,8 @@ public void vertices(HugeVertex owner, HugeVertex other) { } public void vertices(boolean outEdge, HugeVertex owner, HugeVertex other) { - this.isOutEdge = outEdge; - if (this.isOutEdge) { + this.element.vertices(outEdge, owner.element(), other.element()); + if (this.element.isOutEdge()) { this.sourceVertex = owner; this.targetVertex = other; } else { @@ -404,10 +361,7 @@ public void vertices(boolean outEdge, HugeVertex owner, HugeVertex other) { @Watched public HugeEdge switchOwner() { - HugeEdge edge = this.clone(); - edge.isOutEdge = !edge.isOutEdge; - edge.id = ((EdgeId) edge.id).switchDirection(); - return edge; + return this.cloneWithElement(this.element.switchOwner()); } public HugeEdge switchToOutDirection() { @@ -418,7 +372,7 @@ public HugeEdge switchToOutDirection() { } public HugeVertex ownerVertex() { - return this.isOutEdge ? this.sourceVertex() : this.targetVertex(); + return this.element.isOutEdge() ? this.sourceVertex() : this.targetVertex(); } public HugeVertex sourceVertex() { @@ -428,6 +382,7 @@ public HugeVertex sourceVertex() { public void sourceVertex(HugeVertex sourceVertex) { this.sourceVertex = sourceVertex; + this.element.sourceVertex(sourceVertex.element()); } public HugeVertex targetVertex() { @@ -437,6 +392,7 @@ public HugeVertex targetVertex() { public void targetVertex(HugeVertex targetVertex) { this.targetVertex = targetVertex; + this.element.targetVertex(targetVertex.element()); } private void checkAdjacentVertexExist(HugeVertex vertex) { @@ -477,7 +433,7 @@ public HugeVertex otherVertex(HugeVertex vertex) { } public HugeVertex otherVertex() { - return this.isOutEdge ? this.targetVertex() : this.sourceVertex(); + return this.element.isOutEdge() ? this.targetVertex() : this.sourceVertex(); } /** @@ -501,8 +457,14 @@ public HugeEdge copy() { @Override protected HugeEdge clone() { + return this.cloneWithElement(this.element.clone()); + } + + private HugeEdge cloneWithElement(BaseEdge element) { try { - return (HugeEdge) super.clone(); + HugeEdge edge = (HugeEdge) super.clone(); + edge.element = element; + return edge; } catch (CloneNotSupportedException e) { throw new HugeException("Failed to clone HugeEdge", e); } @@ -528,36 +490,21 @@ public static HugeEdge constructEdge(HugeVertex ownerVertex, EdgeLabel edgeLabel, String sortValues, Id otherVertexId) { - HugeGraph graph = ownerVertex.graph(); - VertexLabel srcLabel = graph.vertexLabelOrNone(edgeLabel.sourceLabel()); - VertexLabel tgtLabel = graph.vertexLabelOrNone(edgeLabel.targetLabel()); - - VertexLabel otherVertexLabel; - if (isOutEdge) { - ownerVertex.correctVertexLabel(srcLabel); - otherVertexLabel = tgtLabel; - } else { - ownerVertex.correctVertexLabel(tgtLabel); - otherVertexLabel = srcLabel; - } - HugeVertex otherVertex = new HugeVertex(graph, otherVertexId, otherVertexLabel); - - ownerVertex.propNotLoaded(); - otherVertex.propNotLoaded(); - - HugeEdge edge = new HugeEdge(graph, null, edgeLabel); - edge.name(sortValues); - edge.vertices(isOutEdge, ownerVertex, otherVertex); - edge.assignId(); + BaseEdge element = BaseEdge.createEdge(ownerVertex.graph(), ownerVertex.element(), isOutEdge, + edgeLabel, sortValues, otherVertexId); + return attachEdge(ownerVertex, element); + } - if (isOutEdge) { + private static HugeEdge attachEdge(HugeVertex ownerVertex, BaseEdge element) { + HugeEdge edge = new HugeEdge(ownerVertex.graph(), element, ownerVertex); + HugeVertex otherVertex = edge.otherVertex(); + if (element.isOutEdge()) { ownerVertex.addOutEdge(edge); otherVertex.addInEdge(edge.switchOwner()); } else { ownerVertex.addInEdge(edge); otherVertex.addOutEdge(edge.switchOwner()); } - return edge; } @@ -566,25 +513,11 @@ public static HugeEdge constructEdgeWithoutLabel(HugeVertex ownerVertex, String sortValues, Id otherVertexId) { HugeGraph graph = ownerVertex.graph(); - HugeVertex otherVertex = new HugeVertex(graph, otherVertexId, - VertexLabel.NONE); - ownerVertex.propNotLoaded(); - otherVertex.propNotLoaded(); - - HugeEdge edge = new HugeEdge(graph, null, EdgeLabel.NONE); - edge.name(sortValues); - edge.vertices(isOutEdge, ownerVertex, otherVertex); - edge.assignId(); - - if (isOutEdge) { - ownerVertex.addOutEdge(edge); - otherVertex.addInEdge(edge.switchOwner()); - } else { - ownerVertex.addInEdge(edge); - otherVertex.addOutEdge(edge.switchOwner()); - } - - return edge; + EdgeLabel label = EdgeLabel.undefined(graph, EdgeLabel.NONE.id()); + VertexLabel otherLabel = VertexLabel.undefined(graph); + BaseEdge element = BaseEdge.createEdge(ownerVertex.element(), isOutEdge, label, + sortValues, otherVertexId, otherLabel); + return attachEdge(ownerVertex, element); } public static HugeEdge constructEdgeWithoutGraph(HugeVertex ownerVertex, @@ -592,37 +525,8 @@ public static HugeEdge constructEdgeWithoutGraph(HugeVertex ownerVertex, EdgeLabel edgeLabel, String sortValues, Id otherVertexId) { - Id ownerLabelId = edgeLabel.sourceLabel(); - Id otherLabelId = edgeLabel.targetLabel(); - VertexLabel srcLabel = new VertexLabel(null, ownerLabelId, "UNDEF"); - VertexLabel tgtLabel = new VertexLabel(null, otherLabelId, "UNDEF"); - - VertexLabel otherVertexLabel; - if (isOutEdge) { - ownerVertex.correctVertexLabel(srcLabel); - otherVertexLabel = tgtLabel; - } else { - ownerVertex.correctVertexLabel(tgtLabel); - otherVertexLabel = srcLabel; - } - HugeVertex otherVertex = new HugeVertex(null, otherVertexId, - otherVertexLabel); - ownerVertex.propNotLoaded(); - otherVertex.propNotLoaded(); - - HugeEdge edge = new HugeEdge(null, null, edgeLabel); - edge.name(sortValues); - edge.vertices(isOutEdge, ownerVertex, otherVertex); - edge.assignId(); - - if (isOutEdge) { - ownerVertex.addOutEdge(edge); - otherVertex.addInEdge(edge.switchOwner()); - } else { - ownerVertex.addInEdge(edge); - otherVertex.addOutEdge(edge.switchOwner()); - } - - return edge; + BaseEdge element = BaseEdge.createEdgeWithoutSchema(ownerVertex.graph(), ownerVertex.element(), + isOutEdge, edgeLabel, sortValues, otherVertexId); + return attachEdge(ownerVertex, element); } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeEdgeProperty.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeEdgeProperty.java index f89e4f0cf4..ab3d87587b 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeEdgeProperty.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeEdgeProperty.java @@ -17,8 +17,8 @@ package org.apache.hugegraph.structure; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.E; import org.apache.tinkerpop.gremlin.structure.Property; @@ -30,6 +30,10 @@ public HugeEdgeProperty(HugeElement owner, PropertyKey key, V value) { super(owner, key, value); } + public HugeEdgeProperty(HugeElement owner, BaseProperty property) { + super(owner, property); + } + @Override public HugeType type() { return this.pkey.aggregateType().isNone() ? @@ -68,6 +72,6 @@ public int hashCode() { public HugeEdgeProperty switchEdgeOwner() { assert this.owner instanceof HugeEdge; return new HugeEdgeProperty<>(((HugeEdge) this.owner).switchOwner(), - this.pkey, this.value); + this.baseProperty()); } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeElement.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeElement.java index adb12f31bc..b3f46c46d3 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeElement.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeElement.java @@ -17,66 +17,55 @@ package org.apache.hugegraph.structure; -import java.util.ArrayList; +import java.util.AbstractCollection; import java.util.Collection; -import java.util.Date; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.function.Supplier; +import java.util.function.Consumer; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaLabel; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.type.GraphType; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.Idfiable; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.HugeKeys; -import org.apache.hugegraph.util.CollectionUtil; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.InsertionOrderUtil; -import org.apache.hugegraph.util.collection.CollectionFactory; import org.apache.tinkerpop.gremlin.structure.Element; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.util.ElementHelper; import org.eclipse.collections.api.iterator.IntIterator; -import org.eclipse.collections.api.map.primitive.MutableIntObjectMap; public abstract class HugeElement implements Element, GraphType, Idfiable, Comparable { - private static final MutableIntObjectMap> EMPTY_MAP = - CollectionFactory.newIntObjectMap(); - private final HugeGraph graph; - private MutableIntObjectMap> properties; - // TODO: move into properties to keep small object - private long expiredTime; - - private boolean removed; - private boolean fresh; - private boolean propLoaded; - private boolean defaultValueUpdated; public HugeElement(final HugeGraph graph) { E.checkArgument(graph != null, "HugeElement graph can't be null"); this.graph = graph; - this.properties = EMPTY_MAP; - this.expiredTime = 0L; - this.removed = false; - this.fresh = false; - this.propLoaded = true; - this.defaultValueUpdated = false; } + /** + * Shared state for internal adapters and codecs. As with setProperty(), mutations + * through this view bypass transaction callbacks; application callers must use + * the graph/TinkerPop mutation APIs to update persisted elements and indexes. + */ + public abstract BaseElement element(); + + protected abstract HugeProperty wrapProperty(BaseProperty property); + public abstract SchemaLabel schemaLabel(); protected abstract GraphTransaction tx(); @@ -89,22 +78,7 @@ protected abstract void onUpdateProperty(Cardinality cardinality, protected abstract boolean ensureFilledProperties(boolean throwIfNotExist); protected void updateToDefaultValueIfNone() { - if (this.fresh() || this.defaultValueUpdated) { - return; - } - this.defaultValueUpdated = true; - // Set default value if needed - for (Id pkeyId : this.schemaLabel().properties()) { - if (this.properties.containsKey(intFromId(pkeyId))) { - continue; - } - PropertyKey pkey = this.graph().propertyKey(pkeyId); - Object value = pkey.defaultValue(); - if (value != null) { - this.setProperty(this.newProperty(pkey, value)); - } - } - this.defaultValueUpdated = true; + this.element().updateDefaultValues(this.graph()::propertyKey); } @Override @@ -113,31 +87,31 @@ public HugeGraph graph() { } protected void removed(boolean removed) { - this.removed = removed; + this.element().removed(removed); } public boolean removed() { - return this.removed; + return this.element().removed(); } protected void fresh(boolean fresh) { - this.fresh = fresh; + this.element().fresh(fresh); } public boolean fresh() { - return this.fresh; + return this.element().fresh(); } public boolean isPropLoaded() { - return this.propLoaded; + return this.element().propLoaded(); } protected void propLoaded() { - this.propLoaded = true; + this.element().propLoaded(true); } public void propNotLoaded() { - this.propLoaded = false; + this.element().propLoaded(false); } public void forceLoad() { @@ -145,59 +119,38 @@ public void forceLoad() { } public void committed() { - this.fresh = false; + this.element().fresh(false); // Set expired time this.setExpiredTimeIfNeeded(); } public void setExpiredTimeIfNeeded() { - SchemaLabel label = this.schemaLabel(); - if (label.ttl() == 0L) { - return; - } - long now = this.graph.now(); - if (SchemaLabel.NONE_ID.equals(label.ttlStartTime())) { - this.expiredTime(now + label.ttl()); - return; - } - Date date = this.getPropertyValue(label.ttlStartTime()); - if (date == null) { - this.expiredTime(now + label.ttl()); - return; - } - long expired = date.getTime() + label.ttl(); - E.checkArgument(expired > now, - "The expired time '%s' of '%s' is prior to now: %s", - new Date(expired), this, now); - this.expiredTime(expired); + this.element().setExpiredTimeIfNeeded(this.graph.now()); } public long expiredTime() { - return this.expiredTime; + return this.element().expiredTime(); } public void expiredTime(long expiredTime) { - this.expiredTime = expiredTime; + this.element().expiredTime(expiredTime); } public boolean expired() { - return 0L < this.expiredTime && this.expiredTime < this.graph.now(); + return 0L < this.expiredTime() && this.expiredTime() < this.graph.now(); } public long ttl() { - if (this.expiredTime == 0L || this.expiredTime < this.graph.now()) { - return 0L; - } - return this.expiredTime - this.graph.now(); + return this.element().ttl(this.graph.now()); } public boolean hasTtl() { - return this.schemaLabel().ttl() > 0L; + return this.element().hasTtl(); } public Set getPropertyKeys() { Set propKeys = InsertionOrderUtil.newSet(); - IntIterator keys = this.properties.keysView().intIterator(); + IntIterator keys = this.element().properties().keysView().intIterator(); while (keys.hasNext()) { propKeys.add(IdGenerator.of(keys.next())); } @@ -205,7 +158,39 @@ public Set getPropertyKeys() { } public Collection> getProperties() { - return this.properties.values(); + Collection> properties = this.element().properties().values(); + return new AbstractCollection>() { + @Override + public Iterator> iterator() { + Iterator> iterator = properties.iterator(); + return new Iterator>() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public HugeProperty next() { + return HugeElement.this.wrapProperty(iterator.next()); + } + + @Override + public void remove() { + iterator.remove(); + } + }; + } + + @Override + public int size() { + return properties.size(); + } + + @Override + public void clear() { + properties.clear(); + } + }; } public Collection> getFilledProperties() { @@ -214,17 +199,12 @@ public Collection> getFilledProperties() { } public Map getPropertiesMap() { - Map props = InsertionOrderUtil.newMap(); - for (HugeProperty prop : this.properties.values()) { - props.put(prop.propertyKey().id(), prop.value()); - } - // TODO: return MutableIntObjectMap for this method? - return props; + return this.element().getPropertiesMap(); } public Collection> getAggregateProperties() { List> aggrProps = InsertionOrderUtil.newList(); - for (HugeProperty prop : this.properties.values()) { + for (HugeProperty prop : this.getProperties()) { if (prop.type().isAggregateProperty()) { aggrProps.add(prop); } @@ -232,59 +212,40 @@ public Collection> getAggregateProperties() { return aggrProps; } - @SuppressWarnings("unchecked") public HugeProperty getProperty(Id key) { - return (HugeProperty) this.properties.get(intFromId(key)); + BaseProperty property = this.element().getProperty(key); + return property == null ? null : this.wrapProperty(property); } - @SuppressWarnings("unchecked") public V getPropertyValue(Id key) { - HugeProperty prop = this.properties.get(intFromId(key)); - if (prop == null) { - return null; - } - return (V) prop.value(); + return this.element().getPropertyValue(key); } public boolean hasProperty(Id key) { - return this.properties.containsKey(intFromId(key)); + return this.element().hasProperty(key); } public boolean hasProperties() { - return !this.properties.isEmpty(); + return this.element().hasProperties(); } public int sizeOfProperties() { - return this.properties.size(); + return this.element().sizeOfProperties(); } public int sizeOfSubProperties() { - int size = 0; - for (HugeProperty p : this.properties.values()) { - size++; - if (p.propertyKey().cardinality() != Cardinality.SINGLE && - p.value() instanceof Collection) { - size += ((Collection) p.value()).size(); - } - } - return size; + return this.element().sizeOfSubProperties(); } @Watched(prefix = "element") public HugeProperty setProperty(HugeProperty prop) { - if (this.properties == EMPTY_MAP) { - this.properties = CollectionFactory.newIntObjectMap(); - } - PropertyKey pkey = prop.propertyKey(); - - E.checkArgument(this.properties.containsKey(intFromId(pkey.id())) || - this.properties.size() < BytesBuffer.MAX_PROPERTIES, - "Exceeded the maximum number of properties"); - return this.properties.put(intFromId(pkey.id()), prop); + BaseProperty previous = this.element().addProperty(prop.baseProperty()); + return previous == null ? null : this.wrapProperty(previous); } public HugeProperty removeProperty(Id key) { - return this.properties.remove(intFromId(key)); + BaseProperty previous = this.element().removeProperty(key); + return previous == null ? null : this.wrapProperty(previous); } public HugeProperty addProperty(PropertyKey pkey, V value) { @@ -292,92 +253,30 @@ public HugeProperty addProperty(PropertyKey pkey, V value) { } @Watched(prefix = "element") - public HugeProperty addProperty(PropertyKey pkey, V value, - boolean notify) { - HugeProperty prop = null; - switch (pkey.cardinality()) { - case SINGLE: - prop = this.newProperty(pkey, value); - if (notify) { - /* - * NOTE: this method should be called before setProperty() - * because tx need to delete index without the new property - */ - this.onUpdateProperty(pkey.cardinality(), prop); - } - this.setProperty(prop); - break; - case SET: - prop = this.addProperty(pkey, value, HashSet::new); - if (notify) { - this.onUpdateProperty(pkey.cardinality(), prop); - } - break; - case LIST: - prop = this.addProperty(pkey, value, ArrayList::new); - if (notify) { - this.onUpdateProperty(pkey.cardinality(), prop); - } - break; - default: - assert false; - break; - } - return prop; - } - - @Watched(prefix = "element") - @SuppressWarnings({"rawtypes", "unchecked"}) - private HugeProperty addProperty(PropertyKey pkey, V value, - Supplier> supplier) { - assert pkey.cardinality().multiple(); - HugeProperty> property; - if (this.hasProperty(pkey.id())) { - property = this.getProperty(pkey.id()); - } else { - property = this.newProperty(pkey, supplier.get()); - this.setProperty(property); - } - - Collection values; - if (pkey.cardinality() == Cardinality.SET) { - if (value instanceof Set) { - values = (Set) value; - } else { - values = CollectionUtil.toSet(value); - } - } else { - assert pkey.cardinality() == Cardinality.LIST; - if (value instanceof List) { - values = (List) value; - } else { - values = CollectionUtil.toList(value); + public HugeProperty addProperty(PropertyKey pkey, V value, boolean notify) { + Consumer> callback = property -> { + if (notify) { + this.onUpdateProperty(pkey.cardinality(), this.wrapProperty(property)); } - } - property.value().addAll(pkey.validValueOrThrow(values)); - - // Any better ways? - return (HugeProperty) property; + }; + BaseProperty property = this.element().addProperty( + pkey, value, (key, val) -> this.newProperty(key, val).baseProperty(), + callback, callback); + return this.wrapProperty(property); } public void resetProperties() { - this.properties = CollectionFactory.newIntObjectMap(); - this.propLoaded = false; + this.element().resetProperties(); + this.element().propLoaded(false); } protected void copyProperties(HugeElement element) { - if (element.properties == EMPTY_MAP) { - this.properties = EMPTY_MAP; - } else { - this.properties = CollectionFactory.newIntObjectMap( - element.properties); - } - this.propLoaded = true; + this.element().copyProperties(element.element()); } public HugeElement copyAsFresh() { HugeElement elem = this.copy(); - elem.fresh = true; + elem.fresh(true); return elem; } @@ -512,9 +411,7 @@ public static final Object getLabelValue(Object... keyValues) { } public static int intFromId(Id id) { - E.checkArgument(id instanceof IdGenerator.LongId, - "Can't get number from %s(%s)", id, id.getClass()); - return ((IdGenerator.LongId) id).intValue(); + return BaseElement.intFromId(id); } public static final class ElementKeys { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeFeatures.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeFeatures.java index f8bdb8c75c..52b0345ebe 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeFeatures.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeFeatures.java @@ -20,7 +20,7 @@ import java.util.UUID; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.config.CoreOptions; import org.apache.tinkerpop.gremlin.structure.Graph; import org.apache.tinkerpop.gremlin.structure.VertexProperty; @@ -186,7 +186,7 @@ public boolean supportsSerializableValues() { @Override public boolean supportsUniformListValues() { - return true; + return false; } @Override @@ -225,7 +225,7 @@ public boolean supportsSerializableValues() { @Override public boolean supportsUniformListValues() { - return true; + return false; } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeIndex.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeIndex.java deleted file mode 100644 index 6e76913935..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeIndex.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.structure; - -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.Id.IdType; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; -import org.apache.hugegraph.backend.serializer.BytesBuffer; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.define.DataType; -import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.HashUtil; -import org.apache.hugegraph.util.InsertionOrderUtil; -import org.apache.hugegraph.util.NumericUtil; - -public class HugeIndex implements GraphType, Cloneable { - - private final HugeGraph graph; - private Object fieldValues; - private IndexLabel indexLabel; - private Set elementIds; - private static final int HUGE_TYPE_CODE_LENGTH = 1; - - public HugeIndex(HugeGraph graph, IndexLabel indexLabel) { - E.checkNotNull(graph, "graph"); - E.checkNotNull(indexLabel, "label"); - E.checkNotNull(indexLabel.id(), "label id"); - this.graph = graph; - this.indexLabel = indexLabel; - this.elementIds = new LinkedHashSet<>(); - this.fieldValues = null; - } - - @Override - public String name() { - return this.indexLabel.name(); - } - - @Override - public HugeType type() { - if (this.indexLabel == IndexLabel.label(HugeType.VERTEX)) { - return HugeType.VERTEX_LABEL_INDEX; - } else if (this.indexLabel == IndexLabel.label(HugeType.EDGE)) { - return HugeType.EDGE_LABEL_INDEX; - } - return this.indexLabel.indexType().type(); - } - - public HugeGraph graph() { - return this.graph; - } - - public Id id() { - return formatIndexId(type(), this.indexLabelId(), this.fieldValues()); - } - - public Id hashId() { - return formatIndexHashId(type(), this.indexLabelId(), this.fieldValues()); - } - - public Object fieldValues() { - return this.fieldValues; - } - - public void fieldValues(Object fieldValues) { - this.fieldValues = fieldValues; - } - - public Id indexLabelId() { - return this.indexLabel.id(); - } - - public IndexLabel indexLabel() { - return this.indexLabel; - } - - public IdWithExpiredTime elementIdWithExpiredTime() { - E.checkState(this.elementIds.size() == 1, - "Expect one element id, actual %s", - this.elementIds.size()); - return this.elementIds.iterator().next(); - } - - public Id elementId() { - return this.elementIdWithExpiredTime().id(); - } - - public Set elementIds() { - Set ids = InsertionOrderUtil.newSet(this.elementIds.size()); - for (IdWithExpiredTime idWithExpiredTime : this.elementIds) { - ids.add(idWithExpiredTime.id()); - } - return Collections.unmodifiableSet(ids); - } - - public Set expiredElementIds() { - long now = this.graph.now(); - Set expired = InsertionOrderUtil.newSet(); - for (IdWithExpiredTime id : this.elementIds) { - if (0L < id.expiredTime && id.expiredTime < now) { - expired.add(id); - } - } - this.elementIds.removeAll(expired); - return expired; - } - - public void elementIds(Id elementId) { - this.elementIds(elementId, 0L); - } - - public void elementIds(Id elementId, long expiredTime) { - this.elementIds.add(new IdWithExpiredTime(elementId, expiredTime)); - } - - public void resetElementIds() { - this.elementIds = new LinkedHashSet<>(); - } - - public long expiredTime() { - return this.elementIdWithExpiredTime().expiredTime(); - } - - public boolean hasTtl() { - if (this.indexLabel.system()) { - return false; - } - return this.indexLabel.baseLabel().ttl() > 0L; - } - - public long ttl() { - return this.expiredTime() - this.graph.now(); - } - - @Override - public HugeIndex clone() { - try { - return (HugeIndex) super.clone(); - } catch (CloneNotSupportedException e) { - throw new HugeException("Failed to clone HugeIndex", e); - } - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof HugeIndex)) { - return false; - } - - HugeIndex other = (HugeIndex) obj; - return this.id().equals(other.id()); - } - - @Override - public int hashCode() { - return this.id().hashCode(); - } - - @Override - public String toString() { - return String.format("{label=%s<%s>, fieldValues=%s, elementIds=%s}", - this.indexLabel.name(), - this.indexLabel.indexType().string(), - this.fieldValues, this.elementIds); - } - - public static Id formatIndexHashId(HugeType type, Id indexLabel, - Object fieldValues) { - E.checkState(!type.isRangeIndex(), - "RangeIndex can't return a hash id"); - String value = fieldValues == null ? "" : fieldValues.toString(); - return formatIndexId(type, indexLabel, HashUtil.hash(value)); - } - - public static Id formatIndexId(HugeType type, Id indexLabelId, - Object fieldValues) { - if (type.isStringIndex()) { - String value = ""; - if (fieldValues instanceof Id) { - value = IdGenerator.asStoredString((Id) fieldValues); - } else if (fieldValues != null) { - value = fieldValues.toString(); - } - /* - * Modify order between index label and field-values to put the - * index label in front(hugegraph-1317) - */ - String strIndexLabelId = IdGenerator.asStoredString(indexLabelId); - return SplicingIdGenerator.splicing(type.string(), strIndexLabelId, value); - } else { - assert type.isRangeIndex(); - int length = type.isRange4Index() ? 4 : 8; - BytesBuffer buffer = BytesBuffer.allocate(HUGE_TYPE_CODE_LENGTH + 4 + length); - buffer.write(type.code()); - buffer.writeInt(SchemaElement.schemaId(indexLabelId)); - if (fieldValues != null) { - E.checkState(fieldValues instanceof Number, - "Field value of range index must be number:" + - " %s", fieldValues.getClass().getSimpleName()); - byte[] bytes = number2bytes((Number) fieldValues); - buffer.write(bytes); - } - return buffer.asId(); - } - } - - public static HugeIndex parseIndexId(HugeGraph graph, HugeType type, - byte[] id) { - Object values; - IndexLabel indexLabel; - if (type.isStringIndex()) { - Id idObject = IdGenerator.of(id, IdType.STRING); - String[] parts = SplicingIdGenerator.parse(idObject); - E.checkState(parts.length == 3, "Invalid secondary index id"); - Id label = IdGenerator.ofStoredString(parts[1], IdType.LONG); - indexLabel = IndexLabel.label(graph, label); - values = parts[2]; - } else { - assert type.isRange4Index() || type.isRange8Index(); - final int labelLength = 4; - E.checkState(id.length > labelLength, "Invalid range index id"); - BytesBuffer buffer = BytesBuffer.wrap(id); - buffer.read(HUGE_TYPE_CODE_LENGTH); - Id label = IdGenerator.of(buffer.readInt()); - indexLabel = IndexLabel.label(graph, label); - List fields = indexLabel.indexFields(); - E.checkState(fields.size() == 1, "Invalid range index fields"); - DataType dataType = graph.propertyKey(fields.get(0)).dataType(); - E.checkState(dataType.isNumber() || dataType.isDate(), - "Invalid range index field type"); - Class clazz = dataType.isNumber() ? - dataType.clazz() : DataType.LONG.clazz(); - values = bytes2number(buffer.read(id.length - labelLength - HUGE_TYPE_CODE_LENGTH), - clazz); - } - HugeIndex index = new HugeIndex(graph, indexLabel); - index.fieldValues(values); - return index; - } - - public static byte[] number2bytes(Number number) { - if (number instanceof Byte) { - // Handle byte as integer to store as 4 bytes in RANGE4_INDEX - number = number.intValue(); - } - return NumericUtil.numberToSortableBytes(number); - } - - public static Number bytes2number(byte[] bytes, Class clazz) { - return NumericUtil.sortableBytesToNumber(bytes, clazz); - } - - public static class IdWithExpiredTime { - - private Id id; - private long expiredTime; - - public IdWithExpiredTime(Id id, long expiredTime) { - this.id = id; - this.expiredTime = expiredTime; - } - - public Id id() { - return this.id; - } - - public long expiredTime() { - return this.expiredTime; - } - - @Override - public String toString() { - return String.format("%s(%s)", this.id, this.expiredTime); - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeProperty.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeProperty.java index 65472e148b..9cc41db401 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeProperty.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeProperty.java @@ -19,8 +19,9 @@ import java.util.NoSuchElementException; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.type.GraphType; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.E; import org.apache.tinkerpop.gremlin.structure.Property; @@ -31,7 +32,7 @@ public abstract class HugeProperty implements Property, GraphType { protected final HugeElement owner; protected final PropertyKey pkey; - protected V value; + protected BaseProperty property; public HugeProperty(HugeElement owner, PropertyKey pkey, V value) { E.checkArgument(owner != null, "Property owner can't be null"); @@ -40,7 +41,19 @@ public HugeProperty(HugeElement owner, PropertyKey pkey, V value) { this.owner = owner; this.pkey = pkey; - this.value = pkey.validValueOrThrow(value); + this.property = new BaseProperty<>(pkey, pkey.validValueOrThrow(value)); + } + + protected HugeProperty(HugeElement owner, BaseProperty property) { + E.checkArgument(owner != null, "Property owner can't be null"); + E.checkArgument(property != null, "Property can't be null"); + this.owner = owner; + this.pkey = property.propertyKey(); + this.property = property; + } + + public BaseProperty baseProperty() { + return this.property; } public PropertyKey propertyKey() { @@ -68,16 +81,16 @@ public String key() { @Override public V value() throws NoSuchElementException { - return this.value; + return this.property.value(); } public Object serialValue(boolean encodeNumber) { - return this.pkey.serialValue(this.value, encodeNumber); + return this.property.serialValue(encodeNumber); } @Override public boolean isPresent() { - return null != this.value; + return null != this.value(); } public boolean isAggregateType() { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeVertex.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeVertex.java index 389b0f0e8c..e33cd1d77c 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeVertex.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeVertex.java @@ -17,45 +17,40 @@ package org.apache.hugegraph.structure; +import java.util.AbstractCollection; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; -import java.util.Set; +import java.util.Map; import org.apache.commons.collections.CollectionUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.id.SnowflakeIdGenerator; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryResults; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.config.CoreOptions; -import org.apache.hugegraph.masterelection.StandardClusterRoleStore; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; -import org.apache.hugegraph.task.HugeServerInfo; -import org.apache.hugegraph.task.HugeTask; -import org.apache.hugegraph.task.HugeTaskResult; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; -import org.apache.hugegraph.type.define.CollectionType; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.type.define.IdStrategy; import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.collection.CollectionFactory; -import org.apache.logging.log4j.util.Strings; +import org.apache.hugegraph.util.TinkerPopUtil; import org.apache.tinkerpop.gremlin.structure.Direction; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.T; @@ -65,25 +60,20 @@ import org.apache.tinkerpop.gremlin.structure.util.StringFactory; import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyVertexProperty; -import com.google.common.collect.ImmutableList; public class HugeVertex extends HugeElement implements Vertex, Cloneable { - private static final List EMPTY_LIST = ImmutableList.of(); - - private Id id; - private VertexLabel label; - protected Collection edges; + private BaseVertex element; + private Map edgeWrappers; public HugeVertex(final HugeGraph graph, Id id, VertexLabel label) { super(graph); E.checkArgumentNotNull(label, "Vertex label can't be null"); - this.label = label; - - this.id = id; - this.edges = EMPTY_LIST; - if (this.id != null) { + this.element = new BaseVertex(id, label, BaseVertex.TypeContext.ENGINE); + this.element.encodeNumber(this.graph().option(CoreOptions.VERTEX_ENCODE_PK_NUMBER)); + this.edgeWrappers = new IdentityHashMap<>(); + if (this.element.id() != null) { if (label.idStrategy() == IdStrategy.CUSTOMIZE_UUID) { this.assignId(id); } else { @@ -92,56 +82,45 @@ public HugeVertex(final HugeGraph graph, Id id, VertexLabel label) { } } + /** Adopt decoded shared state without copying its property values. */ + public HugeVertex(final HugeGraph graph, BaseVertex element) { + super(graph); + E.checkArgumentNotNull(element, "Vertex element can't be null"); + E.checkArgumentNotNull(element.schemaLabel(), "Vertex label can't be null"); + this.element = element; + this.element.typeContext(BaseVertex.TypeContext.ENGINE); + this.element.encodeNumber(this.graph().option(CoreOptions.VERTEX_ENCODE_PK_NUMBER)); + this.edgeWrappers = new IdentityHashMap<>(); + if (element.id() != null) { + this.checkIdLength(); + } + } + @Override public HugeType type() { - if (label != null && - (label.name().equals(HugeTask.P.TASK) || - label.name().equals(HugeTaskResult.P.TASKRESULT))) { - return HugeType.TASK; - } - if (label != null && - (label.name().equals(HugeServerInfo.P.SERVER) || - label.name().equals(StandardClusterRoleStore.P.ROLE_DATA))) { - return HugeType.SERVER; - } - return HugeType.VERTEX; + return this.element.type(); } @Override public Id id() { - return this.id; + return this.element.id(); } @Override public VertexLabel schemaLabel() { - assert VertexLabel.OLAP_VL.equals(this.label) || - this.graph().sameAs(this.label.graph()); - return this.label; + assert VertexLabel.OLAP_VL.equals(this.element.schemaLabel()) || + this.graph().sameAs(this.element.schemaLabel().graph()); + return this.element.schemaLabel(); } @Override public String name() { - E.checkState(this.label.idStrategy() == IdStrategy.PRIMARY_KEY, - "Only primary key vertex has name, " + - "but got '%s' with id strategy '%s'", - this, this.label.idStrategy()); - String name; - if (this.id != null) { - String[] parts = SplicingIdGenerator.parse(this.id); - E.checkState(parts.length == 2, - "Invalid primary key vertex id '%s'", this.id); - name = parts[1]; - } else { - assert this.id == null; - List propValues = this.primaryValues(); - E.checkState(!propValues.isEmpty(), - "Primary values must not be empty " + - "(has properties %s)", hasProperties()); - name = ConditionQuery.concatValues(propValues); - E.checkArgument(!name.isEmpty(), - "The value of primary key can't be empty"); - } - return name; + return this.element.name(); + } + + @Override + public BaseVertex element() { + return this.element; } public void assignId(Id id) { @@ -150,31 +129,31 @@ public void assignId(Id id) { @Watched(prefix = "vertex") public void assignId(Id id, boolean force) { - IdStrategy strategy = this.label.idStrategy(); + IdStrategy strategy = this.element.schemaLabel().idStrategy(); // Generate an id and assign switch (strategy) { case CUSTOMIZE_STRING: assert !id.number(); - this.id = id; + this.element.id(id); break; case CUSTOMIZE_NUMBER: assert id.number(); - this.id = id; + this.element.id(id); break; case CUSTOMIZE_UUID: - this.id = id.uuid() ? id : IdGenerator.of(id.asString(), true); + this.element.id(id.uuid() ? id : IdGenerator.of(id.asString(), true)); break; case PRIMARY_KEY: - this.id = SplicingIdGenerator.instance().generate(this); + this.element.id(SplicingIdGenerator.instance().generate(this.element())); break; case AUTOMATIC: if (force) { // Resume id for AUTOMATIC id strategy in restoring mode assert id.number(); - this.id = id; + this.element.id(id); } else { - this.id = SnowflakeIdGenerator.instance(this.graph()) - .generate(this); + this.element.id(SnowflakeIdGenerator.instance(this.graph()) + .generate(this.element())); } break; default: @@ -184,14 +163,14 @@ public void assignId(Id id, boolean force) { } protected void checkIdLength() { - assert this.id != null; - int len = this.id.asBytes().length; + assert this.element.id() != null; + int len = this.element.id().asBytes().length; if (len <= BytesBuffer.ID_LEN_MAX) { return; } E.checkArgument(false, "The max length of vertex id is %s, but got %s {%s}", - BytesBuffer.ID_LEN_MAX, len, this.id); + BytesBuffer.ID_LEN_MAX, len, this.element.id()); } @Override @@ -200,67 +179,67 @@ public String label() { } public void correctVertexLabel(VertexLabel correctLabel) { - E.checkArgumentNotNull(correctLabel, "Vertex label can't be null"); - if (this.label != null && !this.label.undefined() && - !correctLabel.undefined()) { - E.checkArgument(this.label.equals(correctLabel), - "Vertex label can't be changed from '%s' to '%s'", - this.label, correctLabel); - } - this.label = correctLabel; + this.element.correctVertexLabel(correctLabel); } @Watched(prefix = "vertex") protected List primaryValues() { - E.checkArgument(this.label.idStrategy() == IdStrategy.PRIMARY_KEY, - "The id strategy '%s' don't have primary keys", - this.label.idStrategy()); - List primaryKeys = this.label.primaryKeys(); - E.checkArgument(!primaryKeys.isEmpty(), - "Primary key can't be empty for id strategy '%s'", - IdStrategy.PRIMARY_KEY); - - boolean encodeNumber = this.graph() - .option(CoreOptions.VERTEX_ENCODE_PK_NUMBER); - List propValues = new ArrayList<>(primaryKeys.size()); - for (Id pk : primaryKeys) { - HugeProperty property = this.getProperty(pk); - E.checkState(property != null, - "The value of primary key '%s' can't be null", - this.graph().propertyKey(pk).name()); - Object propValue = property.serialValue(encodeNumber); - if (Strings.EMPTY.equals(propValue)) { - propValue = ConditionQuery.INDEX_VALUE_EMPTY; - } - propValues.add(propValue); - } - return propValues; + return this.element.primaryValues(); } public boolean existsEdges() { - return !this.edges.isEmpty(); + return !this.element.edges().isEmpty(); } public Collection getEdges() { - return Collections.unmodifiableCollection(this.edges); + Collection edges = this.element.edges(); + return Collections.unmodifiableCollection(new AbstractCollection() { + @Override + public Iterator iterator() { + Iterator iterator = edges.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public HugeEdge next() { + BaseEdge edge = iterator.next(); + return HugeVertex.this.edgeWrappers.computeIfAbsent( + edge, value -> new HugeEdge(HugeVertex.this.graph(), value, + HugeVertex.this)); + } + }; + } + + @Override + public int size() { + return edges.size(); + } + }); } public void resetEdges() { - /* - * Use List to hold edges to reduce memory usage and operation time. - */ - this.edges = newList(); + this.resetEdges(false); + } + + protected void resetEdges(boolean unique) { + this.element.resetEdges(unique); + this.edgeWrappers.clear(); } public void removeEdge(HugeEdge edge) { - this.edges.remove(edge); + BaseEdge removed = this.element.removeEdge(edge.element()); + if (removed != null) { + this.edgeWrappers.remove(removed); + } } public void addEdge(HugeEdge edge) { - if (this.edges == EMPTY_LIST) { - this.edges = newList(); + if (this.element.addEdge(edge.element())) { + this.edgeWrappers.put(edge.element(), edge); } - this.edges.add(edge); } /** @@ -375,7 +354,7 @@ public void addInEdge(HugeEdge edge) { public Iterator getEdges(Directions direction, String... edgeLabels) { List list = new LinkedList<>(); - for (HugeEdge edge : this.edges) { + for (HugeEdge edge : this.getEdges()) { if (edge.matchDirection(direction) && edge.belongToLabels(edgeLabels)) { list.add(edge); @@ -398,7 +377,7 @@ public Iterator getVertices(Directions direction, @Watched(prefix = "vertex") @Override public Iterator edges(Direction tinkerpopDir, String... edgeLabels) { - Directions direction = Directions.convert(tinkerpopDir); + Directions direction = TinkerPopUtil.direction(tinkerpopDir); Id[] edgeLabelIds = this.graph().mapElName2Id(edgeLabels); Query query = GraphTransaction.constructEdgesQuery(this.id(), direction, edgeLabelIds); @@ -457,17 +436,17 @@ public VertexProperty property(VertexProperty.Cardinality cardinality, */ if (cardinality != VertexProperty.Cardinality.single) { E.checkArgument(propertyKey.cardinality() == - Cardinality.convert(cardinality), + TinkerPopUtil.cardinality(cardinality), "Invalid cardinality '%s' for property key '%s', " + "expect '%s'", cardinality, key, propertyKey.cardinality().string()); } // Check key in vertex label - E.checkArgument(VertexLabel.OLAP_VL.equals(this.label) || - this.label.properties().contains(propertyKey.id()), + E.checkArgument(VertexLabel.OLAP_VL.equals(this.element.schemaLabel()) || + this.element.schemaLabel().properties().contains(propertyKey.id()), "Invalid property '%s' for vertex label '%s'", - key, this.label); + key, this.element.schemaLabel()); // Primary-Keys can only be set once if (this.schemaLabel().primaryKeys().contains(propertyKey.id())) { E.checkArgument(!this.hasProperty(propertyKey.id()), @@ -495,6 +474,11 @@ protected HugeVertexProperty newProperty(PropertyKey pkey, V val) { return new HugeVertexProperty<>(this, pkey, val); } + @Override + protected HugeVertexProperty wrapProperty(BaseProperty property) { + return new HugeVertexProperty<>(this, property); + } + @Watched(prefix = "vertex") @Override protected void onUpdateProperty(Cardinality cardinality, @@ -537,7 +521,7 @@ protected boolean ensureFilledProperties(boolean throwIfNotExist) { if (vertex == null && !throwIfNotExist) { return false; } - E.checkState(vertex != null, "Vertex '%s' does not exist", this.id); + E.checkState(vertex != null, "Vertex '%s' does not exist", this.element.id()); if (vertex.schemaLabel().undefined() || !vertex.schemaLabel().equals(this.schemaLabel())) { // Update vertex label of dangling edge to undefined @@ -590,20 +574,7 @@ public Iterator> properties(String... keys) { @Override public Object sysprop(HugeKeys key) { - switch (key) { - case ID: - return this.id(); - case LABEL: - return this.schemaLabel().id(); - case PRIMARY_VALUES: - return this.name(); - case PROPERTIES: - return this.getPropertiesMap(); - default: - E.checkArgument(false, - "Invalid system property '%s' of Vertex", key); - return null; - } + return this.element.sysprop(key); } public boolean valid() { @@ -616,14 +587,12 @@ public boolean valid() { } /** - * Clear edges/properties of the cloned vertex, and set `removed` true + * Clear edges/properties of the cloned vertex, and set `removed` true. * * @return a new vertex */ public HugeVertex prepareRemoved() { - // NOTE: clear edges/properties of the cloned vertex and return HugeVertex vertex = this.clone(); - // Remove self vertex.removed(true); vertex.resetEdges(); vertex.resetProperties(); @@ -631,7 +600,7 @@ public HugeVertex prepareRemoved() { } public boolean olap() { - return this.label.olap(); + return this.element.schemaLabel().olap(); } @Override @@ -644,7 +613,10 @@ public HugeVertex copy() { @Override protected HugeVertex clone() { try { - return (HugeVertex) super.clone(); + HugeVertex vertex = (HugeVertex) super.clone(); + vertex.element = (BaseVertex) this.element.clone(); + vertex.edgeWrappers = new IdentityHashMap<>(this.edgeWrappers); + return vertex; } catch (CloneNotSupportedException e) { throw new HugeException("Failed to clone HugeVertex", e); } @@ -669,13 +641,7 @@ public static HugeVertex create(final GraphTransaction tx, return new HugeVertex4Insert(tx, id, label); } - private static Set newSet() { - return CollectionFactory.newSet(CollectionType.EC); - } - private static List newList() { - return CollectionFactory.newList(CollectionType.EC); - } private static final class HugeVertex4Insert extends HugeVertex { @@ -688,22 +654,14 @@ public HugeVertex4Insert(final GraphTransaction tx, * Use Set to hold edges inserted into vertex * to avoid duplicated edges */ - this.edges = newSet(); + this.resetEdges(true); this.tx = tx; this.fresh(true); } @Override public void resetEdges() { - this.edges = newSet(); - } - - @Override - public void addEdge(HugeEdge edge) { - if (this.edges == EMPTY_LIST) { - this.edges = newSet(); - } - this.edges.add(edge); + this.resetEdges(true); } @Override diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeVertexProperty.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeVertexProperty.java index 4cef519799..8ef5cca262 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeVertexProperty.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/structure/HugeVertexProperty.java @@ -20,8 +20,8 @@ import java.util.Iterator; import org.apache.hugegraph.exception.NotSupportException; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.E; import org.apache.tinkerpop.gremlin.structure.Element; @@ -36,6 +36,10 @@ public HugeVertexProperty(HugeElement owner, PropertyKey key, V value) { super(owner, key, value); } + public HugeVertexProperty(HugeElement owner, BaseProperty property) { + super(owner, property); + } + @Override public HugeType type() { return this.pkey.aggregateType().isNone() ? diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/DistributedTaskScheduler.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/DistributedTaskScheduler.java index 57885a9d99..4e050c33ee 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/DistributedTaskScheduler.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/DistributedTaskScheduler.java @@ -25,17 +25,20 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.exception.ConnectionException; @@ -65,8 +68,12 @@ public class DistributedTaskScheduler extends TaskAndResultScheduler { * the status of scheduler */ private final AtomicBoolean closed = new AtomicBoolean(true); + private volatile boolean closeCompleted; + private final AtomicInteger cancellations = new AtomicInteger(); private final ConcurrentHashMap> runningTasks = new ConcurrentHashMap<>(); + // Local execution owners, including accepted work that has not acquired its task lock. + private final ConcurrentHashMap executions = new ConcurrentHashMap<>(); private final Set deletingTasks = ConcurrentHashMap.newKeySet(); public DistributedTaskScheduler(HugeGraphParams graph, @@ -75,9 +82,8 @@ public DistributedTaskScheduler(HugeGraphParams graph, ExecutorService schemaTaskExecutor, ExecutorService olapTaskExecutor, ExecutorService gremlinTaskExecutor, - ExecutorService ephemeralTaskExecutor, - ExecutorService serverInfoDbExecutor) { - super(graph, serverInfoDbExecutor); + ExecutorService ephemeralTaskExecutor) { + super(graph); this.taskDbExecutor = taskDbExecutor; this.schemaTaskExecutor = schemaTaskExecutor; @@ -193,7 +199,8 @@ public void cronSchedule() { initTaskParams(cancelling); LOG.info("Try to cancel task({})@({}/{})", cancelling.id(), this.graphSpace, this.graphName); - if (!cancelling.cancel(true)) { + if (!this.cancelOwned(this.executions.get(cancellingId), + () -> cancelling.cancel(true), true)) { // Task already completed normally; force CANCELLED so // it doesn't stay stuck in CANCELLING forever. updateStatusWithLock(cancellingId, TaskStatus.CANCELLING, @@ -216,8 +223,10 @@ public void cronSchedule() { Id deletingId = deletings.next().id(); HugeTask deleting = runningTasks.get(deletingId); if (deleting != null) { - this.markTaskDeleting(deleting); - deleting.cancel(true); + this.cancelOwned(this.executions.get(deletingId), () -> { + this.markTaskDeleting(deleting); + return deleting.cancel(true); + }, true); } else { // Local has no task execution, but the current task has no nodes executing anymore. if (!isLockedTask(deletingId.toString())) { @@ -241,7 +250,7 @@ public HugeGraph graph() { @Override public int pendingTasks() { - return this.runningTasks.size(); + return this.executions.size() + this.cancellations.get(); } @Override @@ -250,14 +259,21 @@ public void restoreTasks() { } @Override - public Future schedule(HugeTask task) { + public synchronized Future schedule(HugeTask task) { E.checkArgumentNotNull(task, "Task can't be null"); + E.checkState(!this.closed.get(), "TaskScheduler has closed"); + PendingTask pending = this.executions.get(task.id()); + if (pending != null && pending.task == task && task.status() == TaskStatus.NEW && + pending.started.get() && !pending.ownerFinished) { + // HugeTask checks dependencies before running and may reschedule itself. + return pending; + } initTaskParams(task); if (task.ephemeralTask()) { // Handle ephemeral tasks, no scheduling needed, execute directly - return this.ephemeralTaskExecutor.submit(task); + return this.submitTask(task, this.ephemeralTaskExecutor, task); } // Validate task state before saving to ensure correct exception type @@ -304,6 +320,62 @@ protected void initTaskParams(HugeTask task) { @Override public void cancel(HugeTask task) { E.checkArgumentNotNull(task, "Task can't be null"); + PendingTask pending; + synchronized (this) { + E.checkState(!this.closed.get(), "TaskScheduler has closed"); + pending = this.executions.get(task.id()); + if (pending != null) { + synchronized (pending) { + pending.callbacks++; + } + } + this.cancellations.incrementAndGet(); + } + String context = TaskManager.getContext(); + AtomicBoolean cleaned = new AtomicBoolean(true); + TaskManager.ContextCallable owned = new TaskManager.ContextCallable<>(() -> { + boolean callerTxOpen = this.graph.graph().tx().isOpen(); + try { + this.cancelTask(task, true); + return null; + } finally { + if (!callerTxOpen) { + try { + this.graph.closeTx(); + } catch (RuntimeException | Error error) { + cleaned.set(false); + throw error; + } + } + } + }); + try { + // Keep the original synchronous callback semantics, including nested cancel requests. + owned.call(); + } catch (RuntimeException | Error error) { + throw error; + } catch (Exception e) { + throw new HugeException("Failed to cancel task '%s'", e, task.id()); + } finally { + TaskManager.setContext(context); + if (cleaned.get()) { + this.releaseCancellation(pending); + } + } + } + + private void releaseCancellation(PendingTask pending) { + if (pending != null) { + synchronized (pending) { + pending.callbacks--; + pending.release(); + } + } + this.cancellations.decrementAndGet(); + } + + private void cancelTask(HugeTask task, boolean direct) { + E.checkArgumentNotNull(task, "Task can't be null"); if (task.completed() || task.cancelling()) { return; @@ -313,12 +385,19 @@ public void cancel(HugeTask task) { // Check if task is running locally, cancel it directly if so HugeTask runningTask = this.runningTasks.get(task.id()); - if (runningTask != null) { - boolean cancelled = runningTask.cancel(true); - if (cancelled) { - task.overwriteStatus(TaskStatus.CANCELLED); - this.save(runningTask); - } + PendingTask pending = this.executions.get(task.id()); + if (runningTask != null || task.ephemeralTask()) { + HugeTask local = runningTask != null ? runningTask : task; + Callable cancellation = () -> { + boolean accepted = local.cancel(true); + if (accepted && !local.ephemeralTask()) { + task.overwriteStatus(TaskStatus.CANCELLED); + this.save(local); + } + return accepted; + }; + boolean cancelled = pending == null ? local.cancel(true) : + this.cancelOwned(pending, cancellation, direct); LOG.info("Cancel local running task '{}' result: {}", task.id(), cancelled); return; } @@ -474,11 +553,17 @@ public HugeTask delete(Id id, boolean force) { HugeTask running = this.runningTasks.get(id); if (running != null) { - this.markTaskDeleting(running); - running.cancel(true); - @SuppressWarnings("unchecked") - HugeTask result = (HugeTask) running; - return result; + boolean cancelled = this.cancelOwned(this.executions.get(id), () -> { + this.markTaskDeleting(running); + return running.cancel(true); + }, false); + if (cancelled || this.executions.containsKey(id)) { + @SuppressWarnings("unchecked") + HugeTask result = (HugeTask) running; + return result; + } + // The local owner finished before cancellation; delete its now-completed database record. + task = this.taskWithoutResult(id); } if (!force && !task.completed()) { @@ -526,38 +611,57 @@ private static void checkDeleteLock(Id id, LockResult lockResult) { @Override public boolean close() { - if (this.closed.get()) { + // Dispatch stopped does not imply running jobs and owners have drained. + if (this.closeCompleted) { return true; } - // set closed - this.closed.set(true); + // Fence schedule() and cron admission before starting the drain. + synchronized (this) { + this.closed.set(true); + } // cancel cron thread if (!cronFuture.isDone() && !cronFuture.isCancelled()) { cronFuture.cancel(false); } - // Wait behind the scheduler thread to ensure any running cron task is completed - try { - Future barrier = this.schedulerExecutor.submit(() -> { - // pass - }); - barrier.get(schedulePeriod + 5, TimeUnit.SECONDS); - } catch (TimeoutException e) { - LOG.warn("Cron task did not complete in time when closing scheduler"); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - LOG.warn("Interrupted while waiting for cron task to complete", e); - } catch (ExecutionException e) { - LOG.warn("Exception while waiting for cron task to complete", e); + // A terminated executor has already completed all its cron work. + // Otherwise wait behind that work before closing its owners. + if (!this.schedulerExecutor.isTerminated()) { + try { + Future barrier = this.schedulerExecutor.submit(() -> { + // pass + }); + barrier.get(schedulePeriod + 5, TimeUnit.SECONDS); + } catch (TimeoutException e) { + LOG.warn("Cron task did not complete in time when closing scheduler"); + return false; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.warn("Interrupted while waiting for cron task to complete", e); + return false; + } catch (ExecutionException e) { + LOG.warn("Exception while waiting for cron task to complete", e); + return false; + } catch (RejectedExecutionException e) { + LOG.warn("Scheduler executor was stopped before its graphs drained", e); + return false; + } } // cancel all running tasks - for (HugeTask task : this.runningTasks.values()) { + for (PendingTask pending : this.executions.values()) { + // Durable work that never ran stays NEW for a later scheduler. + if (!pending.task.ephemeralTask() && + !this.runningTasks.containsKey(pending.task.id())) { + pending.cancelBeforeStart(); + // A wrapper already entering its lock/read phase cleans up without running the job. + continue; + } LOG.info("cancel task({}) @({}/{}) when closing scheduler", - task.id(), graphSpace, graphName); - this.cancel(task); + pending.task.id(), graphSpace, graphName); + this.cancelTask(pending.task, false); } try { @@ -567,19 +671,54 @@ public boolean close() { return false; } - if (!this.taskDbExecutor.isShutdown()) { - this.call(() -> { - try { - this.tx().close(); - } catch (ConnectionException ignored) { - // ConnectionException means no connection established + Throwable failure = null; + boolean closed = false; + try { + if (!this.taskDbExecutor.isShutdown()) { + this.call(() -> { + Throwable workerFailure = null; + for (Runnable close : new Runnable[]{() -> { + try { + this.tx().close(); + } catch (ConnectionException ignored) { + // ConnectionException means no connection established + } + }, this.graph::closeTx}) { + try { + close.run(); + } catch (RuntimeException | Error error) { + if (workerFailure == null) { + workerFailure = error; + } else if (workerFailure != error) { + workerFailure.addSuppressed(error); + } + } + } + if (workerFailure instanceof Error) { + throw (Error) workerFailure; + } + if (workerFailure != null) { + throw (RuntimeException) workerFailure; + } + }); + } + } catch (RuntimeException | Error error) { + failure = error; + throw error; + } finally { + try { + closed = this.serverManager().close(); + } catch (RuntimeException | Error error) { + if (failure == null) { + throw error; } - this.graph.closeTx(); - }); + if (failure != error) { + failure.addSuppressed(error); + } + } } - - // TODO: serverInfoManager section should be removed in the future. - return this.serverManager().close(); + this.closeCompleted = closed; + return closed; } @Override @@ -722,7 +861,10 @@ protected boolean updateStatusWithLock(Id id, TaskStatus prestatus, * @param task * @return true if the task have start */ - private boolean tryStartHugeTask(HugeTask task) { + private synchronized boolean tryStartHugeTask(HugeTask task) { + if (this.closed.get() || this.executions.containsKey(task.id())) { + return false; + } // Print Scheduler status logCurrentState(); @@ -750,7 +892,7 @@ private boolean tryStartHugeTask(HugeTask task) { ThreadPoolExecutor executor = (ThreadPoolExecutor) chosenExecutor; if (executor.getActiveCount() < executor.getMaximumPoolSize()) { TaskRunner runner = new TaskRunner<>(task); - chosenExecutor.submit(runner); + this.submitTask(task, chosenExecutor, runner); LOG.info("Submit task({})@({}/{})", task.id(), this.graphSpace, this.graphName); @@ -760,6 +902,179 @@ private boolean tryStartHugeTask(HugeTask task) { return false; } + private Future submitTask(HugeTask task, ExecutorService executor, Runnable work) { + TaskManager.ContextCallable owned = new TaskManager.ContextCallable<>(() -> { + try { + work.run(); + return null; + } finally { + this.closeOwner(this.executions.get(task.id())); + } + }); + PendingTask pending = new PendingTask(task, executor, owned); + E.checkState(this.executions.putIfAbsent(task.id(), pending) == null, + "Task '%s' is already pending", task.id()); + try { + executor.execute(pending); + return pending; + } catch (RuntimeException | Error error) { + pending.cancelBeforeStart(); + throw error; + } + } + + private boolean cancelOwned(PendingTask pending, Callable action, boolean direct) { + if (pending == null) { + return false; + } + synchronized (pending) { + if (this.executions.get(pending.task.id()) != pending) { + return false; + } + pending.callbacks++; + } + TaskManager.ContextCallable owned = new TaskManager.ContextCallable<>(() -> { + boolean callerTxOpen = this.graph.graph().tx().isOpen(); + try { + return action.call(); + } finally { + if (!callerTxOpen) { + this.closeOwner(pending); + } + } + }); + Callable cancellation = () -> { + try { + return owned.call(); + } finally { + synchronized (pending) { + pending.callbacks--; + if (pending.task.isDone()) { + pending.cancelBeforeStart(); + } + pending.release(); + } + } + }; + if (direct) { + try { + return cancellation.call(); + } catch (Exception e) { + throw new HugeException("Failed to cancel task '%s'", e, pending.task.id()); + } + } + Future future; + try { + future = this.schedulerExecutor.submit(cancellation); + } catch (RuntimeException | Error error) { + synchronized (pending) { + pending.callbacks--; + pending.release(); + } + throw error; + } + try { + return future.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HugeException("Interrupted when cancelling task '%s'", e, pending.task.id()); + } catch (ExecutionException e) { + throw new HugeException("Failed to cancel task '%s'", e, pending.task.id()); + } + } + + private void closeOwner(PendingTask pending) { + try { + this.graph.closeTx(); + } catch (RuntimeException | Error error) { + synchronized (pending) { + // A failed owner cleanup must never become a successful graph drain. + pending.cleanupFailed = true; + } + throw error; + } + } + + private class PendingTask extends FutureTask { + + private final HugeTask task; + private final ExecutorService executor; + private final AtomicBoolean started = new AtomicBoolean(); + private volatile boolean ownerFinished; + private int callbacks; + private boolean cleanupFailed; + + PendingTask(HugeTask task, ExecutorService executor, Callable work) { + super(work); + this.task = task; + this.executor = executor; + } + + @Override + public void run() { + if (!this.started.compareAndSet(false, true)) { + return; + } + try { + if (this.task.ephemeralTask()) { + if (super.runAndReset() && this.task.completed()) { + this.set(null); + } + } else { + super.run(); + } + } finally { + synchronized (this) { + this.ownerFinished = true; + if (this.task.ephemeralTask() && !this.task.completed() && + !this.isDone() && !closed.get() && this.callbacks == 0) { + this.started.set(false); + this.ownerFinished = false; + try { + this.executor.execute(this); + } catch (RuntimeException | Error error) { + this.ownerFinished = true; + this.setException(error); + } + } + // A dependency retry fenced by close still needs its cancellation callback. + if (!this.task.ephemeralTask() || this.task.completed() || !closed.get()) { + this.release(); + } + } + } + } + + @Override + protected void done() { + if (this.isCancelled()) { + this.cancelBeforeStart(); + } + } + + private synchronized boolean cancelBeforeStart() { + if (!this.started.compareAndSet(false, true)) { + return false; + } + this.cancel(false); + if (this.executor instanceof ThreadPoolExecutor) { + ((ThreadPoolExecutor) this.executor).remove(this); + } + this.ownerFinished = true; + this.release(); + return true; + } + + private void release() { + if (this.ownerFinished && this.callbacks == 0 && !this.cleanupFailed) { + executions.remove(this.task.id(), this); + if (this.task.completed() && !this.isDone()) { + this.set(null); + } + } + } + } + protected void logCurrentState() { int gremlinActive = ((ThreadPoolExecutor) gremlinTaskExecutor).getActiveCount(); @@ -849,7 +1164,12 @@ public void run() { return; } - runningTasks.put(task.id(), task); + synchronized (DistributedTaskScheduler.this) { + if (closed.get()) { + return; + } + runningTasks.put(task.id(), task); + } // Task execution will not throw exceptions, HugeTask will catch exceptions // during execution and store them in the DB. @@ -857,7 +1177,7 @@ public void run() { } catch (Throwable t) { LOG.warn("exception when execute task", t); } finally { - runningTasks.remove(task.id()); + runningTasks.remove(task.id(), task); deletingTasks.remove(task.id()); unlockTask(task.id().asString(), lockResult); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/EphemeralJobQueue.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/EphemeralJobQueue.java index c7c840a882..0b2debb553 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/EphemeralJobQueue.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/EphemeralJobQueue.java @@ -25,7 +25,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.job.EphemeralJob; import org.apache.hugegraph.job.EphemeralJobBuilder; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeServerInfo.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeServerInfo.java deleted file mode 100644 index f0485f6656..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeServerInfo.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.task; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.define.Cardinality; -import org.apache.hugegraph.type.define.DataType; -import org.apache.hugegraph.type.define.NodeRole; -import org.apache.hugegraph.type.define.SerialEnum; -import org.apache.hugegraph.util.DateUtil; -import org.apache.hugegraph.util.E; -import org.apache.tinkerpop.gremlin.structure.Graph; -import org.apache.tinkerpop.gremlin.structure.T; -import org.apache.tinkerpop.gremlin.structure.Vertex; -import org.apache.tinkerpop.gremlin.structure.VertexProperty; - -public class HugeServerInfo { - - // Unit millisecond - private static final long EXPIRED_INTERVAL = TaskManager.SCHEDULE_PERIOD * 10; - - private NodeRole role; - private Date updateTime; - private int maxLoad; - private int load; - private final Id id; - - private transient boolean updated = false; - - public HugeServerInfo(String name, NodeRole role) { - this(IdGenerator.of(name), role); - } - - public HugeServerInfo(Id id) { - this.id = id; - this.role = NodeRole.WORKER; - this.maxLoad = 0; - this.load = 0; - this.updateTime = DateUtil.now(); - } - - public HugeServerInfo(Id id, NodeRole role) { - this.id = id; - this.load = 0; - this.role = role; - this.updateTime = DateUtil.now(); - } - - public Id id() { - return this.id; - } - - public String name() { - return this.id.asString(); - } - - public NodeRole role() { - return this.role; - } - - public void role(NodeRole role) { - this.role = role; - } - - public int maxLoad() { - return this.maxLoad; - } - - public void maxLoad(int maxLoad) { - this.maxLoad = maxLoad; - } - - public int load() { - return this.load; - } - - public void load(int load) { - this.load = load; - } - - public void increaseLoad(int delta) { - this.load += delta; - this.updated = true; - } - - public long expireTime() { - return this.updateTime.getTime() + EXPIRED_INTERVAL; - } - - public Date updateTime() { - return this.updateTime; - } - - public void updateTime(Date updateTime) { - this.updateTime = updateTime; - } - - public boolean alive() { - long now = DateUtil.now().getTime(); - return this.updateTime != null && - this.updateTime.getTime() + EXPIRED_INTERVAL > now; - } - - public boolean updated() { - return this.updated; - } - - @Override - public String toString() { - return String.format("HugeServerInfo(%s)%s", this.id, this.asMap()); - } - - protected boolean property(String key, Object value) { - switch (key) { - case P.ROLE: - this.role = SerialEnum.fromCode(NodeRole.class, (byte) value); - break; - case P.MAX_LOAD: - this.maxLoad = (int) value; - break; - case P.LOAD: - this.load = (int) value; - break; - case P.UPDATE_TIME: - this.updateTime = (Date) value; - break; - default: - throw new AssertionError("Unsupported key: " + key); - } - return true; - } - - protected Object[] asArray() { - E.checkState(this.id != null, "Server id can't be null"); - - List list = new ArrayList<>(12); - - list.add(T.label); - list.add(P.SERVER); - - list.add(T.id); - list.add(this.id); - - list.add(P.ROLE); - list.add(this.role.code()); - - list.add(P.MAX_LOAD); - list.add(this.maxLoad); - - list.add(P.LOAD); - list.add(this.load); - - list.add(P.UPDATE_TIME); - list.add(this.updateTime); - - return list.toArray(); - } - - public Map asMap() { - E.checkState(this.id != null, "Server id can't be null"); - - Map map = new HashMap<>(); - - map.put(Graph.Hidden.unHide(P.ID), this.id); - map.put(Graph.Hidden.unHide(P.LABEL), P.SERVER); - map.put(Graph.Hidden.unHide(P.ROLE), this.role); - map.put(Graph.Hidden.unHide(P.MAX_LOAD), this.maxLoad); - map.put(Graph.Hidden.unHide(P.LOAD), this.load); - map.put(Graph.Hidden.unHide(P.UPDATE_TIME), this.updateTime); - - return map; - } - - public static HugeServerInfo fromVertex(Vertex vertex) { - HugeServerInfo serverInfo = new HugeServerInfo((Id) vertex.id()); - for (var iter = vertex.properties(); iter.hasNext(); ) { - VertexProperty prop = iter.next(); - serverInfo.property(prop.key(), prop.value()); - } - return serverInfo; - } - - public static Schema schema(HugeGraphParams graph) { - return new Schema(graph); - } - - public static final class P { - - public static final String SERVER = Graph.Hidden.hide("server"); - - public static final String ID = T.id.getAccessor(); - public static final String LABEL = T.label.getAccessor(); - - public static final String NAME = "~server_name"; - public static final String ROLE = "~server_role"; - public static final String LOAD = "~server_load"; - public static final String MAX_LOAD = "~server_max_load"; - public static final String UPDATE_TIME = "~server_update_time"; - - public static String unhide(String key) { - final String prefix = Graph.Hidden.hide("server_"); - if (key.startsWith(prefix)) { - return key.substring(prefix.length()); - } - return key; - } - } - - public static final class Schema { - - public static final String SERVER = P.SERVER; - - private final HugeGraphParams graph; - - public Schema(HugeGraphParams graph) { - this.graph = graph; - } - - public void initSchemaIfNeeded() { - if (this.existVertexLabel(SERVER)) { - return; - } - - HugeGraph graph = this.graph.graph(); - String[] properties = this.initProperties(); - - // Create vertex label '~server' - VertexLabel label = graph.schema().vertexLabel(SERVER) - .properties(properties) - .useCustomizeStringId() - .nullableKeys(P.ROLE, P.MAX_LOAD, P.LOAD, P.UPDATE_TIME) - .enableLabelIndex(true) - .build(); - this.graph.schemaTransaction().addVertexLabel(label); - } - - private String[] initProperties() { - List props = new ArrayList<>(); - props.add(createPropertyKey(P.ROLE, DataType.BYTE)); - props.add(createPropertyKey(P.MAX_LOAD, DataType.INT)); - props.add(createPropertyKey(P.LOAD, DataType.INT)); - props.add(createPropertyKey(P.UPDATE_TIME, DataType.DATE)); - - return props.toArray(new String[0]); - } - - public boolean existVertexLabel(String label) { - return this.graph.schemaTransaction().getVertexLabel(label) != null; - } - - @SuppressWarnings("unused") - private String createPropertyKey(String name) { - return this.createPropertyKey(name, DataType.TEXT); - } - - private String createPropertyKey(String name, DataType dataType) { - return this.createPropertyKey(name, dataType, Cardinality.SINGLE); - } - - private String createPropertyKey(String name, DataType dataType, Cardinality cardinality) { - SchemaManager schema = this.graph.graph().schema(); - PropertyKey propertyKey = schema.propertyKey(name) - .dataType(dataType) - .cardinality(cardinality) - .build(); - this.graph.schemaTransaction().addPropertyKey(propertyKey); - return name; - } - - @SuppressWarnings("unused") - private IndexLabel createIndexLabel(VertexLabel label, String field) { - SchemaManager schema = this.graph.graph().schema(); - String name = Graph.Hidden.hide("server-index-by-" + field); - IndexLabel indexLabel = schema.indexLabel(name) - .on(HugeType.VERTEX_LABEL, SERVER) - .by(field) - .build(); - this.graph.schemaTransaction().addIndexLabel(label, indexLabel); - return indexLabel; - } - - @SuppressWarnings("unused") - private IndexLabel indexLabel(String field) { - String name = Graph.Hidden.hide("server-index-by-" + field); - return this.graph.graph().indexLabel(name); - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTask.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTask.java index f63d4c0762..6d36212f56 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTask.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTask.java @@ -17,6 +17,7 @@ package org.apache.hugegraph.task; +import java.io.InterruptedIOException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -29,11 +30,11 @@ import java.util.stream.Collector; import java.util.stream.Collectors; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.exception.LimitExceedException; import org.apache.hugegraph.exception.NotFoundException; @@ -52,6 +53,7 @@ import org.apache.tinkerpop.gremlin.structure.Graph.Hidden; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalInterruptedException; import org.apache.tinkerpop.gremlin.structure.VertexProperty; import org.slf4j.Logger; @@ -346,9 +348,16 @@ public boolean cancel(boolean mayInterruptIfRunning) { return cancelled; } + private static boolean isInterrupted(Throwable error) { + Throwable cause = HugeException.rootCause(error); + return cause instanceof InterruptedException || + cause instanceof TraversalInterruptedException || + cause instanceof InterruptedIOException; + } + public boolean fail(Throwable e) { E.checkNotNull(e, "exception"); - if (!(this.cancelled() && HugeException.isInterrupted(e))) { + if (!(this.cancelled() && isInterrupted(e))) { LOG.warn("An exception occurred when running task: {}", this.id(), e); // Update status to FAILED if exception occurred(not interrupted) diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTaskResult.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTaskResult.java index 1c47ee9da5..9f01d60acc 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTaskResult.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/HugeTaskResult.java @@ -21,7 +21,7 @@ import java.util.Iterator; import java.util.List; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.util.Blob; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/ServerInfoManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/ServerInfoManager.java index dac78f2fcc..7155913fef 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/ServerInfoManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/ServerInfoManager.java @@ -17,15 +17,9 @@ package org.apache.hugegraph.task; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; - -import org.apache.hugegraph.HugeException; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.tx.GraphTransaction; -import org.apache.hugegraph.exception.ConnectionException; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.masterelection.GlobalMasterInfo; import org.apache.hugegraph.type.define.NodeRole; import org.apache.hugegraph.util.E; @@ -33,31 +27,22 @@ public class ServerInfoManager { private final HugeGraphParams graph; - private final ExecutorService dbExecutor; private volatile GlobalMasterInfo globalNodeInfo; private volatile boolean closed; - public ServerInfoManager(HugeGraphParams graph, ExecutorService dbExecutor) { + public ServerInfoManager(HugeGraphParams graph) { E.checkNotNull(graph, "graph"); - E.checkNotNull(dbExecutor, "db executor"); this.graph = graph; - this.dbExecutor = dbExecutor; this.globalNodeInfo = null; this.closed = false; } - public void init() { - // ServerInfo is soft-disabled; keep this method for compatibility. - } - public synchronized boolean close() { - // ServerInfo persistence is soft-deprecated; init() and heartbeat() - // are no-ops, so there's nothing to clean up in close(). this.closed = true; return true; } @@ -103,27 +88,4 @@ public NodeRole selfNodeRole() { public boolean selfIsMaster() { return this.selfNodeRole() != null && this.selfNodeRole().master(); } - - public synchronized void heartbeat() { - // ServerInfo heartbeat is deprecated for local scheduling. - } - - private GraphTransaction tx() { - assert Thread.currentThread().getName().contains("server-info-db-worker"); - return this.graph.systemTransaction(); - } - - private V call(Callable callable) { - assert !Thread.currentThread().getName().startsWith( - "server-info-db-worker") : "can't call by itself"; - try { - // Pass context for db thread - callable = new TaskManager.ContextCallable<>(callable); - // Ensure all db operations are executed in dbExecutor thread(s) - return this.dbExecutor.submit(callable).get(); - } catch (Throwable e) { - throw new HugeException("Failed to update/query server info: %s", - e, e.toString()); - } - } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/StandardTaskScheduler.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/StandardTaskScheduler.java index 269094bc7e..841e8bdb7a 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/StandardTaskScheduler.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/StandardTaskScheduler.java @@ -26,15 +26,18 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.page.PageInfo; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.store.BackendStore; import org.apache.hugegraph.config.CoreOptions; @@ -43,8 +46,8 @@ import org.apache.hugegraph.iterator.ExtendableIterator; import org.apache.hugegraph.iterator.MapperIterator; import org.apache.hugegraph.job.EphemeralJob; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.task.HugeTask.P; import org.apache.hugegraph.task.TaskCallable.SysTaskCallable; @@ -69,13 +72,15 @@ public class StandardTaskScheduler implements TaskScheduler { private final ExecutorService taskDbExecutor; private final Map> tasks; + private final Map> executions; private volatile TaskTransaction taskTx; + private boolean admissionClosed; + private int cancellations; public StandardTaskScheduler(HugeGraphParams graph, ExecutorService taskExecutor, - ExecutorService taskDbExecutor, - ExecutorService serverInfoDbExecutor) { + ExecutorService taskDbExecutor) { E.checkNotNull(graph, "graph"); E.checkNotNull(taskExecutor, "taskExecutor"); E.checkNotNull(taskDbExecutor, "dbExecutor"); @@ -84,8 +89,9 @@ public StandardTaskScheduler(HugeGraphParams graph, this.taskExecutor = taskExecutor; this.taskDbExecutor = taskDbExecutor; - this.serverManager = new ServerInfoManager(graph, serverInfoDbExecutor); + this.serverManager = new ServerInfoManager(graph); this.tasks = new ConcurrentHashMap<>(); + this.executions = new ConcurrentHashMap<>(); this.taskTx = null; } @@ -116,8 +122,8 @@ public String spaceGraphName() { } @Override - public int pendingTasks() { - return this.tasks.size(); + public synchronized int pendingTasks() { + return this.tasks.size() + this.cancellations; } private TaskTransaction tx() { @@ -142,7 +148,8 @@ private TaskTransaction tx() { } @Override - public void restoreTasks() { + public synchronized void restoreTasks() { + this.checkAdmission(); List> taskList = new ArrayList<>(); // Single-node mode: restore pending tasks without server filtering. // Don't restore legacy SCHEDULING/SCHEDULED tasks globally; they were @@ -185,7 +192,8 @@ private Future restore(HugeTask task) { } @Override - public Future schedule(HugeTask task) { + public synchronized Future schedule(HugeTask task) { + this.checkAdmission(); E.checkArgumentNotNull(task, "Task can't be null"); /* @@ -231,7 +239,7 @@ private Future submitTask(HugeTask task, boolean saveAfterQueued) { throw e; } } - return this.taskExecutor.submit(task); + return this.submitExecution(task); } private Future resubmitTask(HugeTask task) { @@ -241,7 +249,88 @@ private Future resubmitTask(HugeTask task) { E.checkArgument(this.tasks.containsKey(task.id()), "Can't resubmit task '%s' not been submitted before", task.id()); - return this.taskExecutor.submit(task); + return this.submitExecution(task); + } + + private Future submitExecution(HugeTask task) { + PendingTask pending = new PendingTask(task); + this.executions.computeIfAbsent(task.id(), id -> new ArrayList<>()).add(pending); + try { + this.taskExecutor.execute(pending); + } catch (RuntimeException | Error error) { + this.finishExecution(pending); + throw error; + } + return pending; + } + + private synchronized void finishExecution(PendingTask pending) { + List outstanding = this.executions.get(pending.task.id()); + if (outstanding != null && outstanding.remove(pending) && outstanding.isEmpty()) { + this.executions.remove(pending.task.id()); + this.tasks.remove(pending.task.id(), pending.task); + } + } + + private synchronized void cancelQueuedExecutions(Id id) { + List outstanding = this.executions.get(id); + if (outstanding != null) { + for (PendingTask pending : new ArrayList<>(outstanding)) { + pending.cancelBeforeStart(); + } + } + } + + private final class PendingTask extends FutureTask { + + private final HugeTask task; + private final AtomicBoolean started = new AtomicBoolean(); + + private PendingTask(HugeTask task) { + super(new ContextCallable<>(() -> { + task.run(); + return null; + })); + this.task = task; + } + + @Override + public void run() { + if (!this.started.compareAndSet(false, true)) { + return; + } + try { + super.run(); + } finally { + // A cleanup failure must retain this owner and prevent close ACK. + StandardTaskScheduler.this.graph.closeTx(); + StandardTaskScheduler.this.finishExecution(this); + } + } + + @Override + protected void done() { + if (this.isCancelled()) { + // Cancelling the dispatch Future must not run HugeTask callbacks + // on its caller or cancel a later dependency retry. + this.cancelBeforeStart(); + } + } + + private void cancelBeforeStart() { + if (this.started.compareAndSet(false, true)) { + super.cancel(false); + if (StandardTaskScheduler.this.taskExecutor instanceof ThreadPoolExecutor) { + ((ThreadPoolExecutor) StandardTaskScheduler.this.taskExecutor).remove(this); + } + StandardTaskScheduler.this.finishExecution(this); + } + } + } + + private void checkAdmission() { + E.checkState(!this.admissionClosed, "Task scheduler for graph '%s' is closing", + this.spaceGraphName()); } public void initTaskCallable(HugeTask task) { @@ -257,32 +346,48 @@ public void initTaskCallable(HugeTask task) { } @Override - public synchronized void cancel(HugeTask task) { + public void cancel(HugeTask task) { E.checkArgumentNotNull(task, "Task can't be null"); - - if (task.completed() || task.cancelling()) { - return; - } - - LOG.info("Cancel task '{}' in status {}", task.id(), task.status()); - - HugeTask memTask = this.tasks.get(task.id()); - if (memTask != null) { - boolean cancelled = memTask.cancel(true); - if (cancelled) { - task.overwriteStatus(TaskStatus.CANCELLED); + boolean callerTxOpen = this.graph.graph().tx().isOpen(); + HugeTask memTask; + synchronized (this) { + if (task.completed() || task.cancelling()) { + return; } - LOG.info("Task '{}' cancel result: {}", task.id(), cancelled); - return; + memTask = this.tasks.get(task.id()); + if (memTask == null) { + this.checkAdmission(); + } + this.cancellations++; } - - if (task.status(TaskStatus.CANCELLED)) { - this.save(task); - return; + // HugeTask.cancel() runs done/cancelled callbacks on this caller. + // Do not hold the scheduler monitor while they wait for storage/workers. + try { + if (memTask != null) { + boolean cancelled = memTask.cancel(true); + if (cancelled) { + task.overwriteStatus(TaskStatus.CANCELLED); + } + return; + } + if (task.status(TaskStatus.CANCELLED)) { + this.save(task); + return; + } + throw new HugeException("Can't cancel task '%s' in status %s", + task.id(), task.status()); + } finally { + if (memTask != null && memTask.isCancelled()) { + this.cancelQueuedExecutions(task.id()); + } + // Keep the callback registered until its own transaction really closes. + if (!callerTxOpen) { + this.graph.closeTx(); + } + synchronized (this) { + this.cancellations--; + } } - - throw new HugeException("Can't cancel task '%s' in status %s", - task.id(), task.status()); } @Override @@ -291,7 +396,10 @@ public ServerInfoManager serverManager() { } @Override public void taskDone(HugeTask task) { - this.remove(task); + // Logical completion/cancellation can precede the worker's final cleanup. + if (!this.executions.containsKey(task.id())) { + this.remove(task); + } // Single-node mode: no need to manage load LOG.debug("Task '{}' done", task.id()); } @@ -302,6 +410,9 @@ protected void remove(HugeTask task) { protected void remove(HugeTask task, boolean force) { E.checkNotNull(task, "remove task"); + if (this.executions.containsKey(task.id())) { + return; + } HugeTask delTask = this.tasks.remove(task.id()); if (delTask != null && delTask != task) { LOG.warn("Task '{}' may be inconsistent status {}(expect {})", @@ -317,6 +428,12 @@ public void save(HugeTask task) { task.scheduler(this); E.checkArgumentNotNull(task, "Task can't be null"); this.call(() -> { + // Completion can save after deletion, while this exact + // execution owner is still retained for native cleanup. + if (task.status() == TaskStatus.DELETING && this.tasks.get(task.id()) == task && + QueryResults.one(this.tx().queryTaskInfos(task.id())) == null) { + return null; + } // Construct vertex from task HugeVertex vertex = this.tx().constructVertex(task); // Delete index of old vertex to avoid stale index @@ -332,18 +449,60 @@ public void init() { } @Override - public boolean close() { - if (!this.taskDbExecutor.isShutdown()) { - this.call(() -> { - try { - this.tx().close(); - } catch (ConnectionException ignored) { - // ConnectionException means no connection established + public synchronized boolean close() { + this.admissionClosed = true; + // Running tasks still need the task DB transaction to persist done(). + // Retain the scheduler and its owners until a later close attempt. + if (this.pendingTasks() != 0) { + return false; + } + Throwable failure = null; + boolean closed = false; + try { + if (!this.taskDbExecutor.isShutdown()) { + this.call(() -> { + Throwable workerFailure = null; + for (Runnable close : new Runnable[]{() -> { + try { + this.tx().close(); + } catch (ConnectionException ignored) { + // ConnectionException means no connection established + } + }, this.graph::closeTx}) { + try { + close.run(); + } catch (RuntimeException | Error error) { + if (workerFailure == null) { + workerFailure = error; + } else if (workerFailure != error) { + workerFailure.addSuppressed(error); + } + } + } + if (workerFailure instanceof Error) { + throw (Error) workerFailure; + } + if (workerFailure != null) { + throw (RuntimeException) workerFailure; + } + }); + } + } catch (RuntimeException | Error error) { + failure = error; + throw error; + } finally { + try { + closed = this.serverManager.close(); + } catch (RuntimeException | Error error) { + if (failure == null) { + throw error; } - this.graph.closeTx(); - }); + if (failure != error) { + failure.addSuppressed(error); + } + } } - return this.serverManager.close(); + return closed; } @Override @@ -356,7 +515,7 @@ public HugeTask task(Id id, boolean withResult) { E.checkArgumentNotNull(id, "Parameter task id can't be null"); @SuppressWarnings("unchecked") HugeTask task = (HugeTask) this.tasks.get(id); - if (task != null) { + if (task != null && task.status() != TaskStatus.DELETING) { return withResult ? task : task.copyWithoutResult(); } return this.findTask(id, withResult); @@ -375,7 +534,7 @@ public Iterator> tasks(List ids, for (Id id : ids) { @SuppressWarnings("unchecked") HugeTask task = (HugeTask) this.tasks.get(id); - if (task != null) { + if (task != null && task.status() != TaskStatus.DELETING) { taskInMem.add(withResult ? task : task.copyWithoutResult()); } else { taskIdsNotInMem.add(id); @@ -472,13 +631,17 @@ public HugeTask delete(Id id, boolean force) { * when the database status is inconsistent. */ if (running != null) { - E.checkArgument(force || running.completed(), + E.checkArgument(force || running.completed() || + (running.status() == TaskStatus.DELETING && task.completed()), "Can't delete incomplete task '%s' in status %s" + ", Please try to cancel the task first", id, running.status()); - if (force && !running.completed()) { + boolean completed = running.completed(); + if (force) { running.overwriteStatus(TaskStatus.DELETING); - running.cancel(true); + if (!completed) { + running.cancel(true); + } } this.remove(running, force); } else if (task != null) { @@ -499,6 +662,11 @@ public HugeTask delete(Id id, boolean force) { E.checkState(force || result.completed(), "Can't delete incomplete task '%s' in status %s", id, result.status()); + // A non-force request must pass the persisted completion check + // before its retained owner can be marked for deletion. + if (!force && running != null && this.tasks.get(id) == running) { + running.overwriteStatus(TaskStatus.DELETING); + } this.tx().removeTaskVertex(vertex); return result; }); @@ -597,7 +765,7 @@ private Iterator> queryTask(Map conditions, boolean withResult) { return this.call(() -> { ConditionQuery query; - if (this.graph.backendStoreFeatures().supportsTaskAndServerVertex()) { + if (this.tx().storeFeatures().supportsTaskAndServerVertex()) { query = new ConditionQuery(HugeType.TASK); } else { query = new ConditionQuery(HugeType.VERTEX); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskAndResultScheduler.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskAndResultScheduler.java index 70d4d2f571..70ccf10ed3 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskAndResultScheduler.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskAndResultScheduler.java @@ -21,19 +21,18 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.concurrent.ExecutorService; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.store.BackendStore; import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.iterator.ListIterator; import org.apache.hugegraph.iterator.MapperIterator; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.HugeKeys; @@ -61,16 +60,14 @@ public abstract class TaskAndResultScheduler implements TaskScheduler { private final ServerInfoManager serverManager; - public TaskAndResultScheduler( - HugeGraphParams graph, - ExecutorService serverInfoDbExecutor) { + public TaskAndResultScheduler(HugeGraphParams graph) { E.checkNotNull(graph, "graph"); this.graph = graph; this.graphSpace = graph.graph().graphSpace(); this.graphName = graph.name(); - this.serverManager = new ServerInfoManager(graph, serverInfoDbExecutor); + this.serverManager = new ServerInfoManager(graph); } @Override diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskAndResultTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskAndResultTransaction.java index c39ae23615..c9f766f2ef 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskAndResultTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskAndResultTransaction.java @@ -20,13 +20,13 @@ import java.util.ArrayList; import java.util.List; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.backend.store.BackendStore; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskCallable.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskCallable.java index 4e0550f5c9..9d7c9a6b79 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskCallable.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskCallable.java @@ -21,7 +21,7 @@ import java.util.Set; import java.util.concurrent.Callable; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.util.E; @@ -56,7 +56,11 @@ public TaskCallable() { } protected void done() { - this.closeTx(); + // Future cancellation runs done() on its caller, whose transaction may predate the callback. + // The scheduler closes callback-owned transactions and the actual worker's transaction separately. + if (this.task == null || !this.task.isCancelled()) { + this.closeTx(); + } } protected void cancelled() { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskManager.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskManager.java index ec063754d8..89b964e434 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskManager.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskManager.java @@ -17,15 +17,18 @@ package org.apache.hugegraph.task; +import java.util.ArrayList; import java.util.Map; +import java.util.Queue; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.concurrent.PausableScheduledThreadPool; import org.apache.hugegraph.util.Consumers; @@ -36,10 +39,7 @@ /** * Central task management system that coordinates task scheduling and execution. - * Manages task schedulers for different graphs and handles role-based execution. - *

- * Note: The local master-worker mechanism will be deprecated in version 1.7 - * (configuration has been removed from config files). + * Manages the task schedulers of each graph and the executors they share. */ public final class TaskManager { @@ -48,8 +48,6 @@ public final class TaskManager { public static final String TASK_WORKER_PREFIX = "task-worker"; public static final String TASK_WORKER = TASK_WORKER_PREFIX + "-%d"; public static final String TASK_DB_WORKER = "task-db-worker-%d"; - public static final String SERVER_INFO_DB_WORKER = "server-info-db-worker-%d"; - public static final String TASK_SCHEDULER = "task-scheduler-%d"; public static final String OLAP_TASK_WORKER = "olap-task-worker-%d"; public static final String SCHEMA_TASK_WORKER = "schema-task-worker-%d"; @@ -65,8 +63,6 @@ public final class TaskManager { private final ExecutorService taskExecutor; private final ExecutorService taskDbExecutor; - private final ExecutorService serverInfoDbExecutor; - private final PausableScheduledThreadPool schedulerExecutor; private final ExecutorService schemaTaskExecutor; private final ExecutorService olapTaskExecutor; @@ -85,18 +81,12 @@ private TaskManager(int pool) { // For save/query task state, just one thread is ok this.taskDbExecutor = ExecutorUtil.newFixedThreadPool( 1, TASK_DB_WORKER); - this.serverInfoDbExecutor = ExecutorUtil.newFixedThreadPool( - 1, SERVER_INFO_DB_WORKER); this.schemaTaskExecutor = ExecutorUtil.newFixedThreadPool(pool, SCHEMA_TASK_WORKER); this.olapTaskExecutor = ExecutorUtil.newFixedThreadPool(pool, OLAP_TASK_WORKER); this.ephemeralTaskExecutor = ExecutorUtil.newFixedThreadPool(pool, EPHEMERAL_TASK_WORKER); this.distributedSchedulerExecutor = ExecutorUtil.newPausableScheduledThreadPool(1, DISTRIBUTED_TASK_SCHEDULER); - - // For a schedule task to run, just one thread is ok - this.schedulerExecutor = ExecutorUtil.newPausableScheduledThreadPool( - 1, TASK_SCHEDULER); } public void addScheduler(HugeGraphParams graph) { @@ -116,8 +106,7 @@ public void addScheduler(HugeGraphParams graph) { schemaTaskExecutor, olapTaskExecutor, taskExecutor, /* gremlinTaskExecutor */ - ephemeralTaskExecutor, - serverInfoDbExecutor); + ephemeralTaskExecutor); this.schedulers.put(graph, scheduler); break; } @@ -127,8 +116,7 @@ public void addScheduler(HugeGraphParams graph) { new StandardTaskScheduler( graph, this.taskExecutor, - this.taskDbExecutor, - this.serverInfoDbExecutor); + this.taskDbExecutor); this.schedulers.put(graph, scheduler); break; } @@ -137,35 +125,55 @@ public void addScheduler(HugeGraphParams graph) { public void closeScheduler(HugeGraphParams graph) { TaskScheduler scheduler = this.schedulers.get(graph); + Throwable failure = null; + boolean drained = scheduler == null; if (scheduler != null) { - /* - * Synch close+remove scheduler and iterate scheduler, details: - * 'closeScheduler' should sync with 'scheduleOrExecuteJob'. - * Because 'closeScheduler' will be called by 'graph.close()' in - * main thread and there is gap between 'scheduler.close()' - * (will close graph tx) and 'this.schedulers.remove(graph)'. - * In this gap 'scheduleOrExecuteJob' may be run in - * scheduler-db-thread and 'scheduleOrExecuteJob' will reopen - * graph tx. As a result, graph tx will mistakenly not be closed - * after 'graph.close()'. - */ - synchronized (scheduler) { - if (scheduler.close()) { - this.schedulers.remove(graph); + try { + // Distributed close waits for cron without holding its monitor. + if (scheduler.close() && scheduler.pendingTasks() == 0) { + this.schedulers.remove(graph, scheduler); + drained = true; } + } catch (RuntimeException | Error error) { + failure = error; } } - - if (!this.taskExecutor.isTerminated()) { - this.closeTaskTx(graph); + if (!drained && (failure == null || scheduler.pendingTasks() != 0)) { + // A running cron/job must finish before owner callbacks are queued + // on its executor. In particular, do not rejoin a timed-out cron. + if (failure instanceof Error) { + throw (Error) failure; + } + if (failure != null) { + throw (RuntimeException) failure; + } + return; } - - if (!this.schedulerExecutor.isTerminated()) { - this.closeSchedulerTx(graph); + // An owner-close failure retains the scheduler, but idle workers can still clean up. + for (Runnable close : new Runnable[]{() -> { + if (!this.taskExecutor.isTerminated()) { + this.closeTaskTx(graph); + } + }, () -> { + if (!this.distributedSchedulerExecutor.isTerminated()) { + this.closeDistributedSchedulerTx(graph); + } + }}) { + try { + close.run(); + } catch (RuntimeException | Error error) { + if (failure == null) { + failure = error; + } else if (failure != error) { + failure.addSuppressed(error); + } + } } - - if (!this.distributedSchedulerExecutor.isTerminated()) { - this.closeDistributedSchedulerTx(graph); + if (failure instanceof Error) { + throw (Error) failure; + } + if (failure != null) { + throw (RuntimeException) failure; } } @@ -177,31 +185,42 @@ private void closeTaskTx(HugeGraphParams graph) { final boolean selfIsTaskWorker = Thread.currentThread().getName() .startsWith(TASK_WORKER_PREFIX); final int totalThreads = selfIsTaskWorker ? THREADS - 1 : THREADS; + Queue failures = new ConcurrentLinkedQueue<>(); + Runnable close = () -> { + try { + graph.closeTx(); + } catch (RuntimeException | Error error) { + // invokeAll() does not inspect worker Futures. Capture failures + // here so each worker still completes its execution accounting. + failures.add(error); + LOG.error("Failed to close task tx in thread '{}'", Thread.currentThread().getName(), error); + } + }; try { if (selfIsTaskWorker) { // Call closeTx directly if myself is task thread(ignore others) - graph.closeTx(); + close.run(); } else { Consumers.executeOncePerThread(this.taskExecutor, totalThreads, - graph::closeTx, TX_CLOSE_TIMEOUT); + close, TX_CLOSE_TIMEOUT); + } + } catch (Exception error) { + failures.add(error); + if (error instanceof InterruptedException) { + Thread.currentThread().interrupt(); } - } catch (Exception e) { - throw new HugeException("Exception when closing task tx", e); } - } - - private void closeSchedulerTx(HugeGraphParams graph) { - final Callable closeTx = () -> { - // Do close-tx for the current thread - graph.closeTx(); - // Let other threads run - Thread.yield(); - return null; - }; - try { - this.schedulerExecutor.submit(closeTx).get(); - } catch (Exception e) { - throw new HugeException("Exception when closing scheduler tx", e); + Throwable failure = failures.poll(); + if (failure != null) { + for (Throwable error : failures) { + if (error != failure) { + failure.addSuppressed(error); + } + } + if (failure instanceof Error) { + throw (Error) failure; + } + throw new HugeException("Exception when closing task tx", failure); } } @@ -233,89 +252,54 @@ public ServerInfoManager getServerInfoManager(HugeGraphParams graph) { } public void shutdown(long timeout) { - assert this.schedulers.isEmpty() : this.schedulers.size(); - - Throwable ex = null; - boolean terminated = this.schedulerExecutor.isTerminated(); - final TimeUnit unit = TimeUnit.SECONDS; - - if (!this.schedulerExecutor.isShutdown()) { - this.schedulerExecutor.shutdown(); - try { - terminated = this.schedulerExecutor.awaitTermination(timeout, unit); - } catch (Throwable e) { - ex = e; - } - } - - if (terminated && !this.distributedSchedulerExecutor.isShutdown()) { - this.distributedSchedulerExecutor.shutdown(); - try { - terminated = this.distributedSchedulerExecutor.awaitTermination(timeout, unit); - } catch (Throwable e) { - ex = e; - } - } - - if (terminated && !this.taskExecutor.isShutdown()) { - this.taskExecutor.shutdown(); - try { - terminated = this.taskExecutor.awaitTermination(timeout, unit); - } catch (Throwable e) { - ex = e; - } - } - - if (terminated && !this.serverInfoDbExecutor.isShutdown()) { - this.serverInfoDbExecutor.shutdown(); - try { - terminated = this.serverInfoDbExecutor.awaitTermination(timeout, unit); - } catch (Throwable e) { - ex = e; - } + if (!this.schedulers.isEmpty()) { + LOG.warn("Stopping task executors with {} schedulers still registered", this.schedulers.size()); } - if (terminated && !this.taskDbExecutor.isShutdown()) { - this.taskDbExecutor.shutdown(); - try { - terminated = this.taskDbExecutor.awaitTermination(timeout, unit); - } catch (Throwable e) { - ex = e; - } + try { + shutdownExecutor(this.distributedSchedulerExecutor, timeout); + shutdownExecutor(this.taskExecutor, timeout); + // Early REST/Gremlin closes can retain graphs while tasks finish. + // Their DB owners must close before the owner executor stops. + this.closeRetainedGraphs(); + shutdownExecutor(this.taskDbExecutor, timeout); + shutdownExecutor(this.ephemeralTaskExecutor, timeout); + shutdownExecutor(this.schemaTaskExecutor, timeout); + shutdownExecutor(this.olapTaskExecutor, timeout); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HugeException("Failed to wait for TaskScheduler", e); + } catch (TimeoutException e) { + throw new HugeException("Failed to wait for TaskScheduler", e); } + } - if (terminated && !this.ephemeralTaskExecutor.isShutdown()) { - this.ephemeralTaskExecutor.shutdown(); - try { - terminated = this.ephemeralTaskExecutor.awaitTermination(timeout, unit); - } catch (Throwable e) { - ex = e; - } + private static void shutdownExecutor(ExecutorService executor, long timeout) + throws InterruptedException, TimeoutException { + if (!executor.isShutdown()) { + executor.shutdown(); } - - if (terminated && !this.schemaTaskExecutor.isShutdown()) { - this.schemaTaskExecutor.shutdown(); - try { - terminated = this.schemaTaskExecutor.awaitTermination(timeout, unit); - } catch (Throwable e) { - ex = e; - } + if (!executor.isTerminated() && !executor.awaitTermination(timeout, TimeUnit.SECONDS)) { + throw new TimeoutException(timeout + "s"); } + } - if (terminated && !this.olapTaskExecutor.isShutdown()) { - this.olapTaskExecutor.shutdown(); + private void closeRetainedGraphs() { + Throwable failure = null; + for (HugeGraphParams graph : new ArrayList<>(this.schedulers.keySet())) { try { - terminated = this.olapTaskExecutor.awaitTermination(timeout, unit); - } catch (Throwable e) { - ex = e; + graph.graph().close(); + } catch (Exception | Error error) { + if (failure == null) { + failure = error; + } else if (failure != error) { + failure.addSuppressed(error); + } } } - - if (!terminated) { - ex = new TimeoutException(timeout + "s"); - } - if (ex != null) { - throw new HugeException("Failed to wait for TaskScheduler", ex); + if (failure != null) { + // Keep the registry and DB executor available for an explicit retry. + throw new HugeException("Failed to close retained graphs before task DB shutdown", failure); } } @@ -331,14 +315,6 @@ public int pendingTasks() { return size; } - public void onAsRoleMaster() { - // ServerInfo based role propagation is deprecated. - } - - public void onAsRoleWorker() { - // ServerInfo based role propagation is deprecated. - } - private static final ThreadLocal CONTEXTS = new ThreadLocal<>(); public static void setContext(String context) { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskScheduler.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskScheduler.java index ea10cd006d..a4f8cbd56e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskScheduler.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskScheduler.java @@ -24,7 +24,7 @@ import java.util.concurrent.TimeoutException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; public interface TaskScheduler { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskTransaction.java index 9dd4a46aa0..74e96cd18b 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/TaskTransaction.java @@ -21,17 +21,17 @@ import java.util.Iterator; import java.util.List; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.store.BackendStore; import org.apache.hugegraph.backend.tx.GraphTransaction; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; -import org.apache.hugegraph.structure.HugeIndex; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.Index; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; @@ -96,7 +96,7 @@ private void removeLabelIndex(HugeVertex vertex) { return; } - HugeIndex index = new HugeIndex(this.graph(), + Index index = new Index(this.graph(), IndexLabel.label(vertex.type())); index.fieldValues(vertex.schemaLabel().id()); index.elementIds(vertex.id(), vertex.expiredTime()); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CollectionPathsTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CollectionPathsTraverser.java index c2a5ba4f79..d820dd03b4 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CollectionPathsTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CollectionPathsTraverser.java @@ -24,7 +24,7 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.algorithm.steps.EdgeStep; import org.apache.hugegraph.traversal.algorithm.strategy.TraverseStrategy; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CountTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CountTraverser.java index 1b9defac46..442dbaeb31 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CountTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CountTraverser.java @@ -23,7 +23,7 @@ import org.apache.commons.lang.mutable.MutableLong; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.iterator.FilterIterator; import org.apache.hugegraph.iterator.FlatMapperIterator; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CustomizePathsTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CustomizePathsTraverser.java index b639df2d4c..dab3d79f70 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CustomizePathsTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CustomizePathsTraverser.java @@ -23,7 +23,7 @@ import java.util.Map; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.algorithm.steps.WeightedEdgeStep; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CustomizedCrosspointsTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CustomizedCrosspointsTraverser.java index be3a244eb2..4c0ede0806 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CustomizedCrosspointsTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/CustomizedCrosspointsTraverser.java @@ -25,7 +25,7 @@ import java.util.stream.Collectors; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.algorithm.steps.EdgeStep; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/EdgeExistenceTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/EdgeExistenceTraverser.java index 98fbfbd9d9..d47f987e0a 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/EdgeExistenceTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/EdgeExistenceTraverser.java @@ -20,10 +20,10 @@ import java.util.Iterator; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.iterator.FilterIterator; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.type.define.HugeKeys; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/FusiformSimilarityTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/FusiformSimilarityTraverser.java index 741a6ef6fe..87dd7c8876 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/FusiformSimilarityTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/FusiformSimilarityTraverser.java @@ -24,8 +24,8 @@ import org.apache.commons.lang3.mutable.MutableInt; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.define.Directions; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/HugeTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/HugeTraverser.java index fe8fa05687..4989c6ecc6 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/HugeTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/HugeTraverser.java @@ -34,13 +34,13 @@ import java.util.stream.Collectors; import org.apache.commons.collections.CollectionUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Aggregate; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Aggregate; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.query.EdgesQueryIterator; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.config.CoreOptions; @@ -50,7 +50,7 @@ import org.apache.hugegraph.iterator.LimitIterator; import org.apache.hugegraph.iterator.MapperIterator; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.SchemaLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.algorithm.iterator.NestedIterator; @@ -483,7 +483,7 @@ private Iterator edgesOfVertex(Id source, EdgeStep edgeStep, if (filter != null) { ConditionQuery finalFilter = filter; edges = new FilterIterator<>(edges, (e) -> { - return finalFilter.test((HugeEdge) e); + return finalFilter.test(((HugeEdge) e).element()); }); } return edgeStep.skipSuperNodeIfNeeded(edges); @@ -544,7 +544,7 @@ private Boolean validateVertex(Map conditions, ConditionQuery cq = conditions.get(sourceV.schemaLabel().id()); if (cq != null) { sourceV = (HugeVertex) this.graph.vertex(sourceV.id()); - if (!cq.test(sourceV)) { + if (!cq.test(sourceV.element())) { return false; } } @@ -552,7 +552,7 @@ private Boolean validateVertex(Map conditions, cq = conditions.get(targetV.schemaLabel().id()); if (cq != null) { targetV = (HugeVertex) this.graph.vertex(targetV.id()); - return cq.test(targetV); + return cq.test(targetV.element()); } return true; } @@ -565,7 +565,7 @@ private Boolean validateEdge(Map conditions, ConditionQuery cq = conditions.get(edge.schemaLabel().id()); if (cq != null) { - return cq.test(edge); + return cq.test(edge.element()); } return true; } @@ -602,7 +602,9 @@ private void fillFilterBySortKeys(Query query, Id[] edgeLabels, ConditionQuery condQuery = (ConditionQuery) query; if (!GraphTransaction.matchFullEdgeSortKeys(condQuery, this.graph())) { - Id label = condQuery.condition(HugeKeys.LABEL); + // Sort-key validation needs one concrete edge label so that the + // error message points to the exact schema label in use. + Id label = condQuery.conditionValue(HugeKeys.LABEL); E.checkArgument(false, "The properties %s does not match " + "sort keys of edge label '%s'", this.graph().mapPkId2Name(properties.keySet()), diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/JaccardSimilarTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/JaccardSimilarTraverser.java index 296e89ad40..37f639390d 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/JaccardSimilarTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/JaccardSimilarTraverser.java @@ -22,9 +22,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.traversal.algorithm.steps.EdgeStep; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.util.CollectionUtil; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/KneighborTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/KneighborTraverser.java index 2b284b26a0..a9563ee0b0 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/KneighborTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/KneighborTraverser.java @@ -21,8 +21,8 @@ import java.util.function.Consumer; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.traversal.algorithm.records.KneighborRecords; import org.apache.hugegraph.traversal.algorithm.steps.Steps; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/KoutTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/KoutTraverser.java index 8cf3c15b7e..909457be26 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/KoutTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/KoutTraverser.java @@ -22,11 +22,11 @@ import java.util.Set; import java.util.function.Consumer; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.traversal.algorithm.records.KoutRecords; import org.apache.hugegraph.traversal.algorithm.steps.Steps; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/MultiNodeShortestPathTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/MultiNodeShortestPathTraverser.java index 38f899c7f3..846317d438 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/MultiNodeShortestPathTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/MultiNodeShortestPathTraverser.java @@ -26,7 +26,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.algorithm.steps.EdgeStep; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/NeighborRankTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/NeighborRankTraverser.java index 914309a780..8a7149318e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/NeighborRankTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/NeighborRankTraverser.java @@ -23,7 +23,7 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.traversal.algorithm.steps.EdgeStep; import org.apache.hugegraph.type.define.Directions; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/OltpTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/OltpTraverser.java index 44f9cf8692..ec35bf1bab 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/OltpTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/OltpTraverser.java @@ -25,13 +25,13 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import org.apache.commons.lang3.tuple.Pair; +import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.query.EdgesQueryIterator; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.iterator.FilterIterator; @@ -73,7 +73,7 @@ protected OltpTraverser(HugeGraph graph) { @Override public void close() { - // pass + // The graph's thread-local transaction belongs to the caller. } public static void destroy() { @@ -116,7 +116,8 @@ protected long traverseByOne(Iterator iterator, } Consumers consumers = new Consumers<>(executors.getExecutor(), - consumer, null); + consumer, + HugeFactory::closeCurrentThreadTransactions); consumers.start(taskName); long total = 0L; try { @@ -229,7 +230,7 @@ private Consumers> buildConsumers(Consumer> consumer ExecutorService executor) { return new Consumers<>(executor, consumer, - null, + HugeFactory::closeCurrentThreadTransactions, e -> done.set(true), queueSizePerWorker); } @@ -285,7 +286,7 @@ public static class ConcurrentVerticesConsumer implements Consumer { private final Set excluded; private final Set neighbors; private final long limit; - private final AtomicInteger count; + private int count; public ConcurrentVerticesConsumer(Id sourceV, Set excluded, long limit, Set neighbors) { @@ -293,12 +294,13 @@ public ConcurrentVerticesConsumer(Id sourceV, Set excluded, long limit, this.excluded = excluded; this.limit = limit; this.neighbors = neighbors; - this.count = new AtomicInteger(0); + this.count = 0; } @Override - public void accept(EdgeId edgeId) { - if (this.limit != NO_LIMIT && count.get() >= this.limit) { + public synchronized void accept(EdgeId edgeId) { + // Each BFS layer shares a non-thread-safe result set across workers. + if (this.limit != NO_LIMIT && this.count >= this.limit) { throw new Consumers.StopExecution("reach limit"); } @@ -313,7 +315,7 @@ public void accept(EdgeId edgeId) { if (this.neighbors.add(targetV)) { if (this.limit != NO_LIMIT) { - this.count.getAndIncrement(); + this.count++; } } } @@ -333,20 +335,32 @@ public EdgesConsumer(Consumer consumer, long capacity) { @Override public void accept(Iterator edgeIter) { - Iterator ids = prepare(edgeIter); - long counter = 0; - while (ids.hasNext()) { - if (Thread.currentThread().isInterrupted()) { - LOG.warn("Consumer is Interrupted"); - break; + Iterator toClose = edgeIter; + Throwable failure = null; + try { + // Bind worker leases before a caller-created lazy query opens a session. + graph().tx().readWrite(); + Iterator ids = prepare(edgeIter); + toClose = ids != null ? ids : edgeIter; + long counter = 0; + while (ids.hasNext()) { + if (Thread.currentThread().isInterrupted()) { + LOG.warn("Consumer is Interrupted"); + break; + } + counter++; + this.consumer.accept(ids.next()); } - counter++; - this.consumer.accept(ids.next()); - } - long total = edgeIterCounter.addAndGet(counter); - // traverse by batch & improve performance - if (this.capacity != NO_LIMIT && total >= this.capacity) { - throw new Consumers.StopExecution("reach capacity"); + long total = edgeIterCounter.addAndGet(counter); + // traverse by batch & improve performance + if (this.capacity != NO_LIMIT && total >= this.capacity) { + throw new Consumers.StopExecution("reach capacity"); + } + } catch (RuntimeException | Error e) { + failure = e; + throw e; + } finally { + closeIterator(toClose, failure); } } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PathTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PathTraverser.java index 01a075bea7..34ddf54a10 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PathTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PathTraverser.java @@ -26,7 +26,7 @@ import java.util.Set; import java.util.function.BiConsumer; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.traversal.algorithm.HugeTraverser.EdgeRecord; import org.apache.hugegraph.traversal.algorithm.steps.EdgeStep; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PathsTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PathsTraverser.java index 5746533a50..763c6b48fa 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PathsTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PathsTraverser.java @@ -20,7 +20,7 @@ import java.util.Iterator; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.perf.PerfUtil.Watched; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.traversal.algorithm.records.PathsRecords; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PersonalRankTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PersonalRankTraverser.java index 2c3bafa238..cd957e42ab 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PersonalRankTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PersonalRankTraverser.java @@ -24,9 +24,9 @@ import java.util.function.BiFunction; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PredictionTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PredictionTraverser.java index 4b2cb69e57..c7aa916bc4 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PredictionTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/PredictionTraverser.java @@ -20,7 +20,7 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.traversal.algorithm.steps.EdgeStep; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/SameNeighborTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/SameNeighborTraverser.java index af16231015..45257afd22 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/SameNeighborTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/SameNeighborTraverser.java @@ -23,7 +23,7 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.util.CollectionUtil; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/ShortestPathTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/ShortestPathTraverser.java index f69fd7da50..bb0ae2ba87 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/ShortestPathTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/ShortestPathTraverser.java @@ -23,7 +23,7 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.perf.PerfUtil.Watched; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.traversal.algorithm.records.ShortestPathRecords; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/SingleSourceShortestPathTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/SingleSourceShortestPathTraverser.java index 0c67228abf..f751621287 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/SingleSourceShortestPathTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/SingleSourceShortestPathTraverser.java @@ -27,7 +27,7 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.type.define.Directions; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/SubGraphTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/SubGraphTraverser.java index f76f3e3771..07f1892a04 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/SubGraphTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/SubGraphTraverser.java @@ -23,7 +23,7 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/TemplatePathsTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/TemplatePathsTraverser.java index e2e99fb87c..2ad40885c4 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/TemplatePathsTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/TemplatePathsTraverser.java @@ -24,7 +24,7 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.traversal.algorithm.steps.EdgeStep; import org.apache.hugegraph.traversal.algorithm.steps.RepeatEdgeStep; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/iterator/NestedIterator.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/iterator/NestedIterator.java index 09d206e30d..db0109740f 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/iterator/NestedIterator.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/iterator/NestedIterator.java @@ -24,7 +24,7 @@ import java.util.Map; import java.util.Set; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.iterator.WrappedIterator; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/AbstractRecords.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/AbstractRecords.java index 74f3db5ffa..08764eca29 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/AbstractRecords.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/AbstractRecords.java @@ -17,8 +17,8 @@ package org.apache.hugegraph.traversal.algorithm.records; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.perf.PerfUtil.Watched; import org.apache.hugegraph.traversal.algorithm.records.record.Record; import org.apache.hugegraph.traversal.algorithm.records.record.RecordFactory; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/DoubleWayMultiPathsRecords.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/DoubleWayMultiPathsRecords.java index 33224dc92c..a958608411 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/DoubleWayMultiPathsRecords.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/DoubleWayMultiPathsRecords.java @@ -22,7 +22,7 @@ import java.util.Stack; import org.apache.commons.collections.CollectionUtils; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.perf.PerfUtil.Watched; import org.apache.hugegraph.traversal.algorithm.HugeTraverser.Path; import org.apache.hugegraph.traversal.algorithm.HugeTraverser.PathSet; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/KneighborRecords.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/KneighborRecords.java index 2a4e5abb66..89940ce762 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/KneighborRecords.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/KneighborRecords.java @@ -24,7 +24,7 @@ import java.util.Set; import java.util.Stack; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.traversal.algorithm.HugeTraverser.PathSet; import org.apache.hugegraph.traversal.algorithm.records.record.Record; import org.apache.hugegraph.traversal.algorithm.records.record.RecordType; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/KoutRecords.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/KoutRecords.java index bd637ce1d4..1db6e41c2c 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/KoutRecords.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/KoutRecords.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.Stack; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.traversal.algorithm.HugeTraverser.PathSet; import org.apache.hugegraph.traversal.algorithm.records.record.Record; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/PathsRecords.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/PathsRecords.java index 96e04204b9..32bbb2ffe6 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/PathsRecords.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/PathsRecords.java @@ -19,7 +19,7 @@ import java.util.function.Function; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.perf.PerfUtil.Watched; import org.apache.hugegraph.traversal.algorithm.HugeTraverser.PathSet; import org.apache.hugegraph.traversal.algorithm.records.record.RecordType; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/Records.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/Records.java index d7d4e69316..889f3e60df 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/Records.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/Records.java @@ -19,7 +19,7 @@ import java.util.function.Function; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.traversal.algorithm.HugeTraverser.PathSet; public interface Records { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/ShortestPathRecords.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/ShortestPathRecords.java index 61c081cd2d..cb7633fa9a 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/ShortestPathRecords.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/ShortestPathRecords.java @@ -22,7 +22,7 @@ import java.util.Stack; import java.util.function.Function; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.traversal.algorithm.HugeTraverser.Path; import org.apache.hugegraph.traversal.algorithm.HugeTraverser.PathSet; import org.apache.hugegraph.traversal.algorithm.records.record.Int2IntRecord; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/SingleWayMultiPathsRecords.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/SingleWayMultiPathsRecords.java index 7c3a4fd1bb..377b477670 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/SingleWayMultiPathsRecords.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/records/SingleWayMultiPathsRecords.java @@ -23,8 +23,8 @@ import java.util.Stack; import java.util.function.Function; -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.perf.PerfUtil.Watched; import org.apache.hugegraph.traversal.algorithm.HugeTraverser.EdgeRecord; import org.apache.hugegraph.traversal.algorithm.HugeTraverser.Path; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/steps/EdgeStep.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/steps/EdgeStep.java index caff267b7d..859cbfe9b3 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/steps/EdgeStep.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/steps/EdgeStep.java @@ -26,8 +26,8 @@ import java.util.Map; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; import org.apache.hugegraph.traversal.optimize.TraversalUtil; import org.apache.hugegraph.type.define.Directions; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/steps/Steps.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/steps/Steps.java index cc4cdfacf2..7c19b4ac43 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/steps/Steps.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/steps/Steps.java @@ -25,9 +25,9 @@ import java.util.Map; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; import org.apache.hugegraph.traversal.optimize.TraversalUtil; import org.apache.hugegraph.type.define.Directions; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/steps/WeightedEdgeStep.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/steps/WeightedEdgeStep.java index b288420ff2..3385d38720 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/steps/WeightedEdgeStep.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/steps/WeightedEdgeStep.java @@ -25,7 +25,7 @@ import java.util.Map; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.util.E; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/strategy/ConcurrentTraverseStrategy.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/strategy/ConcurrentTraverseStrategy.java index 235a3c2d98..6dd10c21f0 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/strategy/ConcurrentTraverseStrategy.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/strategy/ConcurrentTraverseStrategy.java @@ -24,7 +24,7 @@ import java.util.function.BiConsumer; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.traversal.algorithm.OltpTraverser; import org.apache.hugegraph.traversal.algorithm.steps.EdgeStep; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/strategy/SingleTraverseStrategy.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/strategy/SingleTraverseStrategy.java index 8516645cdd..680da6324d 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/strategy/SingleTraverseStrategy.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/strategy/SingleTraverseStrategy.java @@ -23,7 +23,7 @@ import java.util.function.BiConsumer; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; import org.apache.hugegraph.traversal.algorithm.OltpTraverser; import org.apache.hugegraph.traversal.algorithm.steps.EdgeStep; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/strategy/TraverseStrategy.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/strategy/TraverseStrategy.java index 7440133038..819d9a8f6c 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/strategy/TraverseStrategy.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/strategy/TraverseStrategy.java @@ -23,7 +23,7 @@ import java.util.function.BiConsumer; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.traversal.algorithm.HugeTraverser; import org.apache.hugegraph.traversal.algorithm.steps.EdgeStep; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/ConditionP.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/ConditionP.java index e41a0df706..387bfab0fc 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/ConditionP.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/ConditionP.java @@ -17,16 +17,23 @@ package org.apache.hugegraph.traversal.optimize; -import java.util.function.BiPredicate; - -import org.apache.hugegraph.backend.query.Condition; +import org.apache.hugegraph.query.Condition; import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate; +/** + * A HugeGraph-local predicate used by server-side traversal processing. + * + * This type relies on HugeGraph {@link Condition.RelationType} predicates and + * has no registered GraphSON or GraphBinary wire serializer. Remote clients + * should use supported TinkerPop predicates or server-side query APIs instead + * of sending {@code ConditionP} instances directly. + */ public class ConditionP extends P { private static final long serialVersionUID = 9094970577400072902L; - private ConditionP(final BiPredicate predicate, + private ConditionP(final PBiPredicate predicate, Object value) { super(predicate, value); } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java index 403bf5be83..f12a84e40e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java @@ -55,12 +55,18 @@ public boolean equals(Object obj) { HugeCountStep other = (HugeCountStep) obj; return Objects.equals(this.originGraphStep, - other.originGraphStep) && this.done == other.done; + other.originGraphStep); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), this.originGraphStep, this.done); + return Objects.hash(super.hashCode(), this.originGraphStep); + } + + @Override + public void reset() { + super.reset(); + this.done = false; } @Override diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java index ef380d36b2..2765427861 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.Set; -import org.apache.hugegraph.backend.query.Aggregate; +import org.apache.hugegraph.query.Aggregate; import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy.ProviderOptimizationStrategy; @@ -30,11 +30,9 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountGlobalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.NoOpBarrierStep; -import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AggregateGlobalStep; -import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AggregateLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AggregateStep; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.IdentityStep; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.SideEffectStep; -import org.apache.tinkerpop.gremlin.process.traversal.step.util.CollectingBarrierStep; import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy; import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper; import org.apache.tinkerpop.gremlin.structure.Element; @@ -73,16 +71,16 @@ public void apply(Traversal.Admin traversal) { HugeGraphStep graphStep = null; Step step = originStep; do { + // Collecting barriers can filter inputs (for example, unproductive order().by()). + // Only skip steps that preserve the number of traversers. if (!(step instanceof CountGlobalStep || step instanceof GraphStep || step instanceof IdentityStep || - step instanceof NoOpBarrierStep || - step instanceof CollectingBarrierStep) || + step instanceof NoOpBarrierStep) || (step instanceof TraversalParent && TraversalHelper.anyStepRecursively(s -> { return s instanceof SideEffectStep || - s instanceof AggregateGlobalStep || - s instanceof AggregateLocalStep; + s instanceof AggregateStep; }, (TraversalParent) step))) { return; } @@ -94,7 +92,9 @@ public void apply(Traversal.Admin traversal) { step = step.getPreviousStep(); } while (step != null); - if (graphStep == null) { + // A mid-traversal scan runs once per incoming traverser (including bulk). + if (graphStep == null || !graphStep.isStartStep() || + traversal.getStartStep() != graphStep) { return; } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStrategy.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStrategy.java index c3a1542f87..e880b360ed 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStrategy.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStrategy.java @@ -20,16 +20,16 @@ import java.util.Collection; import java.util.Collections; -import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.BiPredicate; import org.apache.tinkerpop.gremlin.process.traversal.Compare; import org.apache.tinkerpop.gremlin.process.traversal.Contains; +import org.apache.tinkerpop.gremlin.process.traversal.NotP; import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate; import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy; @@ -41,6 +41,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.filter.IsStep; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.NotStep; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStepContract; import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountGlobalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchStep; @@ -62,13 +63,18 @@ public final class HugeCountStrategy extends AbstractTraversalStrategy implements TraversalStrategy.OptimizationStrategy { - private static final Map RANGE_PREDICATES = - new HashMap() {{ + private static final Map RANGE_PREDICATES = + new HashMap() {{ put(Contains.within, 1L); - put(Contains.without, 0L); + put(Contains.without, 1L); }}; - private static final Set INCREASED_OFFSET_SCALAR_PREDICATES = - EnumSet.of(Compare.eq, Compare.neq, Compare.lte, Compare.gt); + private static final Set> + INCREASED_OFFSET_SCALAR_PREDICATES = + Set.of(Compare.eq, Compare.neq, Compare.lte, Compare.gt, + new NotP.NotPBiPredicate<>(Compare.eq), + new NotP.NotPBiPredicate<>(Compare.neq), + new NotP.NotPBiPredicate<>(Compare.lte), + new NotP.NotPBiPredicate<>(Compare.gt)); private static final HugeCountStrategy INSTANCE = new HugeCountStrategy(); @@ -99,7 +105,7 @@ public void apply(final Traversal.Admin traversal) { ((ConnectiveP) isStepPredicate).getPredicates() : Collections.singletonList(isStepPredicate)) { final Object value = p.getValue(); - final BiPredicate predicate = p.getBiPredicate(); + final PBiPredicate predicate = p.getBiPredicate(); if (value instanceof Number) { final long highRangeOffset = INCREASED_OFFSET_SCALAR_PREDICATES.contains(predicate) ? @@ -181,11 +187,11 @@ public void apply(final Traversal.Admin traversal) { } /* - * HugeGraph extracts RangeGlobalStep into backend queries. A - * negative upper bound is never useful for count(), and would - * become an invalid backend range like [0, -3). + * HugeGraph extracts RangeGlobalStep into backend queries. + * Non-positive upper bounds cannot preserve count(). A zero + * bound from without(-1) would replace a nonempty count with 0. */ - if (highRange != null && highRange < 0L) { + if (highRange != null && highRange <= 0L) { highRange = null; } @@ -273,12 +279,12 @@ public void apply(final Traversal.Admin traversal) { private boolean doStrategy(final Step step) { if (!(step instanceof CountGlobalStep) || !(step.getNextStep() instanceof IsStep) || - step.getPreviousStep() instanceof RangeGlobalStep) { + step.getPreviousStep() instanceof RangeGlobalStepContract) { return false; } final P predicate = ((IsStep) step.getNextStep()).getPredicate(); - if (this.hasNestedConnectivePredicate(predicate)) { + if (!this.supportsCountPredicate(predicate)) { return false; } @@ -289,16 +295,65 @@ private boolean doStrategy(final Step step) { .getMatchKey().isPresent()); } - private boolean hasNestedConnectivePredicate(P predicate) { - if (!(predicate instanceof ConnectiveP)) { + private boolean supportsCountPredicate(P predicate) { + if (predicate instanceof ConnectiveP) { + for (P child : ((ConnectiveP) predicate).getPredicates()) { + // The bound calculation only handles a flat AND/OR. Every + // branch must be safe: ignoring one can change the truth value. + if (child instanceof ConnectiveP || + !this.supportsCountPredicate(child)) { + return false; + } + } + return !((ConnectiveP) predicate).getPredicates().isEmpty(); + } + + final PBiPredicate biPredicate = predicate.getBiPredicate(); + final Object value = predicate.getValue(); + if (value instanceof Number) { + // Bound derivation uses doubleValue() and ceil(). Restrict it to + // standard number types and the range where adjacent integer + // counts remain distinguishable in the predicate's numeric type. + if (!(value instanceof Byte || value instanceof Short || + value instanceof Integer || value instanceof Long || + value instanceof Float || value instanceof Double)) { + return false; + } + final double bound = ((Number) value).doubleValue(); + final double exactIntegerLimit = value instanceof Float ? + 16777215D : 9007199254740991D; + if (!Double.isFinite(bound) || Math.abs(bound) > exactIntegerLimit) { + return false; + } + for (Compare compare : Compare.values()) { + if (biPredicate.equals(compare) || + biPredicate.equals(new NotP.NotPBiPredicate<>(compare))) { + return true; + } + } return false; } - for (P child : ((ConnectiveP) predicate).getPredicates()) { - if (child instanceof ConnectiveP) { - return true; + if (!(value instanceof Collection) || + !RANGE_PREDICATES.containsKey(biPredicate)) { + // In particular, NotP(collection) and custom predicates have no + // proven bound. Keep the complete count/is traversal for them. + return false; + } + final Collection values = (Collection) value; + if (values.isEmpty()) { + return false; + } + Class numberType = null; + for (Object item : values) { + if (!(item instanceof Byte || item instanceof Short || + item instanceof Integer || item instanceof Long) || + ((Number) item).longValue() == Long.MAX_VALUE || + (numberType != null && numberType != item.getClass())) { + return false; } + numberType = item.getClass(); } - return false; + return true; } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java index bdfb9e0b66..9e8b407fee 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java @@ -24,8 +24,8 @@ import java.util.List; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.Log; @@ -37,7 +37,7 @@ import org.slf4j.Logger; public final class HugeGraphStep - extends GraphStep implements QueryHolder { + extends GraphStep implements QueryHolder { private static final long serialVersionUID = -679873894532085972L; @@ -210,8 +210,7 @@ public boolean equals(Object obj) { HugeGraphStep other = (HugeGraphStep) obj; return this.hasContainers.equals(other.hasContainers) && - this.queryInfo.equals(other.queryInfo) && - this.lastTimeResults.equals(other.lastTimeResults); + this.queryInfo.equals(other.queryInfo); } @Override diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugePrimaryKeyStrategy.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugePrimaryKeyStrategy.java index f371c0f303..32291b30ff 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugePrimaryKeyStrategy.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugePrimaryKeyStrategy.java @@ -19,18 +19,16 @@ import java.util.LinkedList; import java.util.List; -import java.util.Map; import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy; import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy.ProviderOptimizationStrategy; -import org.apache.tinkerpop.gremlin.process.traversal.step.Mutating; import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexStartStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexStepContract; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AddPropertyStep; import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy; -import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality; public class HugePrimaryKeyStrategy @@ -48,17 +46,17 @@ public static HugePrimaryKeyStrategy instance() { public void apply(Traversal.Admin traversal) { List removeSteps = new LinkedList<>(); - Mutating curAddStep = null; + AddVertexStepContract curAddStep = null; List stepList = traversal.getSteps(); for (int i = 0, s = stepList.size(); i < s; i++) { Step step = stepList.get(i); if (i == 0 && step instanceof AddVertexStartStep) { - curAddStep = (Mutating) step; + curAddStep = (AddVertexStepContract) step; continue; - } else if (curAddStep == null && (step) instanceof AddVertexStep) { - curAddStep = (Mutating) step; + } else if (curAddStep == null && step instanceof AddVertexStep) { + curAddStep = (AddVertexStepContract) step; continue; } @@ -71,34 +69,17 @@ public void apply(Traversal.Admin traversal) { continue; } - AddPropertyStep propertyStep = (AddPropertyStep) step; + AddPropertyStep propertyStep = (AddPropertyStep) step; if (propertyStep.getCardinality() == Cardinality.single || propertyStep.getCardinality() == null) { - Object[] kvs = new Object[2]; - boolean extraParams = false; - - for (Map.Entry> param : - propertyStep.getParameters().getRaw().entrySet()) { - Object paramKey = param.getKey(); - if (T.key.equals(paramKey)) { - kvs[0] = param.getValue().get(0); - } else if (T.value.equals(paramKey)) { - kvs[1] = param.getValue().get(0); - } else { - extraParams = true; - } - } - - curAddStep.configure(kvs); - - if (extraParams) { + curAddStep.addProperty(propertyStep.getKey(), + propertyStep.getValue()); + if (!propertyStep.getProperties().isEmpty()) { /* - * Rejecting here would run for every child traversal, so an - * unreachable coalesce()/choose() branch would fail too. Leave - * the step for HugeVertex.property() to reject when it really - * runs, and stop folding across it. + * Keep metadata for HugeVertex.property() to reject only + * when this branch executes, including in coalesce/choose. */ curAddStep = null; } else { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeScriptTraversal.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeScriptTraversal.java index 0ccb48a54a..4f9233211f 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeScriptTraversal.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeScriptTraversal.java @@ -25,7 +25,7 @@ import javax.script.ScriptEngine; import javax.script.ScriptException; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.tinkerpop.gremlin.jsr223.SingleGremlinScriptEngineManager; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeVertexStep.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeVertexStep.java index bd2e1388c8..1912c7b574 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeVertexStep.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeVertexStep.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.traversal.optimize; +import org.apache.hugegraph.util.TinkerPopUtil; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -24,12 +26,12 @@ import java.util.List; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.tx.GraphTransaction; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.util.Log; import org.apache.tinkerpop.gremlin.process.traversal.Traverser; @@ -42,7 +44,7 @@ import org.slf4j.Logger; public class HugeVertexStep - extends VertexStep implements QueryHolder { + extends VertexStep implements QueryHolder { private static final long serialVersionUID = -7850636388424382454L; @@ -132,7 +134,7 @@ protected ConditionQuery constructEdgesQuery( boolean withVertexCond = this.withVertexCondition(); Id vertex = (Id) traverser.get().id(); - Directions direction = Directions.convert(this.getDirection()); + Directions direction = TinkerPopUtil.direction(this.getDirection()); EdgeLabel[] els = graph.mapElName2El(this.getEdgeLabels()); LOG.debug("HugeVertexStep.edges(): vertex={}, direction={}, " + diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeVertexStepByBatch.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeVertexStepByBatch.java index 62d6a2b921..a6fce4682d 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeVertexStepByBatch.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeVertexStepByBatch.java @@ -20,9 +20,9 @@ import java.util.Iterator; import java.util.List; -import org.apache.hugegraph.backend.query.BatchConditionQuery; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.BatchConditionQuery; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.iterator.BatchMapperIterator; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.HugeKeys; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/QueryHolder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/QueryHolder.java index 917f777b95..10934d749f 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/QueryHolder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/QueryHolder.java @@ -19,13 +19,14 @@ import java.util.Iterator; -import org.apache.hugegraph.backend.query.Aggregate; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Aggregate; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.iterator.Metadatable; import org.apache.tinkerpop.gremlin.process.traversal.Order; import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder; -public interface QueryHolder extends HasContainerHolder, Metadatable { +public interface QueryHolder + extends HasContainerHolder, Metadatable { String SYSPROP_PAGE = "~page"; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/Text.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/Text.java index fc1d06cce4..508eac70fd 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/Text.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/Text.java @@ -17,8 +17,8 @@ package org.apache.hugegraph.traversal.optimize; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; public class Text { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java index e6a56027a1..f51d2b7b0e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java @@ -19,32 +19,40 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.UUID; import java.util.function.BiPredicate; import java.util.function.Function; +import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.hugegraph.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.Id; import org.apache.hugegraph.backend.page.PageInfo; import org.apache.hugegraph.backend.page.PageState; -import org.apache.hugegraph.backend.query.Aggregate; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.exception.NotSupportException; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.iterator.FilterIterator; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaLabel; +import org.apache.hugegraph.query.Aggregate; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeElement; import org.apache.hugegraph.structure.HugeProperty; import org.apache.hugegraph.type.HugeType; @@ -56,31 +64,79 @@ import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.JsonUtil; import org.apache.tinkerpop.gremlin.process.traversal.Compare; +import org.apache.tinkerpop.gremlin.process.traversal.CompareType; import org.apache.tinkerpop.gremlin.process.traversal.Contains; +import org.apache.tinkerpop.gremlin.process.traversal.NotP; import org.apache.tinkerpop.gremlin.process.traversal.Order; import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate; import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; +import org.apache.tinkerpop.gremlin.process.traversal.Traverser; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder; import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.AndStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.ClassFilterStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.CoinStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.DedupGlobalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.FilterStep; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.IsStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.NoneStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.NotStep; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.OrStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.PathFilterStep; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.SampleGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TailGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TimeLimitStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TraversalFilterStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.ConstantStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.DedupLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.ElementMapStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.FoldStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.GroupCountStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.GroupStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.IdStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.IndexStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.LabelStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.MaxGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.MaxLocalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.MeanGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.MeanLocalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.MinGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.MinLocalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.NoOpBarrierStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.OrderGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.OrderLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.ProjectStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.PropertiesStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.PropertyKeyStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.PropertyMapStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.PropertyValueStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.RangeLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.SampleLocalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.SumGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.SumLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.TailLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.UnfoldStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.VertexStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AggregateStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.GroupCountSideEffectStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.GroupSideEffectStep; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.IdentityStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.ProfileSideEffectStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.SubgraphStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.TraversalSideEffectStep; import org.apache.tinkerpop.gremlin.process.traversal.step.util.ElementValueComparator; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.EmptyStep; import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.ProfileStep; import org.apache.tinkerpop.gremlin.process.traversal.step.util.ReducingBarrierStep; import org.apache.tinkerpop.gremlin.process.traversal.util.AndP; import org.apache.tinkerpop.gremlin.process.traversal.util.ConnectiveP; @@ -96,6 +152,7 @@ import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; public final class TraversalUtil { @@ -104,6 +161,34 @@ public final class TraversalUtil { public static final String P_CALL = "P."; + // Standard filters, current-value projections/reductions (including local + // collection variants), and side effects that return the current traverser. + // Exact classes keep extension subclasses conservative. History/state readers + // (select/path/tree, sack/cap, labeled where/match), repeat/branches, mutations, + // lambdas, custom folds and unknown steps are deliberately not admitted. + private static final Set> CURRENT_ELEMENT_SUFFIX_STEPS = ImmutableSet.of( + VertexStep.class, HugeVertexStep.class, HugeVertexStepByBatch.class, + PropertiesStep.class, HasStep.class, LocalContainsStep.class, + AndStep.class, OrStep.class, NotStep.class, TraversalFilterStep.class, + IsStep.class, ClassFilterStep.class, NoneStep.class, CoinStep.class, + RangeGlobalStep.class, TailGlobalStep.class, SampleGlobalStep.class, + DedupGlobalStep.class, PathFilterStep.class, TimeLimitStep.class, + NoOpBarrierStep.class, IdentityStep.class, + IdStep.class, LabelStep.class, PropertyKeyStep.class, PropertyValueStep.class, + PropertyMapStep.class, ElementMapStep.class, ProjectStep.class, ConstantStep.class, + FoldStep.class, UnfoldStep.class, IndexStep.class, + GroupStep.class, GroupCountStep.class, + OrderGlobalStep.class, OrderLocalStep.class, RangeLocalStep.class, TailLocalStep.class, + DedupLocalStep.class, SampleLocalStep.class, + CountGlobalStep.class, SumGlobalStep.class, MinGlobalStep.class, + MaxGlobalStep.class, MeanGlobalStep.class, + CountLocalStep.class, SumLocalStep.class, MinLocalStep.class, + MaxLocalStep.class, MeanLocalStep.class, + AggregateStep.class, + GroupSideEffectStep.class, GroupCountSideEffectStep.class, + TraversalSideEffectStep.class, + SubgraphStep.class, ProfileStep.class, ProfileSideEffectStep.class); + public static HugeGraph getGraph(Step step) { HugeGraph graph = tryGetGraph(step); if (graph != null) { @@ -173,11 +258,16 @@ public static void trySetGraph(Step step, HugeGraph graph) { public static void extractHasContainer(HugeGraphStep newStep, Traversal.Admin traversal) { + if (hasUnsafeLabelInTraversal(traversal, newStep)) { + prepareLocalHasContainers(newStep, traversal); + return; + } Step step = newStep.getNextStep(); while (step instanceof HasStep || step instanceof NoOpBarrierStep) { Step nextStep = step.getNextStep(); if (step instanceof HasStep) { - HasContainerHolder holder = (HasContainerHolder) step; + HasContainerHolder holder = + (HasContainerHolder) step; boolean connectiveLabelStep = removeConnectiveLabelStep(step); /* @@ -260,13 +350,8 @@ private static boolean followedByMatchStep(Step step) { return null; } - List labels = new ArrayList<>(); - for (Traversal.Admin child : orStep.getLocalChildren()) { - if (!collectPositiveLabelValues(child, labels)) { - return null; - } - } - if (labels.isEmpty()) { + List labels = positiveLabelValuesOrNull(orStep); + if (labels == null) { return null; } @@ -294,6 +379,16 @@ private static OrStep positiveLabelOnlyOrStepAfter(Step step) { return (OrStep) next; } + private static List positiveLabelValuesOrNull(OrStep orStep) { + List labels = new ArrayList<>(); + for (Traversal.Admin child : orStep.getLocalChildren()) { + if (!collectPositiveLabelValues(child, labels)) { + return null; + } + } + return labels.isEmpty() ? null : labels; + } + private static boolean collectPositiveLabelValues( Traversal.Admin traversal, List labels) { if (traversal.getSteps().size() != 1) { @@ -318,7 +413,7 @@ private static boolean collectPositiveLabelValues( private static void addPositiveLabelValues(HasContainer has, List labels) { P predicate = has.getPredicate(); - BiPredicate bp = predicate.getBiPredicate(); + PBiPredicate bp = predicate.getBiPredicate(); if (bp == Compare.eq) { labels.add(predicate.getValue()); } else { @@ -328,7 +423,7 @@ private static void addPositiveLabelValues(HasContainer has, } private static boolean hasLabelAfterUnusablePredicate(HugeGraphStep step, - HasContainerHolder holder) { + HasContainerHolder holder) { HugeGraph graph = tryGetGraph(step); boolean seenUnusablePredicate = false; for (HasContainer has : holder.getHasContainers()) { @@ -344,7 +439,7 @@ private static boolean hasLabelAfterUnusablePredicate(HugeGraphStep step, } private static boolean hasUnsupportedLabelContainer( - HasContainerHolder holder) { + HasContainerHolder holder) { for (HasContainer has : holder.getHasContainers()) { if (isLabelContainer(has) && !isPositiveLabelContainer(has)) { return true; @@ -366,7 +461,7 @@ private static boolean removeConnectiveLabelStep(Step step) { } private static List extractLabelHasContainers( - HugeGraphStep step, HasContainerHolder holder) { + HugeGraphStep step, HasContainerHolder holder) { List extracted = new ArrayList<>(); for (HasContainer has : holder.getHasContainers()) { if (!isPositiveLabelContainer(has)) { @@ -385,12 +480,12 @@ private static boolean isLabelContainer(HasContainer has) { } static boolean isPositiveLabelContainer(HasContainer has) { - if (!isLabelContainer(has)) { + if (!isLabelContainer(has) || hasNullLabelValue(has)) { return false; } P predicate = has.getPredicate(); - BiPredicate bp = predicate.getBiPredicate(); + PBiPredicate bp = predicate.getBiPredicate(); if (bp == Compare.eq) { return true; } @@ -404,7 +499,7 @@ static boolean isPositiveLabelContainer(HasContainer has) { } private static boolean hasMatchIndexSensitivePredicate( - HasContainerHolder holder) { + HasContainerHolder holder) { for (HasContainer has : holder.getHasContainers()) { if (hasMatchIndexSensitivePredicate(has)) { return true; @@ -414,7 +509,7 @@ private static boolean hasMatchIndexSensitivePredicate( } private static boolean hasUnusableMatchPredicate(HugeGraphStep step, - HasContainerHolder holder) { + HasContainerHolder holder) { HugeGraph graph = tryGetGraph(step); for (HasContainer has : holder.getHasContainers()) { if (!hasMatchIndexSensitivePredicate(has)) { @@ -428,7 +523,7 @@ private static boolean hasUnusableMatchPredicate(HugeGraphStep step, } private static List extractUsableHasContainers( - HugeGraphStep step, HasContainerHolder holder) { + HugeGraphStep step, HasContainerHolder holder) { List extracted = new ArrayList<>(); HugeGraph graph = tryGetGraph(step); for (HasContainer has : holder.getHasContainers()) { @@ -454,7 +549,7 @@ private static boolean hasMatchIndexSensitivePredicate(HasContainer has) { List> predicates = new ArrayList<>(); collectPredicates(predicates, ImmutableList.of(has.getPredicate())); for (P pred : predicates) { - BiPredicate bp = pred.getBiPredicate(); + PBiPredicate bp = pred.getBiPredicate(); if (bp == Compare.neq || bp == Compare.gt || bp == Compare.gte || bp == Compare.lt || bp == Compare.lte) { @@ -541,6 +636,29 @@ private static boolean hasNullPredicate(HasContainer has) { return false; } + private static boolean hasNullLabelValue(HasContainer has) { + if (!isLabelContainer(has)) { + return false; + } + + List> predicates = new ArrayList<>(); + collectPredicates(predicates, ImmutableList.of(has.getPredicate())); + for (P pred : predicates) { + Object value = pred.getValue(); + if (value == null) { + return true; + } + if (value instanceof Collection) { + for (Object label : (Collection) value) { + if (label == null) { + return true; + } + } + } + } + return false; + } + private static boolean hasBooleanIndex(HugeGraph graph, SchemaLabel schemaLabel, PropertyKey pkey) { @@ -591,7 +709,7 @@ private static boolean hasOnlyRangePredicates(HasContainer has) { List> predicates = new ArrayList<>(); collectPredicates(predicates, ImmutableList.of(has.getPredicate())); for (P pred : predicates) { - BiPredicate bp = pred.getBiPredicate(); + PBiPredicate bp = pred.getBiPredicate(); if (bp != Compare.gt && bp != Compare.gte && bp != Compare.lt && bp != Compare.lte) { return false; @@ -602,12 +720,17 @@ private static boolean hasOnlyRangePredicates(HasContainer has) { public static void extractHasContainer(HugeVertexStep newStep, Traversal.Admin traversal) { + if (hasUnsafeLabelInTraversal(traversal, newStep)) { + prepareLocalHasContainers(newStep, traversal); + return; + } Step step = newStep; do { Step nextStep = step.getNextStep(); if (step instanceof HasStep) { removeConnectiveLabelStep(step); - HasContainerHolder holder = (HasContainerHolder) step; + HasContainerHolder holder = + (HasContainerHolder) step; if (extractHasContainers(newStep, holder)) { TraversalHelper.copyLabels(step, step.getPreviousStep(), false); traversal.removeStep(step); @@ -618,7 +741,7 @@ public static void extractHasContainer(HugeVertexStep newStep, } private static boolean extractHasContainers(HugeGraphStep newStep, - HasContainerHolder holder) { + HasContainerHolder holder) { HugeGraph graph = TraversalUtil.tryGetGraph(newStep); if (!canExtractHasContainers(graph, holder)) { return false; @@ -632,7 +755,7 @@ private static boolean extractHasContainers(HugeGraphStep newStep, } private static boolean extractHasContainers(HugeVertexStep newStep, - HasContainerHolder holder) { + HasContainerHolder holder) { HugeGraph graph = TraversalUtil.tryGetGraph(newStep); if (!canExtractHasContainers(graph, holder)) { return false; @@ -644,8 +767,13 @@ private static boolean extractHasContainers(HugeVertexStep newStep, } private static boolean canExtractHasContainers(HugeGraph graph, - HasContainerHolder holder) { - for (HasContainer has : holder.getHasContainers()) { + HasContainerHolder holder) { + // Keep unsafe labels and their sibling properties for local filtering. + if (hasUnsafeLabelPredicate(holder)) { + return false; + } + List hasContainers = holder.getHasContainers(); + for (HasContainer has : hasContainers) { if (!canExtractHasContainer(graph, has)) { return false; } @@ -653,8 +781,523 @@ private static boolean canExtractHasContainers(HugeGraph graph, return true; } + private static void prepareLocalHasContainers( + Step source, Traversal.Admin traversal) { + QueryHolder query = (QueryHolder) source; + Step step = source.getNextStep(); + while (step instanceof HasStep || step instanceof NoOpBarrierStep) { + Step next = step.getNextStep(); + if (step instanceof HasStep) { + HasContainerHolder holder = (HasContainerHolder) step; + for (HasContainer has : new ArrayList<>(holder.getHasContainers())) { + // Paging is query metadata, never an element property filter. + if (QueryHolder.SYSPROP_PAGE.equals(has.getKey())) { + query.addHasContainer(has); + holder.removeHasContainer(has); + continue; + } + if (T.id.getAccessor().equals(has.getKey())) { + // ID lookup is complete across labels. Existing source + // IDs and unsupported predicates still filter locally. + if (source instanceof HugeGraphStep && + GraphStep.processHasContainerIds((HugeGraphStep) source, has)) { + holder.removeHasContainer(has); + continue; + } + holder.removeHasContainer(has); + holder.addHasContainer(new LocalIdHasContainer(localIdPredicate(has.getPredicate()))); + continue; + } + if (T.label.getAccessor().equals(has.getKey())) { + holder.removeHasContainer(has); + HasContainer label = new LocalLabelHasContainer(has.getPredicate()); + if (source instanceof HugeGraphStep && + canPushPositiveLabel((HugeGraphStep) source, has)) { + // A positive label conjunct uses the label index, + // independent of per-label property index coverage. + // Keep the runtime label matcher for source-ID queries. + query.addHasContainer(label); + } else { + // Keep unsupported candidates and adjacent-vertex + // labels local, including their paging boundary. + holder.addHasContainer(label); + } + continue; + } + if (keyForContainsKey(has.getKey()) || keyForContainsValue(has.getKey())) { + // Backend CONTAINS support varies and source IDs use + // local filtering too. Preserve HugeGraph's map query + // semantics without treating "key"/"value" as names. + holder.removeHasContainer(has); + traversal.addStep(traversal.getSteps().indexOf(step), + new LocalContainsStep<>(traversal, has)); + continue; + } + if (containsSearchPredicate(has.getPredicate())) { + // Child traversals can be unbound during optimization; + // the matcher resolves the graph from each runtime element. + holder.removeHasContainer(has); + holder.addHasContainer(new LocalSearchHasContainer( + has.getKey(), has.getPredicate())); + } + } + if (holder.getHasContainers().isEmpty()) { + TraversalHelper.copyLabels(step, step.getPreviousStep(), false); + traversal.removeStep(step); + } + } + step = next; + } + if (query.queryInfo().paging() && step instanceof RangeGlobalStep) { + // Bound the raw backend page even when local filters prevent normal + // range extraction. Keep the range step to apply offset/limit after + // those filters; a filtered page may contain fewer results. + query.setRange(0, ((RangeGlobalStep) step).getHighRange()); + } + } + + private static boolean canPushPositiveLabel(HugeGraphStep source, + HasContainer has) { + if (!isEqInLabelPredicate(has)) { + return false; + } + HugeGraph graph = tryGetGraph(source); + if (graph == null) { + return false; + } + List> predicates = new ArrayList<>(); + collectPredicates(predicates, ImmutableList.of(has.getPredicate())); + for (P predicate : predicates) { + Object value = predicate.getValue(); + BiPredicate bp = predicate.getBiPredicate(); + if (bp == Contains.within && !(value instanceof Collection)) { + return false; + } + Collection values = bp == Contains.within ? + (Collection) value : Collections.singletonList(value); + for (Object candidate : values) { + if (!hasLabelIndex(graph, source.returnsVertex(), candidate)) { + return false; + } + } + } + return true; + } + + private static boolean hasLabelIndex(HugeGraph graph, boolean vertex, Object value) { + if (value instanceof Number) { + value = IdGenerator.of(((Number) value).longValue()); + } + try { + SchemaLabel label; + if (value instanceof Id) { + Id id = (Id) value; + // Nonpositive IDs include internal schema objects that aren't + // user labels. Preserve their local matching behavior. + if (!id.number() || id.asLong() <= 0L) { + return false; + } + label = vertex ? graph.vertexLabel(id) : graph.edgeLabel(id); + } else if (value instanceof String) { + label = vertex ? graph.vertexLabel((String) value) : graph.edgeLabel((String) value); + } else { + return false; + } + // Do not turn a working local filter into a missing-label/index error. + return label != null && label.enableLabelIndex(); + } catch (IllegalArgumentException | NotFoundException e) { + return false; + } + } + + private static P localIdPredicate(P predicate) { + // Keep IDs local to preserve source-ID intersections and step ordering. + // UUID/Element values need the same representation as HugeElement.id(). Leave + // strings unchanged so HasContainer retains its string-ID comparison. + P copy = copyResolvedPredicate(predicate); + List> leaves = new ArrayList<>(); + collectPredicates(leaves, ImmutableList.of(copy)); + for (P leaf : leaves) { + Object value = leaf.getValue(); + if (value instanceof Collection) { + List values = new ArrayList<>(); + for (Object item : (Collection) value) { + values.add(localIdValue(item)); + } + leaf.setValue(values); + } else { + leaf.setValue(localIdValue(value)); + } + } + return copy; + } + + private static Object localIdValue(Object value) { + if (value instanceof UUID || value instanceof Element) { + return HugeElement.getIdValue(HugeType.VERTEX, value); + } + return value; + } + + private static final class LocalLabelHasContainer extends HasContainer { + + private static final long serialVersionUID = 1L; + + private LocalLabelHasContainer(P predicate) { + super(T.label.getAccessor(), copyResolvedPredicate(predicate)); + } + + @Override + protected boolean testLabel(Element element) { + // Resolve against the actual element, not a graph captured while + // strategies run. Child traversals may be unbound or later cloned. + return testLabelPredicate(this.getPredicate(), ((HugeElement) element).schemaLabel()); + } + } + + private static final class LocalContainsStep extends HasStep { + + private static final long serialVersionUID = 1L; + + private LocalContainsStep(Traversal.Admin traversal, HasContainer has) { + super(traversal, has.clone()); + E.checkArgument(has.getPredicate().getBiPredicate() == Compare.eq, + "CONTAINS query with relation '%s' is not supported", + has.getPredicate().getBiPredicate()); + } + + @Override + protected boolean filter(Traverser.Admin traverser) { + HugeElement element = (HugeElement) traverser.get(); + // Adjacent vertices can be ID/label-only shells. Like properties(), + // load their properties before evaluating the system property map. + element.getFilledProperties(); + // Keep a HasStep boundary so count/range cannot bypass this filter. + // Resolve schema from the runtime element; clone/reset must not + // retain a graph or transaction captured during optimization. + for (HasContainer has : this.getHasContainers()) { + boolean matches = keyForContainsKey(has.getKey()) || keyForContainsValue(has.getKey()) ? + convContains2Relation(element.graph(), has).test(element.element()) : + has.test(element); + if (!matches) { + return false; + } + } + return true; + } + } + + @SuppressWarnings("unchecked") + private static boolean testLabelPredicate(P predicate, SchemaLabel label) { + if (predicate instanceof NotP) { + return !testLabelPredicate(((NotP) predicate).negate(), label); + } + if (predicate instanceof ConnectiveP) { + boolean and = predicate instanceof AndP; + for (P child : ((ConnectiveP) predicate).getPredicates()) { + if (testLabelPredicate(child, label) != and) { + return !and; + } + } + return and; + } + BiPredicate bp = predicate.getBiPredicate(); + Object value = predicate.getValue(); + if (bp == Contains.within || bp == Contains.without) { + for (Object item : (Collection) value) { + if (testLabelValue(Compare.eq, label, item)) { + return bp == Contains.within; + } + } + return bp == Contains.without; + } + return testLabelValue((BiPredicate) bp, label, value); + } + + private static boolean testLabelValue(BiPredicate predicate, + SchemaLabel label, Object value) { + if (value instanceof Number) { + value = IdGenerator.of(((Number) value).longValue()); + } + return predicate.test(value instanceof Id ? label.id() : label.name(), value); + } + + private static final class LocalSearchHasContainer extends HasContainer { + + private static final long serialVersionUID = 1L; + + private transient HugeGraph matcherGraph; + private transient P matcherPredicate; + private transient Predicate matcher; + + private LocalSearchHasContainer(String key, P predicate) { + super(key, copyResolvedPredicate(predicate)); + } + + @Override + protected boolean testValue(Property property) { + // Keep the public P tree intact for strategies, hashing and Java + // serialization. Resolve the analyzer from the element's graph, + // including after cloning, deserialization or graph rebinding. + HugeGraph graph = (HugeGraph) property.element().graph(); + if (this.matcherGraph != graph || + !samePredicateValues(this.getPredicate(), this.matcherPredicate)) { + P predicate = copyResolvedPredicate(this.getPredicate()); + this.matcher = localSearchMatcher(predicate, graph); + this.matcherPredicate = predicate; + this.matcherGraph = graph; + } + return this.matcher.test(property.value()); + } + + @Override + public LocalSearchHasContainer clone() { + LocalSearchHasContainer clone = (LocalSearchHasContainer) super.clone(); + clone.matcherGraph = null; + clone.matcherPredicate = null; + clone.matcher = null; + return clone; + } + } + + private static boolean samePredicateValues(P current, P cached) { + // P.equals() compares originalValue, not the value changed by setValue(). + if (cached == null || current.getClass() != cached.getClass()) { + return false; + } + if (current instanceof NotP) { + return samePredicateValues(((NotP) current).negate(), ((NotP) cached).negate()); + } + if (current instanceof ConnectiveP) { + List> children = ((ConnectiveP) current).getPredicates(); + List> oldChildren = ((ConnectiveP) cached).getPredicates(); + if (children.size() != oldChildren.size()) { + return false; + } + for (int i = 0; i < children.size(); i++) { + if (!samePredicateValues(children.get(i), oldChildren.get(i))) { + return false; + } + } + return true; + } + return current.getBiPredicate().equals(cached.getBiPredicate()) && + Objects.equals(current.getValue(), cached.getValue()); + } + + private static boolean containsSearchPredicate(P predicate) { + if (predicate instanceof NotP) { + return containsSearchPredicate(((NotP) predicate).negate()); + } + if (predicate instanceof ConnectiveP) { + for (P child : ((ConnectiveP) predicate).getPredicates()) { + if (containsSearchPredicate(child)) { + return true; + } + } + return false; + } + return predicate.getBiPredicate() == Condition.RelationType.TEXT_CONTAINS; + } + + @SuppressWarnings("unchecked") + private static Predicate localSearchMatcher(P predicate, HugeGraph graph) { + if (predicate instanceof NotP) { + Predicate child = localSearchMatcher(((NotP) predicate).negate(), graph); + return value -> !child.test(value); + } + if (predicate instanceof ConnectiveP) { + List> children = new ArrayList<>(); + for (P child : ((ConnectiveP) predicate).getPredicates()) { + children.add(localSearchMatcher(child, graph)); + } + boolean and = predicate instanceof AndP; + return value -> { + for (Predicate child : children) { + if (child.test(value) != and) { + return !and; + } + } + return and; + }; + } + if (predicate.getBiPredicate() != Condition.RelationType.TEXT_CONTAINS) { + return (P) predicate; + } + // Match SEARCH terms in place, preserving range and side-effect ordering. + return graph.searchPredicate((String) predicate.getValue()); + } + + private static boolean hasUnsafeLabelInTraversal( + Traversal.Admin traversal, Step sourceStep) { + // Partial pushdown can lose candidates before local label filtering. + // Scan the remaining traversal, its children and each ancestor's + // remaining steps for filters on child output. Arbitrary extension + // steps don't reliably expose element identity, so stay conservative. + // FIXME(#3201): Restore selective pushdown when every candidate schema label + // has compatible index coverage for extracted property predicates. + // Outside proven root and child suffixes, negative labels can disable + // property pushdown even across element changes (including ancestors + // and unknown extension steps), potentially requiring a full scan. + List steps = traversal.getSteps(); + int start = 0; + while (start < steps.size() && steps.get(start) != sourceStep) { + start++; + } + start++; + for (int i = start; i < steps.size(); i++) { + Step step = steps.get(i); + if (changesCurrentElement(step) && + traversal.getParent() instanceof EmptyStep && + onlyCurrentElementSuffix(steps.subList(i, steps.size()))) { + return false; + } + if (step instanceof HasStep) { + HasContainerHolder holder = (HasContainerHolder) step; + if (hasUnsafeLabelPredicate(holder)) { + return true; + } + } + if (hasUnsafeLabelInChildren(step)) { + return true; + } + } + TraversalParent parent = traversal.getParent(); + if (parent instanceof Step && !(parent instanceof EmptyStep)) { + Step parentStep = (Step) parent; + // RepeatStep's until/emit siblings can filter this child's output. + // This helper only descends, so revisiting the owning step's children + // cannot recurse back into this ancestor walk. + if (hasUnsafeLabelInChildren(parentStep)) { + return true; + } + return hasUnsafeLabelInTraversal(parentStep.getTraversal(), parentStep); + } + return false; + } + + private static boolean changesCurrentElement(Step step) { + // Vertex adjacency is a scope boundary for this label fallback, not + // proof that a self-loop or later hop cannot return the source. + // Preserve the source property query's existing index/error semantics; + // complete cross-label property candidates are tracked in #3201. + // Edge endpoints can recover the source (outE().outV(), inE().inV(), + // bothV()). Do not admit EdgeVertexStep here or in the suffix allowlist: + // checking only that step is too late if outE() already ended the scan. + return step instanceof VertexStep || step instanceof PropertiesStep; + } + + private static boolean onlyCurrentElementSuffix(List steps) { + for (Step step : steps) { + // Check both child traversals and hidden history/lambda inputs. + // dedup("a") reads step labels even without a select() child; + // fold(seed, function) can run arbitrary code unlike list fold(). + if (!CURRENT_ELEMENT_SUFFIX_STEPS.contains(step.getClass()) || + step instanceof DedupGlobalStep && + !((DedupGlobalStep) step).getScopeKeys().isEmpty() || + step instanceof FoldStep && !((FoldStep) step).isListFold()) { + return false; + } + if (step instanceof TraversalParent) { + TraversalParent parent = (TraversalParent) step; + for (Traversal.Admin child : parent.getLocalChildren()) { + if (!onlyCurrentElementSuffix(child.getSteps())) { + return false; + } + } + for (Traversal.Admin child : parent.getGlobalChildren()) { + if (!onlyCurrentElementSuffix(child.getSteps())) { + return false; + } + } + } + } + return true; + } + + private static boolean hasUnsafeLabelInChildren(Step step) { + return hasUnsafeLabelInChildren(step, false); + } + + private static boolean hasUnsafeLabelInChildren(Step step, boolean negated) { + if (!(step instanceof TraversalParent)) { + return false; + } + TraversalParent parent = (TraversalParent) step; + // Even an EQ label under not() describes a complement, whose property + // index coverage is unknown. Stay conservative for nested negations too. + negated |= step instanceof NotStep; + for (Traversal.Admin child : parent.getLocalChildren()) { + if (hasUnsafeLabelInChildTraversal(child, negated)) { + return true; + } + } + for (Traversal.Admin child : parent.getGlobalChildren()) { + if (hasUnsafeLabelInChildTraversal(child, negated)) { + return true; + } + } + return false; + } + + private static boolean hasUnsafeLabelInChildTraversal( + Traversal.Admin traversal, boolean negated) { + List steps = traversal.getSteps(); + for (int i = 0; i < steps.size(); i++) { + Step childStep = steps.get(i); + // A child may filter a different element without constraining the + // source label. Only stop when the suffix cannot recover the source + // through select/path, repeat, or an unknown extension step. + if (changesCurrentElement(childStep) && + onlyCurrentElementSuffix(steps.subList(i, steps.size()))) { + return false; + } + if (childStep instanceof HasStep && + hasUnsafeLabelPredicate((HasContainerHolder) childStep, negated)) { + return true; + } + if (hasUnsafeLabelInChildren(childStep, negated)) { + return true; + } + } + return false; + } + + private static boolean hasUnsafeLabelPredicate(HasContainerHolder holder) { + return hasUnsafeLabelPredicate(holder, false); + } + + private static boolean hasUnsafeLabelPredicate(HasContainerHolder holder, boolean negated) { + for (HasContainer has : holder.getHasContainers()) { + if (T.label.getAccessor().equals(has.getKey()) && + (negated || !isEqInLabelPredicate(has))) { + return true; + } + } + return false; + } + + private static boolean isEqInLabelPredicate(HasContainer has) { + if (has.getPredicate() == null) { + return false; + } + List> predicates = new ArrayList<>(); + collectPredicates(predicates, ImmutableList.of(has.getPredicate())); + for (P predicate : predicates) { + BiPredicate bp = predicate.getBiPredicate(); + if (bp != Compare.eq && bp != Contains.within) { + return false; + } + } + return true; + } + static boolean canExtractHasContainer(HugeGraph graph, HasContainer has) { + if (has.getKey() == null || has.getPredicate() == null || + hasNullLabelValue(has) || !hasSupportedPredicate(has)) { + return false; + } if (isSysProp(has.getKey())) { return true; } @@ -678,7 +1321,7 @@ static boolean canExtractHasContainer(HugeGraph graph, List> predicates = new ArrayList<>(); collectPredicates(predicates, ImmutableList.of(has.getPredicate())); for (P pred : predicates) { - BiPredicate bp = pred.getBiPredicate(); + PBiPredicate bp = pred.getBiPredicate(); if (bp == Compare.gt || bp == Compare.gte || bp == Compare.lt || bp == Compare.lte) { return false; @@ -687,6 +1330,22 @@ static boolean canExtractHasContainer(HugeGraph graph, return true; } + private static boolean hasSupportedPredicate(HasContainer has) { + List> predicates = new ArrayList<>(); + collectPredicates(predicates, ImmutableList.of(has.getPredicate())); + for (P predicate : predicates) { + // Keep this set aligned with convHas2Condition(). Other predicates + // (including NotP, TextP and typeOf) must execute in HasStep. + PBiPredicate bp = predicate.getBiPredicate(); + if (!(bp instanceof Compare) && + !(bp instanceof Contains) && + !(bp instanceof Condition.RelationType)) { + return false; + } + } + return true; + } + public static void extractOrder(Step newStep, Traversal.Admin traversal) { Step step = newStep; @@ -840,7 +1499,7 @@ public static void fillConditionQuery(ConditionQuery query, public static Condition convHas2Condition(HasContainer has, HugeType type, HugeGraph graph) { P p = has.getPredicate(); E.checkArgument(p != null, "The predicate of has(%s) is null", has); - BiPredicate bp = p.getBiPredicate(); + PBiPredicate bp = p.getBiPredicate(); Condition condition; if (keyForContainsKeyOrValue(has.getKey())) { condition = convContains2Relation(graph, has); @@ -913,7 +1572,7 @@ private static Condition convCompare2Relation(HugeGraph graph, HugeType type, HasContainer has) { assert type.isGraph(); - BiPredicate bp = has.getPredicate().getBiPredicate(); + PBiPredicate bp = has.getPredicate().getBiPredicate(); assert bp instanceof Compare; return isSysProp(has.getKey()) ? @@ -924,7 +1583,7 @@ private static Condition convCompare2Relation(HugeGraph graph, private static Condition.Relation convCompare2SyspropRelation(HugeGraph graph, HugeType type, HasContainer has) { - BiPredicate bp = has.getPredicate().getBiPredicate(); + PBiPredicate bp = has.getPredicate().getBiPredicate(); assert bp instanceof Compare; HugeKeys key = token2HugeKey(has.getKey()); @@ -952,7 +1611,7 @@ private static Condition.Relation convCompare2SyspropRelation(HugeGraph graph, private static Condition convCompare2UserpropRelation(HugeGraph graph, HugeType type, HasContainer has) { - BiPredicate bp = has.getPredicate().getBiPredicate(); + PBiPredicate bp = has.getPredicate().getBiPredicate(); assert bp instanceof Compare; String key = has.getKey(); @@ -1012,7 +1671,7 @@ private static Condition convRelationType2Relation(HugeGraph graph, HugeType type, HasContainer has) { assert type.isGraph(); - BiPredicate bp = has.getPredicate().getBiPredicate(); + PBiPredicate bp = has.getPredicate().getBiPredicate(); assert bp instanceof Condition.RelationType; String key = has.getKey(); @@ -1025,7 +1684,7 @@ private static Condition convRelationType2Relation(HugeGraph graph, public static Condition convIn2Relation(HugeGraph graph, HugeType type, HasContainer has) { - BiPredicate bp = has.getPredicate().getBiPredicate(); + PBiPredicate bp = has.getPredicate().getBiPredicate(); assert bp instanceof Contains; Collection values = (Collection) has.getValue(); @@ -1068,7 +1727,7 @@ public static Condition convIn2Relation(HugeGraph graph, public static Condition convContains2Relation(HugeGraph graph, HasContainer has) { // Convert contains-key or contains-value - BiPredicate bp = has.getPredicate().getBiPredicate(); + PBiPredicate bp = has.getPredicate().getBiPredicate(); E.checkArgument(bp == Compare.eq, "CONTAINS query with relation " + "'%s' is not supported", bp); @@ -1097,6 +1756,9 @@ public static HugeKeys string2HugeKey(String key) { } public static HugeKeys token2HugeKey(String key) { + if (key == null) { + return null; + } if (key.equals(T.label.getAccessor())) { return HugeKeys.LABEL; } else if (key.equals(T.id.getAccessor())) { @@ -1128,7 +1790,18 @@ public static Iterator filterResult( return (Iterator) iterator; } Iterator result = new FilterIterator<>(iterator, elem -> { - return HasContainer.testAll(elem, hasContainers); + for (HasContainer has : hasContainers) { + // Explicit-ID queries bypass backend label conversion. Keep the + // same name/Id/number semantics as backend and fallback queries. + boolean matches = T.label.getAccessor().equals(has.getKey()) && + elem instanceof HugeElement ? + testLabelPredicate(has.getPredicate(), + ((HugeElement) elem).schemaLabel()) : has.test(elem); + if (!matches) { + return false; + } + } + return true; }); return (Iterator) result; } @@ -1178,16 +1851,154 @@ public static void convAllHasSteps(Traversal.Admin traversal) { } public static void convHasStep(HugeGraph graph, HasStep step) { - HasContainerHolder holder = step; - for (HasContainer has : holder.getHasContainers()) { - convPredicateValue(graph, has); + HasContainerHolder holder = step; + List containers = new ArrayList<>(holder.getHasContainers()); + // Use the existing extraction contract to identify a whole local HasStep. + boolean localIds = !canExtractHasContainers(graph, holder); + for (int i = 0; i < containers.size(); i++) { + HasContainer original = containers.get(i); + // P.clone() shares named bindings in TP 3.8. Resolve into a fresh + // execution predicate before conversion so setValue() cannot clear + // the caller's bindings or those of a previously cloned traversal. + P predicate = copyResolvedPredicate(original.getPredicate()); + HasContainer has; + if (original instanceof LocalSearchHasContainer) { + has = new LocalSearchHasContainer(original.getKey(), predicate); + } else if (original instanceof LocalLabelHasContainer) { + has = new LocalLabelHasContainer(predicate); + } else if (original instanceof LocalIdHasContainer || + localIds && T.id.getAccessor().equals(original.getKey())) { + updateLocalIdPredicate(predicate); + has = new LocalIdHasContainer(predicate); + } else { + has = new HasContainer(original.getKey(), predicate); + convPredicateValue(graph, has); + } + containers.set(i, has); + } + for (HasContainer has : new ArrayList<>(holder.getHasContainers())) { + holder.removeHasContainer(has); + } + for (HasContainer has : containers) { + holder.addHasContainer(has); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static P copyResolvedPredicate(P predicate) { + if (predicate == null) { + return null; + } + if (predicate instanceof NotP) { + return new NotP(copyResolvedPredicate(((NotP) predicate).negate())); + } + if (predicate instanceof ConnectiveP) { + List> children = new ArrayList<>(); + for (P child : ((ConnectiveP) predicate).getPredicates()) { + children.add((P) copyResolvedPredicate(child)); + } + return predicate instanceof AndP ? new AndP<>(children) : new OrP<>(children); + } + if (!predicate.isParameterized()) { + // Preserve TextP and custom literal predicate implementations. + // Their empty bindings can be shared safely: setValue() replaces + // the clone's literals without changing the original's values. + return predicate.clone(); + } + Object value = predicate.getValue(); + if (value instanceof Collection) { + value = new ArrayList<>((Collection) value); + } + return new P((PBiPredicate) predicate.getBiPredicate(), value); + } + + private static final class LocalIdHasContainer extends HasContainer { + + private LocalIdHasContainer(P predicate) { + super(T.id.getAccessor(), predicate); + } + + @Override + protected boolean testId(Element element) { + return testIdPredicate(this.getPredicate(), element); + } + + @Override + protected boolean testIdAsString(Element element) { + return testIdPredicate(this.getPredicate(), element); + } + + private static boolean testIdPredicate(P predicate, Element element) { + if (predicate instanceof NotP) { + return !testIdPredicate(((NotP) predicate).negate(), element); + } + if (predicate instanceof ConnectiveP) { + boolean conjunction = predicate instanceof AndP; + for (P child : ((ConnectiveP) predicate).getPredicates()) { + if (testIdPredicate(child, element) != conjunction) { + return !conjunction; + } + } + return conjunction; + } + if (predicate.getBiPredicate() instanceof Contains) { + Collection values = (Collection) predicate.getValue(); + boolean stringIds = values.stream().allMatch(value -> value == null || value instanceof String); + if (!stringIds) { + // Mixed collections use typed IDs. Keep numeric operands as + // Numbers so Compare.eq() does not truncate fractional IDs. + Object id = element.id(); + for (Object value : values) { + Object actual = id instanceof IdGenerator.StringId && value instanceof String ? + ((Id) id).asString() : id; + // Edge lookup accepts its serialized ID as well as EdgeId. + // Preserve that contract in local mixed-ID filters without + // collapsing numeric and string vertex IDs. + Object expected = id instanceof EdgeId && + value instanceof String ? HugeEdge.getIdValue(value, true) : value; + if (Compare.eq.test(actual, expected)) { + return predicate.getBiPredicate() == Contains.within; + } + } + return predicate.getBiPredicate() == Contains.without; + } + } + // Pure String collections use the standard string-ID mode. + // Numeric, custom and type predicates retain their native operand. + return new HasContainer(T.id.getAccessor(), predicate).test(element); + } + } + + @SuppressWarnings("unchecked") + private static void updateLocalIdPredicate(P predicate) { + if (predicate instanceof NotP) { + // negate() exposes NotP's child without removing the wrapper. + updateLocalIdPredicate(((NotP) predicate).negate()); + } else if (predicate instanceof ConnectiveP) { + for (P child : ((ConnectiveP) predicate).getPredicates()) { + updateLocalIdPredicate(child); + } + } else if (predicate.getBiPredicate() instanceof Compare || + predicate.getBiPredicate() instanceof Contains) { + P leaf = (P) predicate; + Object value = leaf.getValue(); + if (value instanceof UUID) { + leaf.setValue(IdGenerator.of((UUID) value)); + } else if (value instanceof Collection) { + List values = new ArrayList<>(); + for (Object item : (Collection) value) { + values.add(item instanceof UUID ? + IdGenerator.of((UUID) item) : item); + } + leaf.setValue(values); + } } } private static void convPredicateValue(HugeGraph graph, HasContainer has) { // No need to convert if key is sys-prop - if (isSysProp(has.getKey())) { + if (has.getKey() == null || isSysProp(has.getKey())) { return; } PropertyKey pkey = graph.propertyKey(has.getKey()); @@ -1198,8 +2009,16 @@ private static void updatePredicateValue(P predicate, PropertyKey pkey) { List> leafPredicates = new ArrayList<>(); collectPredicates(leafPredicates, ImmutableList.of(predicate)); for (P pred : leafPredicates) { - if (pred.getBiPredicate() == Compare.neq && - pred.getValue() == null) { + if (isNullInequalityPredicate(pred)) { + continue; + } + if (pred instanceof NotP) { + updatePredicateValue(((NotP) pred).negate(), pkey); + continue; + } + if (pred.getBiPredicate() instanceof CompareType) { + // GType, Class and type-name operands describe the value's + // type; they are not values of the property's schema type. continue; } Object value = validPropertyValue(pred.getValue(), pkey); @@ -1207,6 +2026,19 @@ private static void updatePredicateValue(P predicate, PropertyKey pkey) { } } + private static boolean isNullInequalityPredicate(P predicate) { + if (predicate.getValue() != null) { + return false; + } + if (predicate.getBiPredicate() == Compare.neq) { + return true; + } + if (!(predicate instanceof NotP)) { + return false; + } + return ((NotP) predicate).negate().getBiPredicate() == Compare.eq; + } + private static boolean isSysProp(String key) { if (QueryHolder.SYSPROP_PAGE.equals(key)) { return true; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/HugeType.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/HugeType.java deleted file mode 100644 index 122036ae38..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/HugeType.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.hugegraph.type.define.SerialEnum; - -public enum HugeType implements SerialEnum { - - UNKNOWN(0, "UNKNOWN"), - - /* Schema types */ - VERTEX_LABEL(1, "VL"), - EDGE_LABEL(2, "EL"), - PROPERTY_KEY(3, "PK"), - INDEX_LABEL(4, "IL"), - - META(40, "M"), - COUNTER(50, "C"), - - /* Data types */ - VERTEX(101, "V"), - // System meta - SYS_PROPERTY(102, "S"), - // Property - PROPERTY(103, "U"), - // Vertex aggregate property - AGGR_PROPERTY_V(104, "VP"), - // Edge aggregate property - AGGR_PROPERTY_E(105, "EP"), - // Olap property - OLAP(106, "AP"), - // Edge - EDGE(120, "E"), - // Edge's direction is OUT for the specified vertex - EDGE_OUT(130, "O"), - // Edge's direction is IN for the specified vertex - EDGE_IN(140, "I"), - - SECONDARY_INDEX(150, "SI"), - VERTEX_LABEL_INDEX(151, "VI"), - EDGE_LABEL_INDEX(152, "EI"), - RANGE_INT_INDEX(160, "II"), - RANGE_FLOAT_INDEX(161, "FI"), - RANGE_LONG_INDEX(162, "LI"), - RANGE_DOUBLE_INDEX(163, "DI"), - SEARCH_INDEX(170, "AI"), - SHARD_INDEX(175, "HI"), - UNIQUE_INDEX(178, "UI"), - - TASK(180, "T"), - SERVER(181, "SERVER"), - - VARIABLE(185, "VA"), - - KV_TYPE(200, "KV"), - KV_RAW(201, "KVR"), - - // System schema - SYS_SCHEMA(250, "SS"), - - MAX_TYPE(255, "~"); - - private byte type = 0; - private final String name; - - private static final Map ALL_NAME = new HashMap<>(); - - static { - SerialEnum.register(HugeType.class); - for (HugeType type : values()) { - ALL_NAME.put(type.name, type); - } - } - - HugeType(int type, String name) { - assert type < 256; - this.type = (byte) type; - this.name = name; - } - - @Override - public byte code() { - return this.type; - } - - public String string() { - return this.name; - } - - public String readableName() { - return this.name().replace('_', ' ').toLowerCase(); - } - - public boolean isSchema() { - return this == HugeType.VERTEX_LABEL || - this == HugeType.EDGE_LABEL || - this == HugeType.PROPERTY_KEY || - this == HugeType.INDEX_LABEL; - } - - public boolean isGraph() { - return this.isVertex() || this.isEdge(); - } - - public boolean isVertex() { - return this == HugeType.VERTEX || this == HugeType.TASK || this == HugeType.SERVER || - this == HugeType.VARIABLE; - } - - public boolean isEdge() { - return this == EDGE || this == EDGE_OUT || this == EDGE_IN; - } - - public boolean isEdgeLabel() { - return this == EDGE_LABEL; - } - - public boolean isIndex() { - return this == VERTEX_LABEL_INDEX || this == EDGE_LABEL_INDEX || - this == SECONDARY_INDEX || this == SEARCH_INDEX || - this == RANGE_INT_INDEX || this == RANGE_FLOAT_INDEX || - this == RANGE_LONG_INDEX || this == RANGE_DOUBLE_INDEX || - this == SHARD_INDEX || this == UNIQUE_INDEX; - } - - public boolean isLabelIndex() { - return this == VERTEX_LABEL_INDEX || this == EDGE_LABEL_INDEX; - } - - public boolean isStringIndex() { - return this == VERTEX_LABEL_INDEX || this == EDGE_LABEL_INDEX || - this == SECONDARY_INDEX || this == SEARCH_INDEX || - this == SHARD_INDEX || this == UNIQUE_INDEX; - } - - public boolean isNumericIndex() { - return this == RANGE_INT_INDEX || this == RANGE_FLOAT_INDEX || - this == RANGE_LONG_INDEX || this == RANGE_DOUBLE_INDEX || - this == SHARD_INDEX; - } - - public boolean isSecondaryIndex() { - return this == VERTEX_LABEL_INDEX || this == EDGE_LABEL_INDEX || - this == SECONDARY_INDEX; - } - - public boolean isSearchIndex() { - return this == SEARCH_INDEX; - } - - public boolean isRangeIndex() { - return this == RANGE_INT_INDEX || this == RANGE_FLOAT_INDEX || - this == RANGE_LONG_INDEX || this == RANGE_DOUBLE_INDEX; - } - - public boolean isRange4Index() { - return this == RANGE_INT_INDEX || this == RANGE_FLOAT_INDEX; - } - - public boolean isRange8Index() { - return this == RANGE_LONG_INDEX || this == RANGE_DOUBLE_INDEX; - } - - public boolean isShardIndex() { - return this == SHARD_INDEX; - } - - public boolean isUniqueIndex() { - return this == UNIQUE_INDEX; - } - - public boolean isVertexAggregateProperty() { - return this == AGGR_PROPERTY_V; - } - - public boolean isEdgeAggregateProperty() { - return this == AGGR_PROPERTY_E; - } - - public boolean isAggregateProperty() { - return this.isVertexAggregateProperty() || - this.isEdgeAggregateProperty(); - } - - public static HugeType fromString(String type) { - return ALL_NAME.get(type); - } - - public static HugeType fromCode(byte code) { - return SerialEnum.fromCode(HugeType.class, code); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Idfiable.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Idfiable.java deleted file mode 100644 index 865ad50ad0..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Idfiable.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type; - -import org.apache.hugegraph.backend.id.Id; - -public interface Idfiable { - - Id id(); -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Indexable.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Indexable.java deleted file mode 100644 index ff3330a567..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Indexable.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type; - -import java.util.Set; - -import org.apache.hugegraph.backend.id.Id; - -public interface Indexable { - - Set indexLabels(); -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Nameable.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Nameable.java deleted file mode 100644 index cf8e71e6d7..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Nameable.java +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2017 JanusGraph 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. - -package org.apache.hugegraph.type; - -/** - * Represents an entity that can be uniquely identified by a String name. - * - * @author Matthias Broecheler (me@matthiasb.com) - */ -public interface Nameable { - - /** - * Returns the unique name of this entity. - * - * @return Name of this entity. - */ - String name(); - -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Propertiable.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Propertiable.java deleted file mode 100644 index e26a356bf6..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Propertiable.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type; - -import java.util.Set; - -import org.apache.hugegraph.backend.id.Id; - -public interface Propertiable { - - Set properties(); -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Typeable.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Typeable.java deleted file mode 100644 index 95a1a11d49..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Typeable.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type; - -public interface Typeable { - - // Return schema/data type - HugeType type(); -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Action.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Action.java deleted file mode 100644 index 9561d52181..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Action.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type.define; - -public enum Action implements SerialEnum { - - INSERT(1, "insert"), - - APPEND(2, "append"), - - ELIMINATE(3, "eliminate"), - - DELETE(4, "delete"), - - UPDATE_IF_PRESENT(5, "update_if_present"), - - UPDATE_IF_ABSENT(6, "update_if_absent"); - - private final byte code; - private final String name; - - static { - SerialEnum.register(Action.class); - } - - Action(int code, String name) { - assert code < 256; - this.code = (byte) code; - this.name = name; - } - - @Override - public byte code() { - return this.code; - } - - public String string() { - return this.name; - } - - public static Action fromCode(byte code) { - switch (code) { - case 1: - return INSERT; - case 2: - return APPEND; - case 3: - return ELIMINATE; - case 4: - return DELETE; - case 5: - return UPDATE_IF_PRESENT; - case 6: - return UPDATE_IF_ABSENT; - default: - throw new AssertionError("Unsupported action code: " + code); - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/AggregateType.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/AggregateType.java deleted file mode 100644 index 31c0f6212d..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/AggregateType.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type.define; - -public enum AggregateType implements SerialEnum { - - NONE(0, "none"), - MAX(1, "max"), - MIN(2, "min"), - SUM(3, "sum"), - OLD(4, "old"), - SET(5, "set"), - LIST(6, "list"); - - private final byte code; - private final String name; - - static { - SerialEnum.register(AggregateType.class); - } - - AggregateType(int code, String name) { - assert code < 256; - this.code = (byte) code; - this.name = name; - } - - @Override - public byte code() { - return this.code; - } - - public String string() { - return this.name; - } - - public boolean isNone() { - return this == NONE; - } - - public boolean isMax() { - return this == MAX; - } - - public boolean isMin() { - return this == MIN; - } - - public boolean isSum() { - return this == SUM; - } - - public boolean isNumber() { - return this.isMax() || this.isMin() || this.isSum(); - } - - public boolean isOld() { - return this == OLD; - } - - public boolean isSet() { - return this == SET; - } - - public boolean isList() { - return this == LIST; - } - - public boolean isUnion() { - return this == SET || this == LIST; - } - - public boolean isIndexable() { - return this == NONE || this == MAX || this == MIN || this == OLD; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Cardinality.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Cardinality.java deleted file mode 100644 index e9d6b59bf6..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Cardinality.java +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2017 JanusGraph 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. - -package org.apache.hugegraph.type.define; - -import org.apache.tinkerpop.gremlin.structure.VertexProperty; - -/** - * The cardinality of the values associated with given key for a particular element. - * - * @author Matthias Broecheler (me@matthiasb.com) - */ -public enum Cardinality implements SerialEnum { - - /** - * Only a single value may be associated with the given key. - */ - SINGLE(1, "single"), - - /** - * Multiple values and duplicate values may be associated with the given - * key. - */ - LIST(2, "list"), - - /** - * Multiple but distinct values may be associated with the given key. - */ - SET(3, "set"); - - private final byte code; - private final String name; - - static { - SerialEnum.register(Cardinality.class); - } - - Cardinality(int code, String name) { - assert code < 256; - this.code = (byte) code; - this.name = name; - } - - @Override - public byte code() { - return this.code; - } - - public String string() { - return this.name; - } - - public boolean single() { - return this == SINGLE; - } - - public boolean multiple() { - return this == LIST || this == SET; - } - - public static Cardinality convert(VertexProperty.Cardinality cardinality) { - switch (cardinality) { - case single: - return SINGLE; - case list: - return LIST; - case set: - return SET; - default: - throw new AssertionError(String.format("Unrecognized cardinality: '%s'", - cardinality)); - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/CollectionType.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/CollectionType.java deleted file mode 100644 index e8a4cd3547..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/CollectionType.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type.define; - -public enum CollectionType implements SerialEnum { - - // Java Collection Framework - JCF(1, "jcf"), - - // Eclipse Collection - EC(2, "ec"), - - // FastUtil - FU(3, "fu"); - - private final byte code; - private final String name; - - static { - SerialEnum.register(CollectionType.class); - } - - CollectionType(int code, String name) { - assert code < 256; - this.code = (byte) code; - this.name = name; - } - - @Override - public byte code() { - return this.code; - } - - public String string() { - return this.name; - } - - public static CollectionType fromCode(byte code) { - switch (code) { - case 1: - return JCF; - case 2: - return EC; - case 3: - return FU; - default: - throw new AssertionError( - "Unsupported collection code: " + code); - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/DataType.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/DataType.java deleted file mode 100644 index 2bfa93e7d7..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/DataType.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type.define; - -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.serializer.BytesBuffer; -import org.apache.hugegraph.util.Blob; -import org.apache.hugegraph.util.Bytes; -import org.apache.hugegraph.util.DateUtil; -import org.apache.hugegraph.util.JsonUtil; -import org.apache.hugegraph.util.StringEncoding; - -public enum DataType implements SerialEnum { - - UNKNOWN(0, "unknown", Object.class), - OBJECT(1, "object", Object.class), - BOOLEAN(2, "boolean", Boolean.class), - BYTE(3, "byte", Byte.class), - INT(4, "int", Integer.class), - LONG(5, "long", Long.class), - FLOAT(6, "float", Float.class), - DOUBLE(7, "double", Double.class), - TEXT(8, "text", String.class), - BLOB(9, "blob", Blob.class), - DATE(10, "date", Date.class), - UUID(11, "uuid", UUID.class); - - private final byte code; - private final String name; - private final Class clazz; - - static { - SerialEnum.register(DataType.class); - } - - DataType(int code, String name, Class clazz) { - assert code < 256; - this.code = (byte) code; - this.name = name; - this.clazz = clazz; - } - - @Override - public byte code() { - return this.code; - } - - public String string() { - return this.name; - } - - public Class clazz() { - return this.clazz; - } - - public boolean isText() { - return this == DataType.TEXT; - } - - public boolean isNumber() { - return this == BYTE || this == INT || this == LONG || - this == FLOAT || this == DOUBLE; - } - - public boolean isNumber4() { - // Store index value of Byte using 4 bytes - return this == BYTE || this == INT || this == FLOAT; - } - - public boolean isNumber8() { - return this == LONG || this == DOUBLE; - } - - public boolean isBlob() { - return this == DataType.BLOB; - } - - public boolean isDate() { - return this == DataType.DATE; - } - - public boolean isUUID() { - return this == DataType.UUID; - } - - public Number valueToNumber(V value) { - if (!(this.isNumber() && value instanceof Number) && - !JsonUtil.isInfinityOrNaN(value)) { - return null; - } - if (this.clazz.isInstance(value)) { - return (Number) value; - } - - Number number; - try { - switch (this) { - case BYTE: - number = Byte.valueOf(value.toString()); - break; - case INT: - number = Integer.valueOf(value.toString()); - break; - case LONG: - number = Long.valueOf(value.toString()); - break; - case FLOAT: - number = Float.valueOf(value.toString()); - break; - case DOUBLE: - number = Double.valueOf(value.toString()); - break; - default: - throw new AssertionError(String.format( - "Number type only contains Byte, Integer, " + - "Long, Float, Double, but got %s", this.clazz())); - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException(String.format( - "Can't read '%s' as %s: %s", - value, this.name, e.getMessage())); - } - return number; - } - - public Date valueToDate(V value) { - if (!this.isDate()) { - return null; - } - if (value instanceof Date) { - return (Date) value; - } else if (value instanceof Integer) { - return new Date(((Number) value).intValue()); - } else if (value instanceof Long) { - return new Date(((Number) value).longValue()); - } else if (value instanceof String) { - return DateUtil.parse((String) value); - } - return null; - } - - public UUID valueToUUID(V value) { - if (!this.isUUID()) { - return null; - } - if (value instanceof UUID) { - return (UUID) value; - } else if (value instanceof String) { - return StringEncoding.uuid((String) value); - } - return null; - } - - public Blob valueToBlob(V value) { - if (!this.isBlob()) { - return null; - } - if (value instanceof Blob) { - return (Blob) value; - } else if (value instanceof byte[]) { - return Blob.wrap((byte[]) value); - } else if (value instanceof ByteBuffer) { - return Blob.wrap(((ByteBuffer) value).array()); - } else if (value instanceof BytesBuffer) { - return Blob.wrap(((BytesBuffer) value).bytes()); - } else if (value instanceof String) { - // Only base64 string or hex string accepted - String str = ((String) value); - if (str.startsWith("0x")) { - return Blob.wrap(Bytes.fromHex(str.substring(2))); - } - return Blob.wrap(StringEncoding.decodeBase64(str)); - } else if (value instanceof List) { - List values = (List) value; - byte[] bytes = new byte[values.size()]; - for (int i = 0; i < bytes.length; i++) { - Object v = values.get(i); - if (v instanceof Byte || v instanceof Integer) { - bytes[i] = ((Number) v).byteValue(); - } else { - throw new IllegalArgumentException(String.format( - "expect byte or int value, but got '%s'", v)); - } - } - return Blob.wrap(bytes); - } - return null; - } - - public static DataType fromClass(Class clazz) { - for (DataType type : DataType.values()) { - if (type.clazz() == clazz) { - return type; - } - } - throw new HugeException("Unknown clazz '%s' for DataType", clazz); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Directions.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Directions.java deleted file mode 100644 index 1309cd89df..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Directions.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type.define; - -import org.apache.hugegraph.type.HugeType; -import org.apache.tinkerpop.gremlin.structure.Direction; - -public enum Directions implements SerialEnum { - - // TODO: add NONE enum for non-directional edges - - BOTH(0, "both"), - - OUT(1, "out"), - - IN(2, "in"); - - private byte code = 0; - private String name = null; - - static { - SerialEnum.register(Directions.class); - } - - Directions(int code, String name) { - assert code < 256; - this.code = (byte) code; - this.name = name; - } - - @Override - public byte code() { - return this.code; - } - - public String string() { - return this.name; - } - - public HugeType type() { - switch (this) { - case OUT: - return HugeType.EDGE_OUT; - case IN: - return HugeType.EDGE_IN; - default: - throw new IllegalArgumentException(String.format( - "Can't convert direction '%s' to HugeType", this)); - } - } - - public Directions opposite() { - if (this.equals(OUT)) { - return IN; - } else { - return this.equals(IN) ? OUT : BOTH; - } - } - - public Direction direction() { - switch (this) { - case OUT: - return Direction.OUT; - case IN: - return Direction.IN; - case BOTH: - return Direction.BOTH; - default: - throw new AssertionError(String.format( - "Unrecognized direction: '%s'", this)); - } - } - - public static Directions convert(Direction direction) { - switch (direction) { - case OUT: - return OUT; - case IN: - return IN; - case BOTH: - return BOTH; - default: - throw new AssertionError(String.format( - "Unrecognized direction: '%s'", direction)); - } - } - - public static Directions convert(HugeType edgeType) { - switch (edgeType) { - case EDGE_OUT: - return OUT; - case EDGE_IN: - return IN; - default: - throw new IllegalArgumentException(String.format( - "Can't convert type '%s' to Direction", edgeType)); - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/EdgeLabelType.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/EdgeLabelType.java deleted file mode 100644 index 2857c6e3b3..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/EdgeLabelType.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -package org.apache.hugegraph.type.define; - -public enum EdgeLabelType implements SerialEnum { - - NORMAL(1, "NORMAL"), - - PARENT(2, "PARENT"), - - SUB(3, "SUB"), - ; - - static { - SerialEnum.register(EdgeLabelType.class); - } - - private final byte code; - private final String name; - - EdgeLabelType(int code, String name) { - assert code < 256; - this.code = (byte) code; - this.name = name; - } - - @Override - public byte code() { - return this.code; - } - - public String string() { - return this.name; - } - - public boolean normal() { - return this == NORMAL; - } - - public boolean parent() { - return this == PARENT; - } - - public boolean sub() { - return this == SUB; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/HugeKeys.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/HugeKeys.java deleted file mode 100644 index 0e3536b011..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/HugeKeys.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type.define; - -public enum HugeKeys { - - UNKNOWN(0, "undefined"), - - /* Column names of schema type (common) */ - ID(1, "id"), - NAME(2, "name"), - TIMESTAMP(3, "timestamp"), - SCHEMA_TYPE(4, "schema_type"), - VALUE(5, "value"), - VERSION(6, "version"), - - USER_DATA(10, "user_data"), - STATUS(11, "status"), - - /* Column names of schema type (VertexLabel) */ - ID_STRATEGY(50, "id_strategy"), - PROPERTIES(51, "properties"), - PRIMARY_KEYS(52, "primary_keys"), - INDEX_LABELS(53, "index_labels"), - NULLABLE_KEYS(54, "nullable_keys"), - ENABLE_LABEL_INDEX(55, "enable_label_index"), - - /* Column names of schema type (EdgeLabel) */ - LINKS(80, "links"), - FREQUENCY(81, "frequency"), - SOURCE_LABEL(82, "source_label"), - TARGET_LABEL(83, "target_label"), - SORT_KEYS(84, "sort_keys"), - TTL(85, "ttl"), - TTL_START_TIME(86, "ttl_start_time"), - EDGELABEL_TYPE(87, "edgelabel_type"), - PARENT_LABEL(89, "parent_label"), - - /* Column names of schema type (PropertyKey) */ - DATA_TYPE(120, "data_type"), - CARDINALITY(121, "cardinality"), - AGGREGATE_TYPE(122, "aggregate_type"), - WRITE_TYPE(123, "write_type"), - - /* Column names of schema type (IndexLabel) */ - BASE_TYPE(150, "base_type"), - BASE_VALUE(151, "base_value"), - INDEX_TYPE(152, "index_type"), - FIELDS(153, "fields"), - - /* Column names of index data */ - INDEX_NAME(180, "index_name"), - FIELD_VALUES(181, "field_values"), - INDEX_LABEL_ID(182, "index_label_id"), - ELEMENT_IDS(183, "element_ids"), - - /* Column names of data type (Vertex/Edge) */ - LABEL(200, "label"), - OWNER_VERTEX(201, "owner_vertex"), - OTHER_VERTEX(202, "other_vertex"), - PROPERTY_KEY(203, "property_key"), - PROPERTY_VALUE(204, "property_value"), - DIRECTION(205, "direction"), - SORT_VALUES(206, "sort_values"), - PRIMARY_VALUES(207, "primary_values"), - EXPIRED_TIME(208, "expired_time"), - SUB_LABEL(211, "sub_label"), - - PROPERTY_TYPE(249, "property_type"), - AGGREGATE_PROPERTIES(250, "aggregate_properties"); - - public static final long NORMAL_PROPERTY_ID = 0L; - - /* HugeKeys define */ - private byte code = 0; - private String name = null; - - HugeKeys(int code, String name) { - assert code < 256; - this.code = (byte) code; - this.name = name; - } - - public byte code() { - return this.code; - } - - public String string() { - return this.name; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/IdStrategy.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/IdStrategy.java deleted file mode 100644 index da9f51313c..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/IdStrategy.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type.define; - -public enum IdStrategy implements SerialEnum { - - DEFAULT(0, "default"), - - AUTOMATIC(1, "automatic"), - - PRIMARY_KEY(2, "primary_key"), - - CUSTOMIZE_STRING(3, "customize_string"), - - CUSTOMIZE_NUMBER(4, "customize_number"), - - CUSTOMIZE_UUID(5, "customize_uuid"); - - private byte code = 0; - private String name = null; - - static { - SerialEnum.register(IdStrategy.class); - } - - IdStrategy(int code, String name) { - assert code < 256; - this.code = (byte) code; - this.name = name; - } - - @Override - public byte code() { - return this.code; - } - - public String string() { - return this.name; - } - - public boolean isAutomatic() { - return this == AUTOMATIC; - } - - public boolean isPrimaryKey() { - return this == PRIMARY_KEY; - } - - public boolean isCustomized() { - return this == CUSTOMIZE_STRING || - this == CUSTOMIZE_NUMBER || - this == CUSTOMIZE_UUID; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/IndexType.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/IndexType.java deleted file mode 100644 index 019ac98df9..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/IndexType.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type.define; - -import org.apache.hugegraph.type.HugeType; - -public enum IndexType implements SerialEnum { - - // For secondary query - SECONDARY(1, "secondary"), - - // For range query - RANGE(2, "range"), - RANGE_INT(21, "range_int"), - RANGE_FLOAT(22, "range_float"), - RANGE_LONG(23, "range_long"), - RANGE_DOUBLE(24, "range_double"), - - // For full-text query (not supported now) - SEARCH(3, "search"), - - // For prefix + range query - SHARD(4, "shard"), - - // For unique index - UNIQUE(5, "unique"); - - private byte code = 0; - private String name = null; - - static { - SerialEnum.register(IndexType.class); - } - - IndexType(int code, String name) { - assert code < 256; - this.code = (byte) code; - this.name = name; - } - - @Override - public byte code() { - return this.code; - } - - public String string() { - return this.name; - } - - public HugeType type() { - switch (this) { - case SECONDARY: - return HugeType.SECONDARY_INDEX; - case RANGE_INT: - return HugeType.RANGE_INT_INDEX; - case RANGE_FLOAT: - return HugeType.RANGE_FLOAT_INDEX; - case RANGE_LONG: - return HugeType.RANGE_LONG_INDEX; - case RANGE_DOUBLE: - return HugeType.RANGE_DOUBLE_INDEX; - case SEARCH: - return HugeType.SEARCH_INDEX; - case SHARD: - return HugeType.SHARD_INDEX; - case UNIQUE: - return HugeType.UNIQUE_INDEX; - default: - throw new AssertionError(String.format( - "Unknown index type '%s'", this)); - } - } - - public boolean isString() { - return this == SECONDARY || this == SEARCH || - this == SHARD || this == UNIQUE; - } - - public boolean isNumeric() { - return this == RANGE_INT || this == RANGE_FLOAT || - this == RANGE_LONG || this == RANGE_DOUBLE || - this == SHARD; - } - - public boolean isSecondary() { - return this == SECONDARY; - } - - public boolean isRange() { - return this == RANGE_INT || this == RANGE_FLOAT || - this == RANGE_LONG || this == RANGE_DOUBLE; - } - - public boolean isSearch() { - return this == SEARCH; - } - - public boolean isShard() { - return this == SHARD; - } - - public boolean isUnique() { - return this == UNIQUE; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/SerialEnum.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/SerialEnum.java deleted file mode 100644 index 471c0c2d70..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/SerialEnum.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type.define; - -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.util.CollectionUtil; -import org.apache.hugegraph.util.E; - -import com.google.common.collect.HashBasedTable; -import com.google.common.collect.Table; - -public interface SerialEnum { - - byte code(); - - Table, Byte, SerialEnum> TABLE = HashBasedTable.create(); - - static void register(Class clazz) { - Object enums; - try { - enums = clazz.getMethod("values").invoke(null); - } catch (Exception e) { - throw new BackendException(e); - } - for (SerialEnum e : CollectionUtil.toList(enums)) { - TABLE.put(clazz, e.code(), e); - } - } - - static T fromCode(Class clazz, byte code) { - @SuppressWarnings("unchecked") - T value = (T) TABLE.get(clazz, code); - if (value == null) { - E.checkArgument(false, "Can't construct %s from code %s", - clazz.getSimpleName(), code); - } - return value; - } - - static void registerInternalEnums() { - SerialEnum.register(Action.class); - SerialEnum.register(AggregateType.class); - SerialEnum.register(Cardinality.class); - SerialEnum.register(DataType.class); - SerialEnum.register(Directions.class); - SerialEnum.register(Frequency.class); - SerialEnum.register(HugeType.class); - SerialEnum.register(IdStrategy.class); - SerialEnum.register(IndexType.class); - SerialEnum.register(SchemaStatus.class); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/WriteType.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/WriteType.java deleted file mode 100644 index ea1018eabd..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/WriteType.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.type.define; - -public enum WriteType implements SerialEnum { - - // OLTP property key - OLTP(1, "oltp"), - - // OLAP property key without index - OLAP_COMMON(2, "olap_common"), - - // OLAP property key with secondary index - OLAP_SECONDARY(3, "olap_secondary"), - - // OLAP property key with range index - OLAP_RANGE(4, "olap_range"); - - private byte code = 0; - private String name = null; - - static { - SerialEnum.register(WriteType.class); - } - - WriteType(int code, String name) { - assert code < 256; - this.code = (byte) code; - this.name = name; - } - - @Override - public byte code() { - return this.code; - } - - public String string() { - return this.name; - } - - public boolean oltp() { - return this == OLTP; - } - - public boolean olap() { - return this == OLAP_COMMON || - this == OLAP_RANGE || - this == OLAP_SECONDARY; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Blob.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Blob.java deleted file mode 100644 index 5b5d8ec6ad..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Blob.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.util; - -import java.util.Arrays; - -public class Blob implements Comparable { - - public static final Blob EMPTY = new Blob(new byte[0]); - - private final byte[] bytes; - - private Blob(byte[] bytes) { - E.checkNotNull(bytes, "bytes"); - this.bytes = bytes; - } - - public byte[] bytes() { - return this.bytes; - } - - public static Blob wrap(byte[] bytes) { - return new Blob(bytes); - } - - @Override - public int hashCode() { - return Arrays.hashCode(this.bytes); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Blob)) { - return false; - } - Blob other = (Blob) obj; - return Arrays.equals(this.bytes, other.bytes); - } - - @Override - public String toString() { - String hex = Bytes.toHex(this.bytes); - StringBuilder sb = new StringBuilder(6 + hex.length()); - sb.append("Blob{").append(hex).append("}"); - return sb.toString(); - } - - @Override - public int compareTo(Blob other) { - E.checkNotNull(other, "other blob"); - return Bytes.compare(this.bytes, other.bytes); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Client.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/CloseFailure.java similarity index 62% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Client.java rename to hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/CloseFailure.java index 52c86bb7d7..9a35a6e350 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/rpc/RpcServiceConfig4Client.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/CloseFailure.java @@ -15,21 +15,21 @@ * limitations under the License. */ -package org.apache.hugegraph.rpc; +package org.apache.hugegraph.util; -public interface RpcServiceConfig4Client { +public final class CloseFailure { - T serviceProxy(String interfaceId); - - T serviceProxy(String graph, String interfaceId); - - default T serviceProxy(Class clazz) { - return this.serviceProxy(clazz.getName()); + private CloseFailure() { } - default T serviceProxy(String graph, Class clazz) { - return this.serviceProxy(graph, clazz.getName()); + public static Throwable collect(Throwable first, Throwable next) { + if (first == null || first == next) { + return first == null ? next : first; + } + Throwable primary = next instanceof Error && !(first instanceof Error) ? next : first; + Throwable secondary = primary == first ? next : first; + primary.addSuppressed(secondary); + return primary; } - void removeAllServiceProxy(); } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java index 54b5392376..a67f2c5680 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/ConfigUtil.java @@ -40,7 +40,7 @@ import org.apache.commons.configuration2.tree.NodeModel; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.config.HugeConfig; import org.slf4j.Logger; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Consumers.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Consumers.java index f4a7671f35..914d4161fe 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Consumers.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Consumers.java @@ -33,7 +33,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.task.TaskManager.ContextCallable; import org.slf4j.Logger; @@ -57,6 +57,7 @@ public final class Consumers { private final CountDownLatch latch; private final BlockingQueue queue; private volatile Throwable exception = null; + private volatile boolean stopped; public Consumers(ExecutorService executor, Consumer consumer) { this(executor, consumer, null); @@ -98,6 +99,7 @@ public Consumers(ExecutorService executor, public void start(String name) { this.exception = null; + this.stopped = false; if (this.executor == null) { return; } @@ -117,15 +119,15 @@ public void start(String name) { private Void safeRun(ContextCallable task) { try { return task.call(); // may fail before/after runAndDone() - } catch (Exception e) { + } catch (Throwable e) { // This exception is from ContextCallable wrapper (setContext/resetContext/delegate dispatch), // not from runAndDone() business logic (that one is handled inside runAndDone()). - if (this.exception == null) { - this.exception = e; + if (this.recordException(e)) { LOG.error("Consumer worker failed in ContextCallable wrapper", e); } else { LOG.warn("Additional worker failure in ContextCallable wrapper; first exception already recorded", e); } + this.stop(); this.exceptionHandle(e); } finally { this.latch.countDown(); @@ -136,18 +138,21 @@ private Void safeRun(ContextCallable task) { private Void runAndDone() { try { this.run(); - } catch (Exception e) { + } catch (Throwable e) { if (e instanceof StopExecution) { - this.queue.clear(); - putQueueEnd(); + Throwable[] suppressed = e.getSuppressed(); + // Stop stays control flow, but failed cleanup must reach the caller. + for (Throwable failure : suppressed) { + this.recordException(failure); + } } else { - if (this.exception == null) { - this.exception = e; + if (this.recordException(e)) { LOG.error("Unhandled exception in consumer task", e); } else { LOG.warn("Additional exception in consumer task; first exception already recorded", e); } } + this.stop(); this.exceptionHandle(e); } finally { this.done(); @@ -155,6 +160,11 @@ private Void runAndDone() { return null; } + private void stop() { + this.stopped = true; + this.queue.clear(); + } + private void run() { LOG.debug("Start to work..."); while (this.consume()) { @@ -167,6 +177,9 @@ private void run() { private boolean consume() { V elem = null; while (elem == null) { + if (this.stopped) { + return false; + } try { elem = this.queue.poll(CONSUMER_WAKE_PERIOD, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { @@ -175,6 +188,9 @@ private boolean consume() { } } + if (this.stopped) { + return false; + } if (elem == QUEUE_END) { putQueueEnd(); return false; @@ -192,9 +208,7 @@ private void exceptionHandle(Throwable e) { try { this.exceptionHandle.accept(e); } catch (Throwable ex) { - if (this.exception == null) { - this.exception = ex; - } else { + if (!this.recordException(ex)) { LOG.warn("Error while calling exceptionHandle()", ex); } } @@ -208,19 +222,32 @@ private void done() { try { this.doneHandle.run(); } catch (Throwable e) { - if (this.exception == null) { - this.exception = e; - } else { + if (!this.recordException(e)) { LOG.warn("Error while calling done()", e); } + this.stop(); } } + private synchronized boolean recordException(Throwable failure) { + if (this.exception == null) { + this.exception = failure; + return true; + } + if (this.exception != failure) { + for (Throwable suppressed : this.exception.getSuppressed()) { + if (suppressed == failure) { + return false; + } + } + this.exception.addSuppressed(failure); + } + return false; + } + private Throwable throwException() { assert this.exception != null; - Throwable e = this.exception; - this.exception = null; - return e; + return this.exception; } public void provide(V v) throws Throwable { @@ -232,7 +259,17 @@ public void provide(V v) throws Throwable { throw this.throwException(); } else { try { - this.queue.put(v); + while (!this.stopped && + !this.queue.offer(v, CONSUMER_WAKE_PERIOD, TimeUnit.MILLISECONDS)) { + // A stopped worker must not leave a producer blocked on a full queue. + } + if (this.stopped) { + this.queue.clear(); + if (this.exception != null) { + throw this.throwException(); + } + throw new StopExecution("Consumer stopped"); + } } catch (InterruptedException e) { LOG.warn("Interrupt while queuing QUEUE_END", e); } @@ -242,7 +279,10 @@ public void provide(V v) throws Throwable { private void putQueueEnd() { if (this.executor != null) { try { - this.queue.put(QUEUE_END); + while (!this.stopped && + !this.queue.offer(QUEUE_END, CONSUMER_WAKE_PERIOD, TimeUnit.MILLISECONDS)) { + // Stop wakes consumers through its state, without another END token. + } } catch (InterruptedException e) { LOG.warn("Interrupted while enqueue", e); } @@ -262,7 +302,8 @@ public void await() throws Throwable { for (Future f : this.runningFutures) { f.cancel(true); } - this.exception = new HugeException(error, e); + this.recordException(new HugeException(error, e)); + this.stop(); LOG.warn(error, e); } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/CopyUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/CopyUtil.java index 7a043b7cd5..cced524bbd 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/CopyUtil.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/CopyUtil.java @@ -21,7 +21,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; public final class CopyUtil { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/FixedTimerWindowRateLimiter.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/FixedTimerWindowRateLimiter.java index 0163656d5b..7b08204bbb 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/FixedTimerWindowRateLimiter.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/FixedTimerWindowRateLimiter.java @@ -34,7 +34,11 @@ public class FixedTimerWindowRateLimiter implements RateLimiter { private final int limit; public FixedTimerWindowRateLimiter(int limitPerSecond) { - this.timer = new Timer("RateAuditLog", true); + this(limitPerSecond, new Timer("RateAuditLog", true)); + } + + FixedTimerWindowRateLimiter(int limitPerSecond, Timer timer) { + this.timer = timer; this.count = new LongAdder(); this.limit = limitPerSecond; // Count will be reset if hit limit (run once per 1000ms) @@ -45,7 +49,7 @@ public void run() { count.reset(); } } - }, 0L, RESET_PERIOD); + }, RESET_PERIOD, RESET_PERIOD); } @Override diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/GZipUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/GZipUtil.java index 962d4a33a1..ef09cc2431 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/GZipUtil.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/GZipUtil.java @@ -22,8 +22,8 @@ import java.util.zip.Inflater; import org.apache.commons.codec.digest.DigestUtils; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.serializer.BytesBuffer; public final class GZipUtil { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java index fd00816f66..5ed4968fe6 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java @@ -20,7 +20,7 @@ import java.io.IOException; import java.util.Date; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.io.HugeGraphSONModule; import org.apache.tinkerpop.shaded.jackson.core.JsonGenerator; import org.apache.tinkerpop.shaded.jackson.core.JsonProcessingException; @@ -52,6 +52,7 @@ public final class JsonUtil { HugeGraphSONModule.registerServiceSerializers(module); HugeGraphSONModule.registerGraphSpaceSerializers(module); HugeGraphSONModule.registerGraphSerializers(module); + HugeGraphSONModule.registerTraversalSerializers(module); MAPPER.registerModule(module); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/KryoUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/KryoUtil.java deleted file mode 100644 index ce789c6d8d..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/KryoUtil.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.UUID; - -import org.apache.hugegraph.backend.BackendException; -import org.apache.tinkerpop.shaded.kryo.Kryo; -import org.apache.tinkerpop.shaded.kryo.Serializer; -import org.apache.tinkerpop.shaded.kryo.io.Input; -import org.apache.tinkerpop.shaded.kryo.io.Output; - -public final class KryoUtil { - - private static final ThreadLocal KRYOS = new ThreadLocal<>(); - - public static Kryo kryo() { - Kryo kryo = KRYOS.get(); - if (kryo != null) { - return kryo; - } - - kryo = new Kryo(); - registerSerializers(kryo); - KRYOS.set(kryo); - return kryo; - } - - public static byte[] toKryo(Object value) { - try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); - Output output = new Output(bos, 256)) { - kryo().writeObject(output, value); - output.flush(); - return bos.toByteArray(); - } catch (IOException e) { - throw new BackendException("Failed to serialize: %s", e, value); - } - } - - public static T fromKryo(byte[] value, Class clazz) { - E.checkState(value != null, - "Kryo value can't be null for '%s'", - clazz.getSimpleName()); - return kryo().readObject(new Input(value), clazz); - } - - public static byte[] toKryoWithType(Object value) { - try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); - Output output = new Output(bos, 256)) { - kryo().writeClassAndObject(output, value); - output.flush(); - return bos.toByteArray(); - } catch (IOException e) { - throw new BackendException("Failed to serialize: %s", e, value); - } - } - - @SuppressWarnings("unchecked") - public static T fromKryoWithType(byte[] value) { - E.checkState(value != null, "Kryo value can't be null for object"); - return (T) kryo().readClassAndObject(new Input(value)); - } - - private static void registerSerializers(Kryo kryo) { - kryo.addDefaultSerializer(UUID.class, new Serializer() { - - @Override - public UUID read(Kryo kryo, Input input, Class c) { - return new UUID(input.readLong(), input.readLong()); - } - - @Override - public void write(Kryo kryo, Output output, UUID uuid) { - output.writeLong(uuid.getMostSignificantBits()); - output.writeLong(uuid.getLeastSignificantBits()); - } - }); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/LZ4Util.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/LZ4Util.java deleted file mode 100644 index 44de9c60f6..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/LZ4Util.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.util; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.serializer.BytesBuffer; - -import net.jpountz.lz4.LZ4BlockInputStream; -import net.jpountz.lz4.LZ4BlockOutputStream; -import net.jpountz.lz4.LZ4Compressor; -import net.jpountz.lz4.LZ4Factory; -import net.jpountz.lz4.LZ4FastDecompressor; - -public class LZ4Util { - - protected static final float DEFAULT_BUFFER_RATIO = 1.5f; - - public static BytesBuffer compress(byte[] bytes, int blockSize) { - return compress(bytes, blockSize, DEFAULT_BUFFER_RATIO); - } - - public static BytesBuffer compress(byte[] bytes, int blockSize, float bufferRatio) { - float ratio = bufferRatio <= 0.0F ? DEFAULT_BUFFER_RATIO : bufferRatio; - LZ4Factory factory = LZ4Factory.fastestInstance(); - LZ4Compressor compressor = factory.fastCompressor(); - int initBufferSize = Math.round(bytes.length / ratio); - BytesBuffer buf = new BytesBuffer(initBufferSize); - LZ4BlockOutputStream lz4Output = new LZ4BlockOutputStream(buf, blockSize, compressor); - try { - lz4Output.write(bytes); - lz4Output.close(); - } catch (IOException e) { - throw new BackendException("Failed to compress", e); - } - // If we need to perform reading outside the method, remember to call forReadWritten() - return buf; - } - - public static BytesBuffer decompress(byte[] bytes, int blockSize) { - return decompress(bytes, blockSize, DEFAULT_BUFFER_RATIO); - } - - public static BytesBuffer decompress(byte[] bytes, int blockSize, float bufferRatio) { - float ratio = bufferRatio <= 0.0F ? DEFAULT_BUFFER_RATIO : bufferRatio; - LZ4Factory factory = LZ4Factory.fastestInstance(); - LZ4FastDecompressor decompressor = factory.fastDecompressor(); - ByteArrayInputStream bais = new ByteArrayInputStream(bytes); - int initBufferSize = Math.min(Math.round(bytes.length * ratio), - BytesBuffer.maxBufferCapacity()); - BytesBuffer buf = new BytesBuffer(initBufferSize); - LZ4BlockInputStream lzInput = new LZ4BlockInputStream(bais, decompressor); - int count; - byte[] buffer = new byte[blockSize]; - try { - while ((count = lzInput.read(buffer)) != -1) { - buf.write(buffer, 0, count); - } - lzInput.close(); - } catch (IOException e) { - throw new BackendException("Failed to decompress", e); - } - // If we need to perform reading outside the method, remember to call forReadWritten() - return buf; - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/LockUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/LockUtil.java index 0fb10989fc..c9d317634e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/LockUtil.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/LockUtil.java @@ -29,8 +29,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.concurrent.KeyLock; import org.apache.hugegraph.concurrent.LockManager; import org.apache.hugegraph.concurrent.RowLock; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Reflection.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Reflection.java index 6e5fd07527..f66d051d5d 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Reflection.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/Reflection.java @@ -20,8 +20,10 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.exception.NotSupportException; import org.slf4j.Logger; @@ -42,14 +44,14 @@ public class Reflection { registerFieldsToFilterMethodTemp = reflectionClazzTemp.getMethod("registerFieldsToFilter", - Class.class, String[].class); + Class.class, Set.class); registerMethodsToFilterMethodTemp = reflectionClazzTemp.getMethod("registerMethodsToFilter", - Class.class, String[].class); + Class.class, Set.class); } catch (ClassNotFoundException e) { LOG.error("Can't find jdk.internal.reflect.Reflection class, " + - "please ensure you are using Java 11", e); + "please ensure you are using Java 17", e); } catch (NoSuchMethodException e) { LOG.error("Can't find reflection filter methods", e); } @@ -62,34 +64,59 @@ public class Reflection { public static void registerFieldsToFilter(Class containingClass, String... fieldNames) { if (REGISTER_FILEDS_TO_FILTER_METHOD == null) { throw new NotSupportException("Reflection.registerFieldsToFilter() - " + - "requires Java 11 or higher"); + "requires Java 17 or higher"); } try { REGISTER_FILEDS_TO_FILTER_METHOD.setAccessible(true); - REGISTER_FILEDS_TO_FILTER_METHOD.invoke(REFLECTION_CLAZZ, containingClass, fieldNames); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new HugeException("Failed to register class '%s' fields to filter: %s", - containingClass, Arrays.toString(fieldNames)); + REGISTER_FILEDS_TO_FILTER_METHOD.invoke(REFLECTION_CLAZZ, containingClass, + toFilterSet(fieldNames)); + } catch (IllegalAccessException e) { + throw new HugeException( + "Failed to register class '%s' fields to filter: %s", + e, containingClass, Arrays.toString(fieldNames)); + } catch (InvocationTargetException e) { + throwInvocationTargetException(e, containingClass, fieldNames, "fields"); } } public static void registerMethodsToFilter(Class containingClass, String... methodNames) { if (REGISTER_METHODS_TO_FILTER_METHOD == null) { throw new NotSupportException("Reflection.registerMethodsToFilter() - " + - "requires Java 11 or higher"); + "requires Java 17 or higher"); } try { REGISTER_METHODS_TO_FILTER_METHOD.setAccessible(true); REGISTER_METHODS_TO_FILTER_METHOD.invoke(REFLECTION_CLAZZ, containingClass, - methodNames); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new HugeException("Failed to register class '%s' methods to filter: %s", - containingClass, Arrays.toString(methodNames)); + toFilterSet(methodNames)); + } catch (IllegalAccessException e) { + throw new HugeException( + "Failed to register class '%s' methods to filter: %s", + e, containingClass, Arrays.toString(methodNames)); + } catch (InvocationTargetException e) { + throwInvocationTargetException(e, containingClass, methodNames, "methods"); } } + private static Set toFilterSet(String... members) { + return new LinkedHashSet<>(Arrays.asList(members)); + } + + private static void throwInvocationTargetException(InvocationTargetException exception, + Class containingClass, + String[] members, + String type) { + Throwable cause = exception.getCause(); + if (cause instanceof IllegalArgumentException) { + throw (IllegalArgumentException) cause; + } + Throwable failure = cause != null ? cause : exception; + throw new HugeException( + "Failed to register class '%s' %s to filter: %s", + failure, containingClass, type, Arrays.toString(members)); + } + public static Class loadClass(String clazz) { try { return Class.forName(clazz); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/StringEncoding.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/StringEncoding.java deleted file mode 100644 index 7b10738580..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/StringEncoding.java +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2017 JanusGraph 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. - -package org.apache.hugegraph.util; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; -import java.util.UUID; - -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.serializer.BytesBuffer; -import org.mindrot.jbcrypt.BCrypt; - -import com.google.common.base.CharMatcher; - -/** - * @author Matthias Broecheler (me@matthiasb.com) - * @author HugeGraph Authors - */ -public final class StringEncoding { - - private static final MessageDigest DIGEST; - private static final byte[] BYTES_EMPTY = new byte[0]; - private static final String STRING_EMPTY = ""; - private static final int BLOCK_SIZE = 4096; - private static final int BCRYPT_WORK_FACTOR = 10; - - static { - final String ALG = "SHA-256"; - try { - DIGEST = MessageDigest.getInstance(ALG); - } catch (NoSuchAlgorithmException e) { - throw new HugeException("Failed to load algorithm %s", e, ALG); - } - } - - private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); - private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); - - /** Similar to {@link StringSerializer} */ - public static int writeAsciiString(byte[] array, int offset, String value) { - E.checkArgument(CharMatcher.ascii().matchesAllOf(value), - "'%s' must be ASCII string", value); - int len = value.length(); - if (len == 0) { - array[offset++] = (byte) 0x80; - return offset; - } - - int i = 0; - do { - int c = value.charAt(i); - assert c <= 127; - byte b = (byte) c; - if (++i == len) { - // End marker - b |= 0x80; - } - array[offset++] = b; - } while (i < len); - - return offset; - } - - public static String readAsciiString(byte[] array, int offset) { - StringBuilder sb = new StringBuilder(); - int c; - do { - c = 0xFF & array[offset++]; - if (c != 0x80) { - sb.append((char) (c & 0x7F)); - } - } while ((c & 0x80) <= 0); - return sb.toString(); - } - - public static int getAsciiByteLength(String value) { - E.checkArgument(CharMatcher.ascii().matchesAllOf(value), - "'%s' must be ASCII string", value); - return value.isEmpty() ? 1 : value.length(); - } - - public static byte[] encode(String value) { - return value.getBytes(StandardCharsets.UTF_8); - } - - public static String decode(byte[] bytes) { - if (bytes.length == 0) { - return STRING_EMPTY; - } - return new String(bytes, StandardCharsets.UTF_8); - } - - public static String decode(byte[] bytes, int offset, int length) { - if (length == 0) { - return STRING_EMPTY; - } - return new String(bytes, offset, length, StandardCharsets.UTF_8); - } - - public static String encodeBase64(byte[] bytes) { - return BASE64_ENCODER.encodeToString(bytes); - } - - public static byte[] decodeBase64(String value) { - if (value.isEmpty()) { - return BYTES_EMPTY; - } - return BASE64_DECODER.decode(value); - } - - public static byte[] compress(String value) { - return compress(value, LZ4Util.DEFAULT_BUFFER_RATIO); - } - - public static byte[] compress(String value, float bufferRatio) { - BytesBuffer buf = LZ4Util.compress(encode(value), BLOCK_SIZE, bufferRatio); - return buf.bytes(); - } - - public static String decompress(byte[] value) { - return decompress(value, LZ4Util.DEFAULT_BUFFER_RATIO); - } - - public static String decompress(byte[] value, float bufferRatio) { - BytesBuffer buf = LZ4Util.decompress(value, BLOCK_SIZE, bufferRatio); - return decode(buf.array(), 0, buf.position()); - } - - public static String hashPassword(String password) { - // OWASP suggests 10 as a minimum and 12–14 for production; - // workFactor 12 is not used by default due to its 200+ ms cost. - return BCrypt.hashpw(password, BCrypt.gensalt(BCRYPT_WORK_FACTOR)); - } - - public static boolean checkPassword(String candidatePassword, String dbPassword) { - return BCrypt.checkpw(candidatePassword, dbPassword); - } - - public static String sha256(String string) { - byte[] stringBytes = encode(string); - DIGEST.reset(); - return StringEncoding.encodeBase64(DIGEST.digest(stringBytes)); - } - - public static String format(byte[] bytes) { - return String.format("%s[0x%s]", decode(bytes), Bytes.toHex(bytes)); - } - - public static UUID uuid(String value) { - E.checkArgument(value != null, "The UUID can't be null"); - try { - if (value.contains("-") && value.length() == 36) { - return UUID.fromString(value); - } - // UUID represented by hex string - E.checkArgument(value.length() == 32, "Invalid UUID string: %s", value); - String high = value.substring(0, 16); - String low = value.substring(16); - return new UUID(Long.parseUnsignedLong(high, 16), - Long.parseUnsignedLong(low, 16)); - } catch (NumberFormatException ignored) { - throw new IllegalArgumentException("Invalid UUID string: " + value); - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/TinkerPopUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/TinkerPopUtil.java new file mode 100644 index 0000000000..574201a243 --- /dev/null +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/TinkerPopUtil.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.hugegraph.util; + +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.Directions; +import org.apache.tinkerpop.gremlin.structure.Direction; +import org.apache.tinkerpop.gremlin.structure.VertexProperty; + +public final class TinkerPopUtil { + + private TinkerPopUtil() { + } + + public static Directions direction(Direction direction) { + switch (direction) { + case OUT: + return Directions.OUT; + case IN: + return Directions.IN; + case BOTH: + return Directions.BOTH; + default: + throw new AssertionError("Unknown direction: " + direction); + } + } + + public static Direction direction(Directions direction) { + switch (direction) { + case OUT: + return Direction.OUT; + case IN: + return Direction.IN; + case BOTH: + return Direction.BOTH; + default: + throw new AssertionError("Unknown direction: " + direction); + } + } + + public static Cardinality cardinality(VertexProperty.Cardinality cardinality) { + switch (cardinality) { + case single: + return Cardinality.SINGLE; + case list: + return Cardinality.LIST; + case set: + return Cardinality.SET; + default: + throw new AssertionError("Unknown cardinality: " + cardinality); + } + } +} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/CollectionFactory.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/CollectionFactory.java deleted file mode 100644 index eba9d4fa6e..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/CollectionFactory.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.util.collection; - -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.hugegraph.backend.id.Id; -import org.apache.hugegraph.type.define.CollectionType; -import org.apache.hugegraph.util.E; -import org.eclipse.collections.api.map.primitive.IntObjectMap; -import org.eclipse.collections.api.map.primitive.MutableIntObjectMap; -import org.eclipse.collections.impl.list.mutable.FastList; -import org.eclipse.collections.impl.map.mutable.UnifiedMap; -import org.eclipse.collections.impl.map.mutable.primitive.IntObjectHashMap; -import org.eclipse.collections.impl.set.mutable.UnifiedSet; - -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; - -public class CollectionFactory { - - private final CollectionType type; - - public CollectionFactory() { - this.type = CollectionType.EC; - } - - public CollectionFactory(CollectionType type) { - this.type = type; - } - - public List newList() { - return newList(this.type); - } - - public List newList(int initialCapacity) { - return newList(this.type, initialCapacity); - } - - public List newList(Collection collection) { - return newList(this.type, collection); - } - - public static List newList(CollectionType type) { - switch (type) { - case EC: - return new FastList<>(); - case JCF: - return new ArrayList<>(); - case FU: - return new ObjectArrayList<>(); - default: - throw new AssertionError( - "Unsupported collection type: " + type); - } - } - - public static List newList(CollectionType type, - int initialCapacity) { - switch (type) { - case EC: - return new FastList<>(initialCapacity); - case JCF: - return new ArrayList<>(initialCapacity); - case FU: - return new ObjectArrayList<>(initialCapacity); - default: - throw new AssertionError( - "Unsupported collection type: " + type); - } - } - - public static List newList(CollectionType type, - Collection collection) { - switch (type) { - case EC: - return new FastList<>(collection); - case JCF: - return new ArrayList<>(collection); - case FU: - return new ObjectArrayList<>(collection); - default: - throw new AssertionError( - "Unsupported collection type: " + type); - } - } - - public Set newSet() { - return newSet(this.type); - } - - public Set newSet(int initialCapacity) { - return newSet(this.type, initialCapacity); - } - - public Set newSet(Collection collection) { - return newSet(this.type, collection); - } - - public static Set newSet(CollectionType type) { - switch (type) { - case EC: - return new UnifiedSet<>(); - case JCF: - return new HashSet<>(); - case FU: - return new ObjectOpenHashSet<>(); - default: - throw new AssertionError( - "Unsupported collection type: " + type); - } - } - - public static Set newSet(CollectionType type, - int initialCapacity) { - switch (type) { - case EC: - return new UnifiedSet<>(initialCapacity); - case JCF: - return new HashSet<>(initialCapacity); - case FU: - return new ObjectOpenHashSet<>(initialCapacity); - default: - throw new AssertionError( - "Unsupported collection type: " + type); - } - } - - public static Set newSet(CollectionType type, - Collection collection) { - switch (type) { - case EC: - return new UnifiedSet<>(collection); - case JCF: - return new HashSet<>(collection); - case FU: - return new ObjectOpenHashSet<>(collection); - default: - throw new AssertionError( - "Unsupported collection type: " + type); - } - } - - public Map newMap() { - return newMap(this.type); - } - - public Map newMap(int initialCapacity) { - return newMap(this.type, initialCapacity); - } - - public Map newMap(Map map) { - return newMap(this.type, map); - } - - public static Map newMap(CollectionType type) { - /* - * EC is faster 10%-20% than JCF, and it's more stable & less - * memory cost(size is bigger, EC is better). - */ - switch (type) { - case EC: - return new UnifiedMap<>(); - case JCF: - return new HashMap<>(); - case FU: - return new Object2ObjectOpenHashMap<>(); - default: - throw new AssertionError( - "Unsupported collection type: " + type); - } - } - - public static Map newMap(CollectionType type, - int initialCapacity) { - switch (type) { - case EC: - return new UnifiedMap<>(initialCapacity); - case JCF: - return new HashMap<>(initialCapacity); - case FU: - return new Object2ObjectOpenHashMap<>(initialCapacity); - default: - throw new AssertionError( - "Unsupported collection type: " + type); - } - } - - public static Map newMap(CollectionType type, - Map map) { - switch (type) { - case EC: - return new UnifiedMap<>(map); - case JCF: - return new HashMap<>(map); - case FU: - return new Object2ObjectOpenHashMap<>(map); - default: - throw new AssertionError( - "Unsupported collection type: " + type); - } - } - - public static MutableIntObjectMap newIntObjectMap() { - return new IntObjectHashMap<>(); - } - - public static MutableIntObjectMap newIntObjectMap( - int initialCapacity) { - return new IntObjectHashMap<>(initialCapacity); - } - - public static MutableIntObjectMap newIntObjectMap( - IntObjectMap map) { - return new IntObjectHashMap<>(map); - } - - @SuppressWarnings("unchecked") - public static MutableIntObjectMap newIntObjectMap( - Object... objects) { - IntObjectHashMap map = IntObjectHashMap.newMap(); - E.checkArgument(objects.length % 2 == 0, - "Must provide even arguments for " + - "CollectionFactory.newIntObjectMap"); - for (int i = 0; i < objects.length; i += 2) { - int key = objects[i] instanceof Id ? - (int) ((Id) objects[i]).asLong() : (int) objects[i]; - map.put(key, (V) objects[i + 1]); - } - return map; - } - - public IdSet newIdSet() { - return newIdSet(this.type); - } - - public static IdSet newIdSet(CollectionType type) { - return new IdSet(type); - } - - public static IntSet newIntSet() { - /* - * Resume to the old version like this: - * return concurrent ? new IntHashSet().asSynchronized() : - * new IntHashSet(); - */ - return new IntSet.IntSetBySegments(Integer.MAX_VALUE); - } - - public static IntMap newIntMap() { - /* - * Resume to the old version like this: - * return concurrent ? new IntIntHashMap().asSynchronized() : - * new IntIntHashMap(); - */ - return new IntMap.IntMapBySegments(Integer.MAX_VALUE); - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IdSet.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IdSet.java deleted file mode 100644 index 57f3b12021..0000000000 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IdSet.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.util.collection; - -import java.util.AbstractSet; -import java.util.Iterator; -import java.util.Set; - -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.iterator.ExtendableIterator; -import org.apache.hugegraph.type.define.CollectionType; -import org.eclipse.collections.api.iterator.MutableLongIterator; -import org.eclipse.collections.impl.set.mutable.primitive.LongHashSet; - -public class IdSet extends AbstractSet { - - private final LongHashSet numberIds; - private final Set nonNumberIds; - - public IdSet(CollectionType type) { - this.numberIds = new LongHashSet(); - this.nonNumberIds = CollectionFactory.newSet(type); - } - - @Override - public int size() { - return this.numberIds.size() + this.nonNumberIds.size(); - } - - @Override - public boolean isEmpty() { - return this.numberIds.isEmpty() && this.nonNumberIds.isEmpty(); - } - - @Override - public boolean contains(Object object) { - if (!(object instanceof Id)) { - return false; - } - Id id = (Id) object; - if (id.type() == Id.IdType.LONG) { - return this.numberIds.contains(id.asLong()); - } else { - return this.nonNumberIds.contains(id); - } - } - - @Override - public Iterator iterator() { - return new ExtendableIterator<>( - this.nonNumberIds.iterator(), - new EcLongIdIterator(this.numberIds.longIterator())); - } - - @Override - public boolean add(Id id) { - if (id.type() == Id.IdType.LONG) { - return this.numberIds.add(id.asLong()); - } else { - return this.nonNumberIds.add(id); - } - } - - public boolean remove(Id id) { - if (id.type() == Id.IdType.LONG) { - return this.numberIds.remove(id.asLong()); - } else { - return this.nonNumberIds.remove(id); - } - } - - @Override - public void clear() { - this.numberIds.clear(); - this.nonNumberIds.clear(); - } - - private static class EcLongIdIterator implements Iterator { - - private final MutableLongIterator iterator; - - public EcLongIdIterator(MutableLongIterator iter) { - this.iterator = iter; - } - - @Override - public boolean hasNext() { - return this.iterator.hasNext(); - } - - @Override - public Id next() { - return IdGenerator.of(this.iterator.next()); - } - - @Override - public void remove() { - this.iterator.remove(); - } - } -} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IntMapByDynamicHash.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IntMapByDynamicHash.java index 52aac88da9..3232197178 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IntMapByDynamicHash.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IntMapByDynamicHash.java @@ -148,8 +148,8 @@ private static boolean casTableAt(Object[] array, int index, Object expected, Ob */ @Override public boolean put(int key, int value) { - int hash = this.hash(key); Entry[] currentArray = this.table; + int hash = this.hash(key, currentArray.length); Entry o = (Entry) IntMapByDynamicHash.tableAt(currentArray, hash); if (o == null) { Entry newEntry = new Entry(key, value); @@ -229,8 +229,8 @@ private int slowPut(int key, int value, Entry[] currentTable) { */ @Override public int get(int key) { - int hash = this.hash(key); Entry[] currentArray = this.table; + int hash = this.hash(key, currentArray.length); Entry o = (Entry) IntMapByDynamicHash.tableAt(currentArray, hash); if (o == RESIZED || o == RESIZING) { return this.slowGet(key, currentArray); @@ -283,8 +283,8 @@ private int slowGet(int key, Entry[] currentArray) { */ @Override public boolean remove(int key) { - int hash = this.hash(key); Entry[] currentTable = this.table; + int hash = this.hash(key, currentTable.length); Entry o = (Entry) IntMapByDynamicHash.tableAt(currentTable, hash); if (o == RESIZED || o == RESIZING) { return this.slowRemove(key, currentTable) != null; @@ -327,7 +327,6 @@ private Entry slowRemove(int key, Entry[] currentTable) { currentTable = this.helpWithResizeWhileCurrentIndex(currentTable, index); } else { Entry e = o; - Entry prev = null; while (e != null) { int candidate = e.getKey(); @@ -340,12 +339,10 @@ private Entry slowRemove(int key, Entry[] currentTable) { // Key found, but CAS failed, restart the loop break; } - prev = e; e = e.getNext(); } - if (prev != null) { - // Key doesn't found + if (e == null) { return null; } } @@ -426,10 +423,6 @@ public boolean concurrent() { return true; } - private int hash(int key) { - return key & (table.length - 2); - } - private int hash(int key, int length) { return key & (length - 2); } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/ObjectIntMappingFactory.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/ObjectIntMappingFactory.java index 832ed680bc..2872b9f998 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/ObjectIntMappingFactory.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/ObjectIntMappingFactory.java @@ -17,7 +17,7 @@ package org.apache.hugegraph.util.collection; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.perf.PerfUtil.Watched; import org.eclipse.collections.impl.map.mutable.primitive.IntObjectHashMap; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/variables/HugeVariables.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/variables/HugeVariables.java index b7f2a6c58f..de1f15fa3d 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/variables/HugeVariables.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/variables/HugeVariables.java @@ -33,17 +33,17 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.iterator.FilterIterator; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/version/CoreVersion.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/version/CoreVersion.java index 2eff71487a..00d27843c2 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/version/CoreVersion.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/version/CoreVersion.java @@ -31,7 +31,7 @@ public class CoreVersion { /** * Update it when the gremlin version changed, search "tinkerpop.version" in pom */ - public static final String GREMLIN_VERSION = "3.5.1"; + public static final String GREMLIN_VERSION = "3.8.1"; static { // Check versions of the dependency packages diff --git a/hugegraph-server/hugegraph-dist/docker/README.md b/hugegraph-server/hugegraph-dist/docker/README.md index aa76092b82..473b4d0c92 100644 --- a/hugegraph-server/hugegraph-dist/docker/README.md +++ b/hugegraph-server/hugegraph-dist/docker/README.md @@ -1,5 +1,7 @@ # Deploy HugeGraph Server with Docker + + > Note: > > 1. The HugeGraph Docker image is a convenience release, not an official ASF distribution artifact. See the [ASF Release Distribution Policy](https://infra.apache.org/release-distribution.html#dockerhub) for details. @@ -131,6 +133,22 @@ HUGEGRAPH_VERSION=1.7.0 docker compose -f docker-compose-3pd-3store-3server.yml See [docker/README.md](../../../docker/README.md) for the full setup guide, environment variable reference, and troubleshooting. +With `usePD=true` (`HG_SERVER_USE_PD`), a Server registers its REST address in +PD. By default that is `restserver.url` (`HG_SERVER_REST_URL`). To register a different list instead, for example the +addresses clients reach through a Kubernetes Service, set both of these on the +Server container: + +| Variable | Property in `rest-server.properties` | +|---|---| +| `HG_SERVER_URLS_TO_PD` | `server.urls_to_pd`, a comma-separated list such as `http://hg-0.hg:8080,http://hg-1.hg:8080` | +| `HG_SERVER_DEPLOY_IN_K8S` | `server.deploy_in_k8s`; `true` makes the Server register `server.urls_to_pd` | + +`HG_SERVER_DEPLOY_IN_K8S` accepts the boolean spellings HugeConfig accepts +(`true`/`false`, `yes`/`no`, `on`/`off`, `y`/`n`, `t`/`f`, any case). Any other +value, `1` and `0` included, stops the container before either conf file is +changed. Give each URL its own `http://` prefix; whitespace in the list is +removed. Unset variables leave the properties as they are. + ## 6. Process Supervision & Health Checks All four HugeGraph Docker images (PD, Store, Server, Server-hstore) include diff --git a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh index 42137dc3b5..68dc76afdf 100755 --- a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh +++ b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh @@ -44,10 +44,16 @@ EOF cat > "${TEST_HOME}/bin/init-store.sh" <<'EOF' #!/usr/bin/env bash printf 'called\n' >> ./docker/init-store-calls +printf '%s' "${LC_ALL-}" > ./docker/init-store-locale if IFS= read -r password; then printf '%s' "${password}" > ./docker/init-store-password fi EOF +cat > "${TEST_HOME}/bin/util.sh" <<'EOF' +# Configuration tests isolate readiness; process lifecycle is tested separately. +read_property() { sed -n "s/^$2=//p" "$1"; } +wait_for_startup() { return 0; } +EOF cat > "${TEST_HOME}/bin/enable-auth.sh" <<'EOF' #!/usr/bin/env bash printf 'called\n' >> ./docker/enable-auth-calls @@ -62,6 +68,36 @@ exit 0 EOF chmod +x "${TEST_HOME}/bin/"*.sh +# CI supplies current HugeConfig and the POM-pinned Commons dependency classpath. +if [[ -n "${HG_CONFIG_TEST_CLASSPATH:-}" ]]; then + javac -cp "${HG_CONFIG_TEST_CLASSPATH}" -d "${TEST_HOME}/docker" \ + "${SCRIPT_DIR}/../../hugegraph-core/src/main/java/org/apache/hugegraph/config/AuthOptions.java" \ + "${SCRIPT_DIR}/test/AuthConfigReadback.java" \ + "${SCRIPT_DIR}/test/ConfigReadback.java" +else + echo "SKIP: native auth and service readback (HG_CONFIG_TEST_CLASSPATH is unset)" +fi +assert_service_readback() { + if [[ -n "${HG_CONFIG_TEST_CLASSPATH:-}" ]]; then + java -cp "${TEST_HOME}/docker:${HG_CONFIG_TEST_CLASSPATH}" ConfigReadback \ + "${TEST_HOME}/conf/rest-server.properties" "$1" + fi +} +assert_auth_readback() { + if [[ -n "${HG_CONFIG_TEST_CLASSPATH:-}" ]]; then + local secret_base64 admin_base64 + secret_base64=$(printf '%s' "$2" | base64 | tr -d '\n') + if [[ $# -eq 3 ]]; then + admin_base64=$(printf '%s' "$3" | base64 | tr -d '\n') + java -Xmx128m -cp "${TEST_HOME}/docker:${HG_CONFIG_TEST_CLASSPATH}" AuthConfigReadback \ + "$1" "${secret_base64}" "${admin_base64}" + else + java -Xmx128m -cp "${TEST_HOME}/docker:${HG_CONFIG_TEST_CLASSPATH}" AuthConfigReadback \ + "$1" "${secret_base64}" + fi + fi +} + ( cd "${TEST_HOME}" HG_SERVER_BACKEND=hstore \ @@ -200,7 +236,7 @@ reused_complex_secret=$(sed -n 's/^auth\.token_secret=//p' \ "${TEST_HOME}/conf/rest-server.properties") [[ "${reused_complex_secret}" == "${complex_secret}" ]] [[ "${reused_complex_secret}" == \ - 'Strong\\Secret\ 9!0123456789abcdef' ]] + 'Strong\\Secret\u00209!0123456789abcdef' ]] ( cd "${TEST_HOME}" @@ -217,7 +253,7 @@ trailing_space_secret=$(sed -n 's/^auth\.token_secret=//p' \ reused_trailing_space_secret=$(sed -n 's/^auth\.token_secret=//p' \ "${TEST_HOME}/conf/rest-server.properties") [[ "${trailing_space_secret}" == \ - 'SecretEnds\ 0123456789abcdefABCDE\ ' ]] + 'SecretEnds\u00200123456789abcdefABCDE\u0020' ]] [[ "${reused_trailing_space_secret}" == "${trailing_space_secret}" ]] grep -Fqx 'auth.admin_pa=pa' \ "${TEST_HOME}/conf/rest-server.properties" @@ -226,7 +262,7 @@ grep -Fqx 'auth.admin_pa=pa' \ cd "${TEST_HOME}" PASSWORD='Strong\Pass 9!' bash ./docker-entrypoint.sh ) -grep -Fqx 'auth.admin_pa=Strong\\Pass\ 9!' \ +grep -Fqx 'auth.admin_pa=Strong\\Pass\u00209!' \ "${TEST_HOME}/conf/rest-server.properties" rm -f "${TEST_HOME}/docker/init_complete" @@ -324,4 +360,204 @@ done ) assert_start_timeout 120 +# Identity mappings apply on every start and leave mounted config alone when unset. +identity_conf="${TEST_HOME}/conf/rest-server.properties" +# Earlier secret-escaping fixtures leave escaped trailing whitespace in this file. +# Start identity cases with their own parseable configuration. +printf 'restserver.url=http://127.0.0.1:8080\nserver.service_id=mounted-service\nserver.graphspace=mounted_space\n' \ + > "${identity_conf}" +(cd "${TEST_HOME}"; bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=mounted-service' "${identity_conf}" +grep -qx 'server.graphspace=mounted_space' "${identity_conf}" +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID=DEFAULT HG_SERVER_GRAPHSPACE=DEFAULT \ + bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=DEFAULT' "${identity_conf}" +grep -qx 'server.graphspace=DEFAULT' "${identity_conf}" +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID=service-a HG_SERVER_GRAPHSPACE=space_a bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=service-a' "${identity_conf}" +grep -qx 'server.graphspace=space_a' "${identity_conf}" +(cd "${TEST_HOME}"; SERVICE_ID=legacy-service GRAPH_SPACE=legacy_space bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=legacy-service' "${identity_conf}" +grep -qx 'server.graphspace=legacy_space' "${identity_conf}" +(cd "${TEST_HOME}"; SERVICE_ID=legacy-service GRAPH_SPACE=legacy_space \ + HG_SERVER_SERVICE_ID=service-b HG_SERVER_GRAPHSPACE=space_b bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=service-b' "${identity_conf}" +grep -qx 'server.graphspace=space_b' "${identity_conf}" +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID= HG_SERVER_GRAPHSPACE= bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=service-b' "${identity_conf}" +grep -qx 'server.graphspace=space_b' "${identity_conf}" + +# Empty new variables still allow the legacy driver values to apply. +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID= HG_SERVER_GRAPHSPACE= \ + SERVICE_ID=legacy-empty GRAPH_SPACE=legacy_empty bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=legacy-empty' "${identity_conf}" +grep -qx 'server.graphspace=legacy_empty' "${identity_conf}" + +# Service permits spaces; Unicode escapes round-trip through the Server parser. +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID='ops team' bash ./docker-entrypoint.sh) +grep -Fxq 'server.service_id=ops\u0020team' "${identity_conf}" +assert_service_readback 'ops team' +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID=' ops team ' bash ./docker-entrypoint.sh) +grep -Fxq 'server.service_id=\u0020ops\u0020team\u0020' "${identity_conf}" +assert_service_readback ' ops team ' +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID='ops\ team' bash ./docker-entrypoint.sh) +grep -Fxq 'server.service_id=ops\\\u0020team' "${identity_conf}" +assert_service_readback 'ops\ team' + +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID=' ops\ team ' bash ./docker-entrypoint.sh) +assert_service_readback ' ops\ team ' +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID='ops\u0020team' bash ./docker-entrypoint.sh) +assert_service_readback 'ops\u0020team' +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID='x-$(POD_IP)-$$(POD_IP)-$name-$' bash ./docker-entrypoint.sh) +assert_service_readback 'x-$(POD_IP)-$$(POD_IP)-$name-$' + +# Unsupported effective identity input must fail before changing mounted config. +cp "${identity_conf}" "${identity_conf}.before-invalid-identity" +for identity_name in HG_SERVER_SERVICE_ID HG_SERVER_GRAPHSPACE SERVICE_ID GRAPH_SPACE; do + for identity_value in 'café' '😀' $'ops\tteam' $'ops\nteam'; do + if (cd "${TEST_HOME}"; env "${identity_name}=${identity_value}" \ + bash ./docker-entrypoint.sh > ./docker/invalid-identity.log 2>&1); then + echo "unsupported ${identity_name} unexpectedly succeeded" >&2 + exit 1 + fi + grep -q 'must be printable ASCII' "${TEST_HOME}/docker/invalid-identity.log" + cmp "${identity_conf}.before-invalid-identity" "${identity_conf}" + done +done +# Ignored legacy input does not override a supported non-empty new value. +(cd "${TEST_HOME}"; HG_SERVER_SERVICE_ID=analytics SERVICE_ID='café' \ + HG_SERVER_GRAPHSPACE=team_a GRAPH_SPACE='😀' bash ./docker-entrypoint.sh) +grep -qx 'server.service_id=analytics' "${identity_conf}" +grep -qx 'server.graphspace=team_a' "${identity_conf}" + +# Apply the existing graph-space naming rule to effective new and legacy input. +graph_space_max=$(printf 'a%.0s' {1..48}) +cp "${identity_conf}" "${identity_conf}.before-invalid-graphspace" +for graphspace_env in HG_SERVER_GRAPHSPACE GRAPH_SPACE; do + for graphspace in TeamA 1team team-a "${graph_space_max}a"; do + if (cd "${TEST_HOME}"; env "${graphspace_env}=${graphspace}" \ + bash ./docker-entrypoint.sh > ./docker/invalid-graphspace.log 2>&1); then + echo "invalid ${graphspace_env} unexpectedly succeeded" >&2 + exit 1 + fi + grep -q 'HG_SERVER_GRAPHSPACE must be DEFAULT' "${TEST_HOME}/docker/invalid-graphspace.log" + cmp "${identity_conf}.before-invalid-graphspace" "${identity_conf}" + done + for graphspace in DEFAULT team_a "${graph_space_max}"; do + (cd "${TEST_HOME}"; env "${graphspace_env}=${graphspace}" bash ./docker-entrypoint.sh) + grep -Fxq "server.graphspace=${graphspace}" "${identity_conf}" + done + cp "${identity_conf}" "${identity_conf}.before-invalid-graphspace" +done +(cd "${TEST_HOME}"; HG_SERVER_GRAPHSPACE=team_a GRAPH_SPACE=TeamA bash ./docker-entrypoint.sh) +grep -qx 'server.graphspace=team_a' "${identity_conf}" + +# Some installed locales make [a-z] include uppercase letters; validation owns its locale. +for identity_locale in sk_SK.ISO8859-2 cs_CZ.ISO8859-2; do + case "$(LC_ALL="${identity_locale}" locale charmap 2>/dev/null)" in + ISO8859-2 | ISO-8859-2) ;; + *) continue ;; + esac + cp "${identity_conf}" "${identity_conf}.before-invalid-locale" + for graphspace_env in HG_SERVER_GRAPHSPACE GRAPH_SPACE; do + if (cd "${TEST_HOME}"; env LC_ALL="${identity_locale}" "${graphspace_env}=TeamA" \ + bash ./docker-entrypoint.sh > ./docker/invalid-graphspace-locale.log 2>&1); then + echo "invalid ${graphspace_env} under ${identity_locale} unexpectedly succeeded" >&2 + exit 1 + fi + grep -q 'HG_SERVER_GRAPHSPACE must be DEFAULT' "${TEST_HOME}/docker/invalid-graphspace-locale.log" + cmp "${identity_conf}.before-invalid-locale" "${identity_conf}" + done +done + +# Signing keys are opaque strings: both files must load exactly what the caller supplied. +fake_key='FAKE_SIGNING_KEY_0123456789abcdef' +control_soh=$(printf '\001') +control_us=$(printf '\037') +min_key="${fake_key:0:31}${control_soh}" +[[ ${#min_key} -eq 32 ]] +auth_secrets=("${fake_key}" " ${fake_key}" "${fake_key} inner" "${fake_key} " + "${fake_key}"$'\t' "${fake_key}\\" "${fake_key}café" "${fake_key}中文" "${fake_key}😀" + "${fake_key}\\u0020😀 café " "${fake_key}"$'\377' + "${control_soh}${fake_key}" "${fake_key}${control_soh}" "${fake_key}"$'\v' "${fake_key}${control_us}" + "${min_key}") +fake_admin_password="${control_soh}"' fake\ admin '"${control_us}" +for auth_secret in "${auth_secrets[@]}"; do + cp "${SCRIPT_DIR}/../src/assembly/static/conf/rest-server.properties" \ + "${TEST_HOME}/conf/rest-server.properties" + (cd "${TEST_HOME}"; PASSWORD="${fake_admin_password}" HG_SERVER_AUTH_TOKEN_SECRET="${auth_secret}" \ + bash ./docker-entrypoint.sh) + expected_secret="${auth_secret}" + if [[ "${auth_secret}" == "${fake_key}"$'\377' ]]; then + expected_secret="${fake_key}ÿ" + fi + assert_auth_readback "${TEST_HOME}/conf/rest-server.properties" "${expected_secret}" "${fake_admin_password}" + assert_auth_readback "${TEST_HOME}/conf/graphs/hugegraph.properties" "${expected_secret}" + cp "${TEST_HOME}/conf/rest-server.properties" "${TEST_HOME}/conf/rest-server.properties.before-reuse" + cp "${TEST_HOME}/conf/graphs/hugegraph.properties" "${TEST_HOME}/conf/graphs/hugegraph.properties.before-reuse" + (cd "${TEST_HOME}"; PASSWORD="${fake_admin_password}" bash ./docker-entrypoint.sh) + cmp "${TEST_HOME}/conf/rest-server.properties.before-reuse" "${TEST_HOME}/conf/rest-server.properties" + cmp "${TEST_HOME}/conf/graphs/hugegraph.properties.before-reuse" "${TEST_HOME}/conf/graphs/hugegraph.properties" + assert_auth_readback "${TEST_HOME}/conf/rest-server.properties" "${expected_secret}" "${fake_admin_password}" +done + +(cd "${TEST_HOME}"; LC_ALL=en_US.UTF-8 PASSWORD='locale probe' \ + HG_SERVER_AUTH_TOKEN_SECRET="${fake_key}" bash ./docker-entrypoint.sh) +[[ "$(cat "${TEST_HOME}/docker/init-store-locale")" == 'en_US.UTF-8' ]] + +# Unset discovery variables leave the shipped defaults alone. +if grep -qE '^server\.(urls_to_pd|deploy_in_k8s)' \ + "${TEST_HOME}/conf/rest-server.properties"; then + echo "unset discovery variables wrote server.* keys" >&2 + exit 1 +fi + +( + cd "${TEST_HOME}" + HG_SERVER_URLS_TO_PD=' http://server-0:8080, http://server-1:8080 ' \ + HG_SERVER_DEPLOY_IN_K8S=' TRUE ' \ + bash ./docker-entrypoint.sh +) +grep -qx 'server.urls_to_pd=http://server-0:8080,http://server-1:8080' \ + "${TEST_HOME}/conf/rest-server.properties" +grep -qx 'server.deploy_in_k8s=true' "${TEST_HOME}/conf/rest-server.properties" + +# A restart with new values replaces each key instead of appending another. +( + cd "${TEST_HOME}" + HG_SERVER_URLS_TO_PD=http://server-2:8080 \ + HG_SERVER_DEPLOY_IN_K8S=off \ + bash ./docker-entrypoint.sh +) +[[ "$(grep -c '^server\.urls_to_pd' "${TEST_HOME}/conf/rest-server.properties")" -eq 1 ]] +[[ "$(grep -c '^server\.deploy_in_k8s' "${TEST_HOME}/conf/rest-server.properties")" -eq 1 ]] +grep -qx 'server.urls_to_pd=http://server-2:8080' \ + "${TEST_HOME}/conf/rest-server.properties" +grep -qx 'server.deploy_in_k8s=off' "${TEST_HOME}/conf/rest-server.properties" + +# HugeConfig rejects 0 and 1 for a boolean, so the entrypoint refuses them +# before writing either conf file, the URL list included. The graph conf holds +# backend=hstore by now, so a different backend makes its cmp able to fail. +cp "${TEST_HOME}/conf/rest-server.properties" \ + "${TEST_HOME}/conf/rest-server.properties.before-k8s-flag" +cp "${TEST_HOME}/conf/graphs/hugegraph.properties" \ + "${TEST_HOME}/conf/graphs/hugegraph.properties.before-k8s-flag" +for invalid_k8s_flag in 1 0 enabled "t rue" "tr ue"; do + if ( + cd "${TEST_HOME}" + HG_SERVER_BACKEND=rocksdb \ + HG_SERVER_URLS_TO_PD=http://server-3:8080 \ + HG_SERVER_DEPLOY_IN_K8S="${invalid_k8s_flag}" \ + bash ./docker-entrypoint.sh + ); then + echo "HG_SERVER_DEPLOY_IN_K8S '${invalid_k8s_flag}' unexpectedly succeeded" >&2 + exit 1 + fi + cmp "${TEST_HOME}/conf/rest-server.properties.before-k8s-flag" \ + "${TEST_HOME}/conf/rest-server.properties" + cmp "${TEST_HOME}/conf/graphs/hugegraph.properties.before-k8s-flag" \ + "${TEST_HOME}/conf/graphs/hugegraph.properties" +done + echo "PASS: Docker entrypoint configures HStore discovery and authentication" +bash "${SCRIPT_DIR}/test/test-docker-entrypoint-process.sh" diff --git a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh index b5ba2de34f..d360d84a68 100755 --- a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh +++ b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh @@ -26,11 +26,13 @@ mkdir -p "${DOCKER_FOLDER}" log() { echo "[hugegraph-server-entrypoint] $*"; } +# Keep typed/list mappings on their existing encoder; auth strings use native-reader escapes below. +# TODO: relax Helm's password-space guard once its minimum Server image includes this auth encoder. encode_prop_value() { local value="$1" encoded="" char local i - LC_ALL=C + local LC_ALL=C for ((i = 0; i < ${#value}; i++)); do char="${value:i:1}" case "${char}" in @@ -68,6 +70,24 @@ set_prop() { set_prop_encoded "$key" "$(encode_prop_value "$val")" "$file" } +# Auth values are strings, not typed/list options. Commons keeps escaped spaces +# and trims a physical trailing space before detecting line continuation. +encode_auth_prop_value() { + local value="$1" encoding=UTF-8 + if (( $(printf '%s' "${value}" | LC_ALL=C tr -d '\040-\176' | wc -c) > 0 )); then + # Retain legacy byte values that the ISO-8859-1 reader already accepted. + if ! printf '%s' "${value}" | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1; then + encoding=ISO-8859-1 + fi + # The Server properties reader is ISO-8859-1. Standard UTF-16 escapes + # preserve UTF-8 input and prevent trimming boundary control characters. + printf '%s' "${value}" | iconv -f "${encoding}" -t UTF-16BE | od -An -v -tx1 | \ + tr -d ' \n' | sed 's/..../\\u&/g' + else + encode_prop_value "${value}" | sed 's/\\ /\\u0020/g' + fi +} + get_prop_encoded() { local key="$1" file="$2" local esc_key @@ -89,10 +109,31 @@ migrate_env() { migrate_env "BACKEND" "HG_SERVER_BACKEND" migrate_env "PD_PEERS" "HG_SERVER_PD_PEERS" +migrate_env "SERVICE_ID" "HG_SERVER_SERVICE_ID" +migrate_env "GRAPH_SPACE" "HG_SERVER_GRAPHSPACE" + +# The Server reads these properties as ISO-8859-1. Reject unsupported input +# before writing UTF-8 bytes that would select a different service identity. +for identity_name in HG_SERVER_SERVICE_ID HG_SERVER_GRAPHSPACE; do + if [[ -n "${!identity_name:-}" ]] && + (( $(printf '%s' "${!identity_name}" | LC_ALL=C tr -d '\040-\176' | wc -c) > 0 )); then + log "ERROR: ${identity_name} must be printable ASCII (bytes 0x20-0x7E)" + exit 1 + fi +done + +# Match the Server API graph-space naming contract after legacy migration. +if [[ -n "${HG_SERVER_GRAPHSPACE:-}" ]] && ! ( + export LC_ALL=C + [[ "${HG_SERVER_GRAPHSPACE}" =~ ^(DEFAULT|[a-z][a-z0-9_]{0,47})$ ]] +); then + log "ERROR: HG_SERVER_GRAPHSPACE must be DEFAULT or a name starting with" \ + "a lowercase letter, followed by lowercase letters, digits or underscores (1-48 characters)" + exit 1 +fi if [[ -n "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then - LC_ALL=C - if (( ${#HG_SERVER_AUTH_TOKEN_SECRET} < 32 )); then + if (( $(printf '%s' "${HG_SERVER_AUTH_TOKEN_SECRET}" | LC_ALL=C wc -c) < 32 )); then log "ERROR: HG_SERVER_AUTH_TOKEN_SECRET must be at least 32 bytes" exit 1 fi @@ -145,7 +186,36 @@ if [[ -n "${PASSWORD:-}" && -z "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then fi fi +# Encode authentication strings before changing either configuration file. +if [[ -n "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then + AUTH_TOKEN_SECRET_ENCODED=$(encode_auth_prop_value "${HG_SERVER_AUTH_TOKEN_SECRET}") +fi +AUTH_ADMIN_PASSWORD_ENCODED="" +if [[ -n "${PASSWORD:-}" ]]; then + AUTH_ADMIN_PASSWORD_ENCODED=$(encode_auth_prop_value "${PASSWORD}") +fi + +# Same accepted spellings as HG_SERVER_INIT_STORE_ENABLED below, checked +# before the mapping so an invalid value leaves both conf files untouched. +# URLs hold no whitespace, and the Server splits the list on bare commas, so +# "a, b" would otherwise register an escaped space in front of the second URL. +URLS_TO_PD=$(printf '%s' "${HG_SERVER_URLS_TO_PD:-}" | tr -d '[:space:]') +# Only surrounding whitespace is trimmed, so a value such as "t rue" stays invalid. +DEPLOY_IN_K8S="${HG_SERVER_DEPLOY_IN_K8S:-}" +DEPLOY_IN_K8S="${DEPLOY_IN_K8S#"${DEPLOY_IN_K8S%%[![:space:]]*}"}" +DEPLOY_IN_K8S="${DEPLOY_IN_K8S%"${DEPLOY_IN_K8S##*[![:space:]]}"}" +DEPLOY_IN_K8S=$(printf '%s' "${DEPLOY_IN_K8S}" | tr '[:upper:]' '[:lower:]') +case "${DEPLOY_IN_K8S}" in + "" | y | t | yes | on | true | n | f | no | off | false) ;; + *) log "ERROR: invalid HG_SERVER_DEPLOY_IN_K8S '${HG_SERVER_DEPLOY_IN_K8S}'" + exit 1 ;; +esac + # ── Map env → properties file ───────────────────────────────────────── +# TODO: drop the Helm chart's (helm/hugegraph) rest-server.properties wrapper once its minimum +# Server image has the server.urls_to_pd and server.deploy_in_k8s mappings below. A read-only root +# filesystem also needs the conf files on a writable path, since set_prop in this script edits them +# in place; the chart refuses readOnlyRootFilesystem until then. [[ -n "${HG_SERVER_BACKEND:-}" ]] && set_prop "backend" "${HG_SERVER_BACKEND}" "${GRAPH_CONF}" [[ -n "${HG_SERVER_PD_PEERS:-}" ]] && set_prop "pd.peers" "${HG_SERVER_PD_PEERS}" "${GRAPH_CONF}" [[ -n "${HG_SERVER_USE_PD:-}" ]] && \ @@ -154,22 +224,32 @@ fi set_prop "pd.peers" "${HG_SERVER_PD_PEERS}" "${REST_SERVER_CONF}" [[ -n "${HG_SERVER_CLUSTER:-}" ]] && \ set_prop "cluster" "${HG_SERVER_CLUSTER}" "${REST_SERVER_CONF}" +if [[ -n "${HG_SERVER_SERVICE_ID:-}" ]]; then + # Service names can contain spaces. Commons Configuration keeps a backslash + # before a literal space, but decodes Unicode escapes, including at the edges. + service_id_encoded=$(encode_prop_value "${HG_SERVER_SERVICE_ID}" | sed 's/\\ /\\u0020/g') + set_prop_encoded "server.service_id" "${service_id_encoded}" "${REST_SERVER_CONF}" +fi +[[ -n "${HG_SERVER_GRAPHSPACE:-}" ]] && \ + set_prop "server.graphspace" "${HG_SERVER_GRAPHSPACE}" "${REST_SERVER_CONF}" [[ -n "${HG_SERVER_REST_URL:-}" ]] && set_prop "restserver.url" \ "${HG_SERVER_REST_URL}" "${REST_SERVER_CONF}" +# The address list this Server registers in PD, read only when +# server.deploy_in_k8s is true; otherwise it registers restserver.url. +[[ -n "${URLS_TO_PD}" ]] && set_prop "server.urls_to_pd" \ + "${URLS_TO_PD}" "${REST_SERVER_CONF}" +[[ -n "${DEPLOY_IN_K8S}" ]] && set_prop "server.deploy_in_k8s" \ + "${DEPLOY_IN_K8S}" "${REST_SERVER_CONF}" [[ -n "${HG_SERVER_MIN_FREE_MEMORY:-}" ]] && set_prop "restserver.min_free_memory" \ "${HG_SERVER_MIN_FREE_MEMORY}" "${REST_SERVER_CONF}" -if [[ -n "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then - set_prop "auth.token_secret" "${HG_SERVER_AUTH_TOKEN_SECRET}" \ - "${REST_SERVER_CONF}" - set_prop "auth.token_secret" "${HG_SERVER_AUTH_TOKEN_SECRET}" "${GRAPH_CONF}" -elif [[ -n "${AUTH_TOKEN_SECRET_ENCODED}" ]]; then +if [[ -n "${AUTH_TOKEN_SECRET_ENCODED}" ]]; then set_prop_encoded "auth.token_secret" "${AUTH_TOKEN_SECRET_ENCODED}" \ "${REST_SERVER_CONF}" set_prop_encoded "auth.token_secret" "${AUTH_TOKEN_SECRET_ENCODED}" \ "${GRAPH_CONF}" fi if [[ -n "${PASSWORD:-}" ]]; then - set_prop "auth.admin_pa" "${PASSWORD}" "${REST_SERVER_CONF}" + set_prop_encoded "auth.admin_pa" "${AUTH_ADMIN_PASSWORD_ENCODED}" "${REST_SERVER_CONF}" # This script is idempotent and must run outside the initialization guard: # an upgrade can preserve the marker from an unauthenticated deployment. ./bin/enable-auth.sh @@ -240,19 +320,83 @@ else ./bin/init-store.sh fi -./bin/start-hugegraph.sh -j "${JAVA_OPTS:-}" -t "${SERVER_STARTUP_TIMEOUT_S}" +# Own the foreground launcher so wait can recover the JVM's actual status. +# A daemon JVM is not our child; watching its PID with tail loses that status. +source ./bin/util.sh +LAUNCHER_PID="" +READINESS_PID="" +PARTITION_PID="" +stop_server_and_exit() { + local exit_code="$1" + # dumb-init can signal the group before we forward to its children. + # Ignore duplicate signals while those children finish shutting down. + trap '' TERM INT + if [[ -n "$READINESS_PID" ]]; then + kill -TERM "$READINESS_PID" 2>/dev/null || true + fi + if [[ -n "$PARTITION_PID" ]]; then + kill -TERM "$PARTITION_PID" 2>/dev/null || true + fi + if [[ -n "$LAUNCHER_PID" ]]; then + kill -TERM "$LAUNCHER_PID" 2>/dev/null || true + fi + if [[ -n "$PARTITION_PID" ]]; then + wait "$PARTITION_PID" 2>/dev/null || true + fi + if [[ -n "$READINESS_PID" ]]; then + wait "$READINESS_PID" 2>/dev/null || true + fi + if [[ -n "$LAUNCHER_PID" ]]; then + wait "$LAUNCHER_PID" 2>/dev/null || true + fi + exit "$exit_code" +} +trap 'stop_server_and_exit 143' TERM +trap 'stop_server_and_exit 130' INT + +./bin/start-hugegraph.sh -d false -j "${JAVA_OPTS:-}" -t "${SERVER_STARTUP_TIMEOUT_S}" & +LAUNCHER_PID=$! +REST_SERVER_URL=$(read_property "$REST_SERVER_CONF" "restserver.url") +# GNU timeout owns the probe group, including a stalled curl. Background it +# so wait is interruptible; a foreground probe would defer our signal traps. +timeout "${SERVER_STARTUP_TIMEOUT_S}s" bash -c ' + source ./bin/util.sh + wait_for_startup "$@" +' bash "$LAUNCHER_PID" 'HugeGraphServer' "$REST_SERVER_URL/graphs" \ + "$SERVER_STARTUP_TIMEOUT_S" & +READINESS_PID=$! +READY_STATUS=0 +wait "$READINESS_PID" || READY_STATUS=$? +READINESS_PID="" +if [[ "$READY_STATUS" -ne 0 ]]; then + log "ERROR: HugeGraphServer did not become ready; see the container logs" + if kill -0 "$LAUNCHER_PID" 2>/dev/null; then + # Keep the startup deadline even if the JVM ignores TERM. Container + # termination cleans up any remaining processes; don't wait forever. + kill -TERM "$LAUNCHER_PID" 2>/dev/null || true + exit 1 + fi + # Preserve an early failure, but a normal exit before readiness is still + # an unsuccessful start. + STATUS=0 + wait "$LAUNCHER_PID" || STATUS=$? + [[ "$STATUS" -ne 0 ]] || STATUS=1 + exit "$STATUS" +fi # Post-startup cluster stabilization check (hstore only — rocksdb has no partitions) ACTUAL_BACKEND=$(grep -E '^[[:space:]]*backend[[:space:]]*=' "${GRAPH_CONF}" | head -n 1 | sed 's/.*=//' | tr -d '[:space:]' || true) if [[ "${ACTUAL_BACKEND}" == "hstore" ]]; then STORE_REST="${STORE_REST:-store:8520}" export STORE_REST - ./bin/wait-partition.sh || log "WARN: partitions not assigned yet" + ./bin/wait-partition.sh & + PARTITION_PID=$! + wait "$PARTITION_PID" || log "WARN: partitions not assigned yet" + PARTITION_PID="" fi -PID=$(cat ./bin/pid 2>/dev/null || true) -if [[ -n "$PID" ]]; then - trap 'kill -TERM "$PID" 2>/dev/null; while kill -0 "$PID" 2>/dev/null; do sleep 1; done; exit 0' TERM INT - tail --pid="$PID" -f /dev/null - exit 1 +if wait "$LAUNCHER_PID"; then + exit 0 +else + exit $? fi diff --git a/hugegraph-server/hugegraph-dist/docker/test/AuthConfigReadback.java b/hugegraph-server/hugegraph-dist/docker/test/AuthConfigReadback.java new file mode 100644 index 0000000000..c07791fcc8 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/docker/test/AuthConfigReadback.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. + */ + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; + +import org.apache.hugegraph.auth.TokenGenerator; +import org.apache.hugegraph.config.AuthOptions; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.OptionSpace; + +public final class AuthConfigReadback { + + public static void main(String[] args) { + OptionSpace.register("auth", AuthOptions.instance()); + HugeConfig config = new HugeConfig(args[0]); + String expected = new String(Base64.getDecoder().decode(args[1]), StandardCharsets.UTF_8); + String actual = config.get(AuthOptions.AUTH_TOKEN_SECRET); + if (!expected.equals(actual)) { + throw new AssertionError("Signing key mismatch: expected characters=" + expected.length() + + ", loaded characters=" + actual.length()); + } + String token = new TokenGenerator(expected).create(Collections.singletonMap("probe", "auth"), 60000L); + if (!"auth".equals(new TokenGenerator(actual).verify(token).get("probe"))) { + throw new AssertionError("Signing key verification mismatch"); + } + if (args.length == 3) { + String admin = new String(Base64.getDecoder().decode(args[2]), StandardCharsets.UTF_8); + if (!admin.equals(config.getProperty("auth.admin_pa"))) { + throw new AssertionError("Administrator password mismatch"); + } + } + System.out.println("PASS: exact authentication property readback; signing key characters=" + actual.length()); + } +} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NotSupportException.java b/hugegraph-server/hugegraph-dist/docker/test/ConfigReadback.java similarity index 62% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NotSupportException.java rename to hugegraph-server/hugegraph-dist/docker/test/ConfigReadback.java index 50148f6b00..6c6b016bfc 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NotSupportException.java +++ b/hugegraph-server/hugegraph-dist/docker/test/ConfigReadback.java @@ -13,22 +13,19 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ -package org.apache.hugegraph.exception; - -import org.apache.hugegraph.HugeException; - -public class NotSupportException extends HugeException { +import org.apache.hugegraph.config.HugeConfig; - private static final long serialVersionUID = -2914329541122906234L; - private static final String PREFIX = "Not support "; - - public NotSupportException(String message) { - super(PREFIX + message); - } +public final class ConfigReadback { - public NotSupportException(String message, Object... args) { - super(PREFIX + message, args); + public static void main(String[] args) { + HugeConfig config = new HugeConfig(args[0]); + Object actual = config.getProperty("server.service_id"); + if (!args[1].equals(actual)) { + throw new AssertionError("Expected service ID [" + args[1] + "] but loaded [" + actual + "]"); + } + System.out.println("PASS: HugeConfig service ID readback [" + actual + "]"); } } diff --git a/hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint-process.sh b/hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint-process.sh new file mode 100644 index 0000000000..7a3a90e600 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint-process.sh @@ -0,0 +1,272 @@ +#!/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. +# +set -euo pipefail + +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd) +# Real shell parents and children, with only REST readiness and Java replaced. +# Stalled-probe cases use one temporary localhost socket; no JVM, Docker or DB. +python3 - "$REPO_ROOT" <<'PY' +import os +import pathlib +import shutil +import signal +import socket +import subprocess +import sys +import tempfile +import threading +import time + +repo = pathlib.Path(sys.argv[1]) +static = repo / 'hugegraph-server/hugegraph-dist/src/assembly/static/bin' +entrypoint = repo / 'hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh' + + +def await_file(path, process): + deadline = time.monotonic() + 5 + while not path.exists(): + assert process.poll() is None, 'entrypoint exited before ' + str(path) + assert time.monotonic() < deadline, 'fixture did not reach ' + str(path) + time.sleep(0.02) + + +with tempfile.TemporaryDirectory(prefix='hugegraph-entrypoint-process-') as directory: + for mode, expected in [('exit0', 0), ('exit42', 42), ('kill137', 137), + ('TERM', 143), ('INT', 130), ('groupTERM', 143), + ('early42', 42), ('early0', 1), ('timeout', 1), + ('timeout-ignore', 1), ('partition42', 42), + ('partitionTERM', 143), ('partitionINT', 130), + ('partitiongroupTERM', 143), ('readinessTERM', 143), + ('readinessINT', 130), ('readinessgroupTERM', 143), + ('partitionDiagnostic42', 42), ('partitionDiagnosticTERM', 143), + ('partitionDiagnosticINT', 130), ('partitionDiagnosticgroupTERM', 143)]: + work = pathlib.Path(directory) / mode + for name in ['bin', 'tools', 'conf/graphs', 'docker', 'logs']: + (work / name).mkdir(parents=True) + shutil.copy(entrypoint, work / 'entrypoint.sh') + for name in ['start-hugegraph.sh', 'util.sh']: + shutil.copy(static / name, work / 'bin' / name) + (work / 'docker/init_complete').touch() + backend = 'hstore' if mode.startswith('partition') else 'rocksdb' + (work / 'conf/graphs/hugegraph.properties').write_text('backend=' + backend + '\n') + (work / 'conf/rest-server.properties').write_text( + 'restserver.url=http://127.0.0.1:1\ngremlinserver.url=http://127.0.0.1:2\n') + listener = None + accepted, stop_http = threading.Event(), threading.Event() + if mode.startswith('partitionDiagnostic'): + listener = socket.socket() + listener.bind(('127.0.0.1', 0)) + listener.listen(2) + port = listener.getsockname()[1] + + def hold_partition_diagnostic(): + # Real curl first receives positive readiness, then stalls on + # the distinct final diagnostic endpoint. + for _ in range(2): + connection, _ = listener.accept() + request = connection.recv(8192) + if b' /v1/partitions ' in request: + accepted.set() + stop_http.wait(10) + connection.close() + return + body = b'{"partitionCount":1}' + response = b'HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n' % len(body) + connection.sendall(response + body) + connection.close() + + threading.Thread(target=hold_partition_diagnostic, daemon=True).start() + if mode.startswith('readiness'): + listener = socket.socket() + listener.bind(('127.0.0.1', 0)) + listener.listen(1) + port = listener.getsockname()[1] + (work / 'conf/rest-server.properties').write_text( + 'restserver.url=http://127.0.0.1:%d\ngremlinserver.url=http://127.0.0.1:2\n' % port) + # The fixture HTTP listener replaces the JVM listener. Skip only + # launcher port preflight; readiness still runs the real helper. + with (work / 'bin/util.sh').open('a') as utility: + utility.write('\ncheck_port() { :; }\n') + + def hold_http(): + connection, _ = listener.accept() + connection.recv(8192) + accepted.set() + stop_http.wait(10) + connection.close() + + threading.Thread(target=hold_http, daemon=True).start() + (work / 'bin/init-store.sh').write_text('#!/bin/bash\nexit 0\n') + (work / 'bin/wait-partition.sh').write_text( + '#!/bin/bash\ntouch ./partition-ran\nexit 7\n') + if mode.startswith('partition') and mode != 'partition42': + shutil.copy(static / 'wait-partition.sh', work / 'bin/wait-partition.sh') + (work / 'tools/curl').write_text('''#!/bin/bash +case "$TEST_CHILD_MODE" in + early* | timeout*) printf '000'; exit 28 ;; +esac +if [[ "$1" != '-I' && "$TEST_CHILD_MODE" == partitionDiagnostic* ]]; then + if [[ "$1" == '-v' ]]; then + echo "$$" > ./partition-curl.pid + echo "$PPID" > ./partition-timeout.pid + ps -o ppid= -p "$PPID" | tr -d ' ' > ./partition-wrapper.pid + touch ./partition-ran + fi + exec "$TEST_REAL_CURL" "$@" +fi +if [[ "$TEST_CHILD_MODE" == readiness* ]]; then + ancestor=$$ + for index in 1 2 3 4; do + echo "$ancestor" > "./readiness-owned${index}.pid" + name=$(ps -o comm= -p "$ancestor") + if [[ "${name##*/}" == *timeout ]]; then + echo "$ancestor" > ./readiness-timeout.pid + break + fi + ancestor=$(ps -o ppid= -p "$ancestor" | tr -d ' ') + done + exec "$TEST_REAL_CURL" "$@" +fi +if [[ "$1" != '-I' && "$TEST_CHILD_MODE" == partition* ]]; then + echo "$PPID" > ./partition-shell.pid + timeout_pid=$(ps -o ppid= -p "$PPID" | tr -d ' ') + echo "$timeout_pid" > ./partition-timeout.pid + ps -o ppid= -p "$timeout_pid" | tr -d ' ' > ./partition-wrapper.pid + touch ./partition-ran + printf '{"partitionCount":0}' + exit 0 +fi +touch ./ready-probe +printf '200' +''') + (work / 'bin/hugegraph-server.sh').write_text('''#!/usr/bin/env python3 +import os, pathlib, signal, sys, time +work = pathlib.Path.cwd() +mode = os.environ['TEST_CHILD_MODE'] +def stop(signum, frame): + signal.signal(signal.SIGTERM, signal.SIG_IGN) + (work / 'term-observed').touch() + # Expose a duplicate group signal killing a wrapper before it reaps us. + time.sleep(0.2) + sys.exit(143) +signal.signal(signal.SIGTERM, signal.SIG_IGN if mode == 'timeout-ignore' else stop) +(work / 'child-running').touch() +if mode.startswith('early'): + sys.exit(int(mode[5:])) +while not (work / 'child-exit').exists(): + time.sleep(0.02) +sys.exit(int((work / 'child-exit').read_text())) +''') + for folder in ['bin', 'tools']: + for script in (work / folder).iterdir(): + script.chmod(0o755) + env = {key: value for key, value in os.environ.items() + if not key.startswith('HG_SERVER_') and key not in ['PASSWORD', 'BACKEND', 'PD_PEERS']} + env.update(PATH=str(work / 'tools') + ':' + os.environ['PATH'], + TEST_CHILD_MODE=mode, HG_SERVER_STARTUP_TIMEOUT_S='3', + TEST_REAL_CURL=shutil.which('curl'), NO_PROXY='127.0.0.1', no_proxy='127.0.0.1') + if mode.startswith('readiness'): + env['HG_SERVER_STARTUP_TIMEOUT_S'] = '60' + if mode.startswith('partitionDiagnostic'): + env['STORE_REST'] = '127.0.0.1:%d' % port + env['WAIT_PARTITION_TIMEOUT_S'] = '1' if mode.endswith('42') else '10' + started = time.monotonic() + with (work / 'output').open('wb') as output: + process = subprocess.Popen(['bash', './entrypoint.sh'], cwd=work, env=env, + stdout=output, stderr=subprocess.STDOUT, + start_new_session=True) + try: + if mode.startswith('timeout'): + await_file(work / 'child-running', process) + if mode.startswith('readiness'): + await_file(work / 'child-running', process) + assert accepted.wait(5), 'real curl did not reach stalled HTTP fixture' + if mode == 'readinessgroupTERM': + os.killpg(process.pid, signal.SIGTERM) + else: + signum = signal.SIGINT if mode.endswith('INT') else signal.SIGTERM + os.kill(process.pid, signum) + elif not mode.startswith(('early', 'timeout')): + await_file(work / 'child-running', process) + await_file(work / 'ready-probe', process) + if mode.startswith('partition'): + await_file(work / 'partition-ran', process) + if mode.startswith('partitionDiagnostic'): + assert accepted.wait(5), 'final real curl did not reach the stalled endpoint' + if mode in ['TERM', 'INT', 'partitionTERM', 'partitionINT', + 'partitionDiagnosticTERM', 'partitionDiagnosticINT']: + signum = signal.SIGINT if mode.endswith('INT') else signal.SIGTERM + os.kill(process.pid, signum) + elif mode in ['groupTERM', 'partitiongroupTERM', 'partitionDiagnosticgroupTERM']: + os.killpg(process.pid, signal.SIGTERM) + elif mode == 'kill137': + os.kill(int((work / 'bin/pid').read_text()), signal.SIGKILL) + else: + status = 42 if mode in ['partition42', 'partitionDiagnostic42'] else int(mode[4:]) + (work / 'child-exit').write_text(str(status)) + actual = process.wait(timeout=5) + assert actual == expected, (mode, expected, actual, (work / 'output').read_text()) + if mode.startswith('timeout'): + assert time.monotonic() - started < 5, 'startup cleanup exceeded the deadline' + if mode in ['TERM', 'INT', 'groupTERM', 'partitionTERM', 'partitionINT', + 'partitiongroupTERM', 'readinessTERM', 'readinessINT', + 'readinessgroupTERM', 'partitionDiagnosticTERM', + 'partitionDiagnosticINT', 'partitionDiagnosticgroupTERM']: + assert (work / 'term-observed').exists(), 'TERM did not reach the JVM substitute' + if not mode.startswith('timeout'): + owned = ([work / 'bin/pid'] + list(work.glob('partition-*.pid')) + + list(work.glob('readiness-*.pid'))) + for pid_file in owned: + child = int(pid_file.read_text()) + try: + os.kill(child, 0) + except ProcessLookupError: + pass + else: + raise AssertionError(str(pid_file) + ' survived entrypoint exit') + if mode in ['partition42', 'partitionDiagnostic42']: + assert 'WARN: partitions not assigned yet' in (work / 'output').read_text() + print('PASS: ' + mode + ' -> ' + str(actual)) + finally: + # Timeout intentionally permits an unresponsive process until + # container termination. The fixture owns and kills that group. + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + # GNU timeout owns a separate process group. Clean it too if + # a regression prevents the wrapper from forwarding shutdown. + for pid_file in [work / 'partition-timeout.pid', work / 'readiness-timeout.pid']: + if pid_file.exists(): + try: + os.killpg(int(pid_file.read_text()), signal.SIGKILL) + except ProcessLookupError: + pass + for pid_file in list(work.glob('partition-*.pid')) + list(work.glob('readiness-*.pid')): + try: + os.kill(int(pid_file.read_text()), signal.SIGKILL) + except ProcessLookupError: + pass + process.wait(timeout=2) + if listener is not None: + stop_http.set() + listener.close() +PY + +# This process fixture is already invoked by the Docker/Server CI entrypoint tests. +bash "$(dirname "${BASH_SOURCE[0]}")/test-wait-partition.sh" diff --git a/hugegraph-server/hugegraph-dist/docker/test/test-wait-partition.sh b/hugegraph-server/hugegraph-dist/docker/test/test-wait-partition.sh new file mode 100644 index 0000000000..b91601036e --- /dev/null +++ b/hugegraph-server/hugegraph-dist/docker/test/test-wait-partition.sh @@ -0,0 +1,123 @@ +#!/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. +# +set -euo pipefail + +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd) +SCRIPT=${1:-"$REPO_ROOT/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-partition.sh"} +# One real loopback socket and GNU timeout; no JVM, database or Docker. +python3 - "$SCRIPT" <<'PY' +import os +import pathlib +import shutil +import signal +import socket +import subprocess +import sys +import tempfile +import threading +import time + +script = pathlib.Path(sys.argv[1]).resolve() +with tempfile.TemporaryDirectory(prefix='hugegraph-partition-diagnostic-') as directory: + for mode, expected in [('healthy', 0), ('stalled', 124), ('TERM', 143), ('groupTERM', 143)]: + work = pathlib.Path(directory) / mode + work.mkdir() + tools = work / 'tools' + tools.mkdir() + (tools / 'curl').write_text('''#!/bin/bash +if [[ "$1" == '-v' ]]; then + echo "$$" > ./curl.pid + echo "$PPID" > ./timeout.pid +fi +exec "$REAL_CURL" "$@" +''') + (tools / 'curl').chmod(0o755) + listener = socket.socket() + listener.bind(('127.0.0.1', 0)) + listener.listen(2) + accepted, stop = threading.Event(), threading.Event() + + def serve(): + for _ in range(2): + connection, _ = listener.accept() + request = connection.recv(8192) + if b' /v1/partitions ' in request: + accepted.set() + if mode != 'healthy': + stop.wait(10) + connection.close() + return + body = b'{"partitions":[]}' + else: + body = b'{"partitionCount":1}' + response = b'HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n' % len(body) + connection.sendall(response + body) + connection.close() + + threading.Thread(target=serve, daemon=True).start() + env = dict(os.environ, STORE_REST='127.0.0.1:%d' % listener.getsockname()[1], + WAIT_PARTITION_TIMEOUT_S='1' if mode == 'stalled' else '10', + PATH=str(tools) + ':' + os.environ['PATH'], REAL_CURL=shutil.which('curl'), + NO_PROXY='127.0.0.1', no_proxy='127.0.0.1') + started = time.monotonic() + with (work / 'output').open('wb') as output: + process = subprocess.Popen(['bash', str(script)], cwd=work, env=env, + stdout=output, stderr=subprocess.STDOUT, start_new_session=True) + try: + assert accepted.wait(3), 'final diagnostic did not reach the real socket' + if mode == 'TERM': + os.kill(process.pid, signal.SIGTERM) + elif mode == 'groupTERM': + os.killpg(process.pid, signal.SIGTERM) + actual = process.wait(timeout=4) + assert actual == expected, (mode, expected, actual, (work / 'output').read_text()) + for pid_file in [work / 'curl.pid', work / 'timeout.pid']: + owned = int(pid_file.read_text()) + try: + os.kill(owned, 0) + except ProcessLookupError: + pass + else: + raise AssertionError(str(pid_file) + ' survived helper exit') + print('PASS: final diagnostic %s -> %d (%.2fs); curl/timeout reaped' % + (mode, actual, time.monotonic() - started)) + finally: + # A regression may leave GNU timeout in its own process group. + # Only this fixture's recorded process IDs/groups are touched. + for pid_file in [work / 'timeout.pid', work / 'curl.pid']: + if pid_file.exists(): + owned = int(pid_file.read_text()) + try: + os.killpg(owned, signal.SIGKILL) + except ProcessLookupError: + pass + try: + os.kill(owned, signal.SIGKILL) + except ProcessLookupError: + pass + try: + process.wait(timeout=0.5) + except subprocess.TimeoutExpired: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait(timeout=2) + stop.set() + listener.close() +PY diff --git a/hugegraph-server/hugegraph-dist/pom.xml b/hugegraph-server/hugegraph-dist/pom.xml index c4575bc877..810bd281be 100644 --- a/hugegraph-server/hugegraph-dist/pom.xml +++ b/hugegraph-server/hugegraph-dist/pom.xml @@ -183,6 +183,33 @@ maven-antrun-plugin 1.8 + + prepare-arthas + prepare-package + + run + + + + + + + + + + + + + + + + + + + + download-swagger-ui prepare-package diff --git a/hugegraph-server/hugegraph-dist/src/assembly/descriptor/assembly.xml b/hugegraph-server/hugegraph-dist/src/assembly/descriptor/assembly.xml index b7f9ee0487..bf5db512fd 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/descriptor/assembly.xml +++ b/hugegraph-server/hugegraph-dist/src/assembly/descriptor/assembly.xml @@ -23,6 +23,10 @@ + + ${project.build.directory}/arthas + lib/arthas + ${assembly.static.dir}/bin bin diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/ArthasAttach.java b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/ArthasAttach.java new file mode 100644 index 0000000000..521d3536f9 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/ArthasAttach.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. + */ + +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Pattern; + +import com.sun.tools.attach.VirtualMachine; +import com.taobao.arthas.core.config.FeatureCodec; + +class ArthasAttach { + public static void main(String[] args) throws Exception { + String ip = args[5]; + InetAddress address = InetAddress.getByName(ip); + // Prefer native IPv4 loopback access for wildcard listeners, including dual-stack ::. + String connectIp = address.isAnyLocalAddress() ? "127.0.0.1" : ip; + Files.writeString(Path.of(args[4]).resolveSibling("connect-ip"), connectIp); + Path home = Path.of(args[1]); + Path top = Path.of(args[3]); + VirtualMachine vm = VirtualMachine.attach(args[0]); + Path configFile = null; + try { + Properties target = vm.getSystemProperties(); + String command = target.getProperty("sun.java.command", ""); + String main = "org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap"; + boolean server = Files.isRegularFile(top.resolve("bin/start-hugegraph.sh")) && + (command.equals(main) || command.startsWith(main + " ")); + boolean store = Files.isRegularFile(top.resolve("bin/start-hugegraph-store.sh")) && + (command.matches(Pattern.quote(top.resolve("lib/hg-store-node-").toString()) + + "[^/]+[.]jar(?: .*|$)") || + (command.matches("org[.]springframework[.]boot[.]loader[.]JarLauncher(?: .*|$)") && + "HugeGraphStore".equals(target.getProperty("name")))); + if (!server && !store) { + throw new IllegalArgumentException("PID does not identify a Server or Store in this distribution"); + } + // Allow exit cleanup only after this distribution's target has been validated. + Files.createFile(Path.of(args[4])); + // FeatureCodec drops blank values; a properties file preserves them. + configFile = Files.createTempFile("hugegraph-arthas-", ".properties"); + Properties settings = new Properties(); + settings.setProperty("arthas.config.overrideAll", "true"); + settings.setProperty("arthas.ip", ip); + settings.setProperty("arthas.telnetPort", args[2]); + settings.setProperty("arthas.httpPort", "-1"); + settings.setProperty("arthas.mcpEndpoint", ""); + settings.setProperty("arthas.tunnelServer", ""); + settings.setProperty("arthas.disabledCommands", ""); + settings.setProperty("arthas.sessionTimeout", "1800"); + if (!args[6].isEmpty() || !args[7].isEmpty()) { + settings.setProperty("arthas.username", args[6].isEmpty() ? "arthas" : args[6]); + } + if (!args[7].isEmpty()) { + settings.setProperty("arthas.password", args[7]); + } + settings.setProperty("arthas.localConnectionNonAuth", + target.getProperty("arthas.localConnectionNonAuth", "true")); + try (OutputStream output = Files.newOutputStream(configFile)) { + settings.store(output, null); + } + Map config = new LinkedHashMap<>(); + config.put("config.overrideAll", "true"); + config.put("config.location", configFile.toString()); + config.put("home", home.toString()); + String agentArgs = home.resolve("arthas-core.jar") + ";" + + FeatureCodec.DEFAULT_COMMANDLINE_CODEC.toString(config); + vm.loadAgent(home.resolve("arthas-agent.jar").toString(), + URLEncoder.encode(agentArgs, StandardCharsets.UTF_8)); + } finally { + vm.detach(); + if (configFile != null) { + Files.deleteIfExists(configFile); + } + } + } +} diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/arthas.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/arthas.sh new file mode 100755 index 0000000000..3b20dcdc64 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/arthas.sh @@ -0,0 +1,232 @@ +#!/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. +# +# Offline attachment to a running Server or Store using the bundled agent. +set -euo pipefail + +fail() { echo "Error: $*" >&2; exit 1; } +usage() { + echo "Usage: $0 [--pid PID] [--telnet-port PORT] [--target-ip ADDRESS] [--command COMMAND]" + echo " [--username USER] [--password PASSWORD]" + echo "Run as the service OS user; defaults to bin/pid and loopback port 3658." + echo "New agents disable HTTP/MCP; use the existing telnet port for an active agent." + echo "Exiting this helper stops Arthas, including after --command or quit; the JVM keeps running." +} + +BIN=$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd) +TOP=$(cd -P "$BIN/.." && pwd) +pid="" +port=3658 +target_ip=127.0.0.1 +username="" +password="" +command="" +boot_args=() +while [ "$#" -gt 0 ]; do + case "$1" in + --pid|--telnet-port|--target-ip|--command|--username|--password) + [ "$#" -ge 2 ] || fail "Missing value for $1" + case "$1" in + --pid) pid=$2 ;; + --telnet-port) port=$2 ;; + --target-ip) target_ip=$2 ;; + --command) command=$2 ;; + --username) username=$2 ;; + --password) password=$2 ;; + esac + shift 2 ;; + -h|--help) usage; exit 0 ;; + *) usage >&2; fail "Unknown option: $1" ;; + esac +done +# Native batch files contain one command per line. +for credential in "$username" "$password"; do + [[ "$credential" != *$'\n'* && "$credential" != *$'\r'* ]] || fail "Credentials must be single-line values" +done +if [ -z "$pid" ]; then + [ -r "$BIN/pid" ] || fail "No bin/pid; start the service or supply --pid PID" + pid=$(cat "$BIN/pid") +fi +[[ "$pid" =~ ^[1-9][0-9]{0,9}$ ]] || fail "PID must be a positive integer" +[[ "$port" =~ ^[1-9][0-9]{0,4}$ ]] && [ "$port" -le 65535 ] || fail "Invalid telnet port" +[ -t 0 ] || [ -n "$command" ] || fail "Interactive use needs a terminal; use --command COMMAND" +case "$target_ip" in + [Ll][Oo][Cc][Aa][Ll][Hh][Oo][Ss][Tt]) target_ip=127.0.0.1 ;; +esac +JAVA=${JAVA_HOME:+$JAVA_HOME/bin/}java +ARTHAS="$TOP/lib/arthas" +[ -f "$ARTHAS/arthas-boot.jar" ] || fail "Bundled Arthas files are missing from lib/arthas" +# Keep the listener within this diagnosis session. Verify its PID before stopping +# it, since an occupied port must never stop a different application's agent. +state_dir=$(mktemp -d) +client_pid="" +attaching=true +connect_ip=$target_ip +quote_argument() { + local value=${1//\'/\'\\\'\'} + printf "'%s'" "$value" +} +write_batch() { + local file=$1 + local commands=$2 + { + if [ -n "$password" ]; then + printf 'auth --username %s %s\n' "$(quote_argument "${username:-arthas}")" "$(quote_argument "$password")" + fi + # Match arthas-client --command's semicolon-separated command syntax. + printf '%s\n' "${commands//;/$'\n'}" + } > "$file" +} +redact_auth_echo() { + local line + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + *'$ auth --username '*) printf '%s$ auth [credentials omitted]\n' "${line%%'$ auth --username '*}" ;; + *) printf '%s\n' "$line" ;; + esac + done +} +# The native client's -t option does not bound its final response wait. +bounded_client() { + write_batch "$state_dir/cleanup.commands" "$1" + "$JAVA" -jar "$ARTHAS/arthas-client.jar" -t 5000 -f "$state_dir/cleanup.commands" "$connect_ip" "$port" & + local command_pid=$! + (sleep 10; kill -KILL "$command_pid" 2>/dev/null) >/dev/null 2>&1 & + local timer_pid=$! + local status=0 + wait "$command_pid" || status=$? + kill -KILL "$timer_pid" 2>/dev/null || true + wait "$timer_pid" 2>/dev/null || true + return "$status" +} +format_endpoint() { + case "$connect_ip" in + *:*) printf '[%s]:%s' "$connect_ip" "$port" ;; + *) printf '%s:%s' "$connect_ip" "$port" ;; + esac +} +stop_agent() { + local session attempt agent_pid="" line endpoint rows=0 + connect_ip=$(cat "$state_dir/connect-ip") + endpoint=$(format_endpoint) + if ! session=$(bounded_client session 2>&1); then + if [[ "$session" == *"Connection refused"* ]]; then + echo "Arthas listener closed at $endpoint" >&2 + return 0 + fi + if [[ "$session" == *"not permitted"* ]]; then + echo "Error: Arthas credentials rejected at $endpoint; agent left running" >&2 + else + echo "Error: could not verify Arthas cleanup at $endpoint; agent left running" >&2 + fi + return 1 + fi + # AuthCommand echoes the username. Trust only one complete session table row. + while IFS= read -r line; do + if [[ "$line" =~ ^[[:space:]]*JAVA_PID[[:space:]]+([0-9]+)[[:space:]]*$ ]]; then + agent_pid=${BASH_REMATCH[1]} + rows=$((rows + 1)) + fi + done <<< "$session" + if [ "$rows" -ne 1 ]; then + if [ "$rows" -eq 0 ] && [[ "$session" == *"not permitted"* ]]; then + echo "Error: Arthas credentials rejected at $endpoint; agent left running" >&2 + else + echo "Error: could not verify an unambiguous Arthas PID at $endpoint; agent left running" >&2 + fi + return 1 + fi + if [ "$agent_pid" != "$pid" ]; then + echo "Error: Arthas at $endpoint belongs to PID $agent_pid, expected $pid; agent left running" >&2 + return 1 + fi + bounded_client stop >/dev/null 2>&1 || true + # Command errors can return zero; verify closure after asynchronous shutdown. + for attempt in 1 2 3; do + if ! session=$(bounded_client session 2>&1) && [[ "$session" == *"Connection refused"* ]]; then + echo "Arthas listener closed at $endpoint" >&2 + return 0 + fi + sleep 0.2 + done + echo "Error: could not stop Arthas at $endpoint; check session/stop settings before retrying" >&2 + return 1 +} +cleanup() { + local status=$? + trap '' INT TERM HUP + trap - EXIT + if [ -n "$client_pid" ]; then + # Let an in-flight loadAgent finish before checking/closing its listener. + if [ "$attaching" = false ]; then + kill "$client_pid" 2>/dev/null || true + fi + # A queued second signal can interrupt wait even after traps are ignored. + while kill -0 "$client_pid" 2>/dev/null; do + wait "$client_pid" 2>/dev/null || true + done + fi + if [ -f "$state_dir/validated" ]; then + stop_agent || { [ "$status" -ne 0 ] || status=1; } + fi + rm -f "$state_dir/validated" "$state_dir/connect-ip" "$state_dir/cleanup.commands" "$state_dir/run.commands" + rmdir "$state_dir" + exit "$status" +} +trap cleanup EXIT +trap 'trap "" INT TERM HUP; exit 130' INT +trap 'trap "" INT TERM HUP; exit 143' TERM +trap 'trap "" INT TERM HUP; exit 129' HUP +echo "Waiting for PID $pid to answer the attach request..." >&2 +( + # Group signals must not end the client before target-side loadAgent finishes. + trap '' INT TERM HUP + exec "$JAVA" --add-modules jdk.attach --class-path "$ARTHAS/arthas-core.jar" \ + "$BIN/ArthasAttach.java" "$pid" "$ARTHAS" "$port" "$TOP" "$state_dir/validated" "$target_ip" \ + "$username" "$password" +) <&0 & +client_pid=$! +wait "$client_pid" +client_pid="" +attaching=false +connect_ip=$(cat "$state_dir/connect-ip") +if ! session=$(bounded_client session 2>&1) && [[ "$session" == *"Connection refused"* ]]; then + echo "Error: no Arthas listener at $(format_endpoint);" \ + "an existing agent may use another port (API default 8562); retry with --telnet-port" >&2 + rm -f "$state_dir/validated" + exit 1 +fi +if [ -n "$command" ]; then + if [ -n "$password" ]; then + write_batch "$state_dir/run.commands" "$command" + boot_args+=(--batch-file "$state_dir/run.commands") + else + boot_args+=(--command "$command") + fi +fi +[ -z "$username" ] || boot_args+=(--username "$username") +[ -z "$password" ] || boot_args+=(--password "$password") +boot_args+=(--arthas-home "$ARTHAS" --target-ip "$connect_ip" --telnet-port "$port" --http-port -1 "$pid") +if [ -n "$password" ] && [ -n "$command" ]; then + # The native batch client echoes commands, including its auth command. + "$JAVA" -jar "$ARTHAS/arthas-boot.jar" "${boot_args[@]}" <&0 > >(redact_auth_echo) & +else + "$JAVA" -jar "$ARTHAS/arthas-boot.jar" "${boot_args[@]}" <&0 & +fi +client_pid=$! +wait "$client_pid" +client_pid="" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/dump-store.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/dump-store.sh index 14b6ac1f79..eb05bb4568 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/dump-store.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/dump-store.sh @@ -29,6 +29,12 @@ BIN=`abs_path` TOP="$(cd $BIN/../ && pwd)" CONF=$TOP/conf LIB=$TOP/lib +JVM_MODULE_OPTIONS="${BIN}/jvm-module.options" + +if [[ ! -r "${JVM_MODULE_OPTIONS}" ]]; then + echo "Missing or unreadable JVM module options file: ${JVM_MODULE_OPTIONS}" >&2 + exit 1 +fi . "${BIN}"/util.sh @@ -51,5 +57,7 @@ echo "Dumping HugeGraph Store($conf)..." dump_store_ext_jar_path=$LIB/hugegraph-dist-*.jar for i in $LIB/*.jar; do dump_store_ext_jar_path=$dump_store_ext_jar_path:$i; export dump_store_ext_jar_path; done -exec "$JAVA" -cp "$dump_store_ext_jar_path" \ +source "$BIN/preload-topling.sh" || exit 1 +exec "$JAVA" @"${JVM_MODULE_OPTIONS}" \ +-cp "${TOPLING_RUNTIME_CLASSPATH:+$TOPLING_RUNTIME_CLASSPATH:}$dump_store_ext_jar_path" \ org.apache.hugegraph.cmd.StoreDumper "$@" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/gremlin-console.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/gremlin-console.sh index 7a92b20068..eeee6b07cf 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/gremlin-console.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/gremlin-console.sh @@ -30,6 +30,11 @@ while [ -h "$SOURCE" ]; do [[ $SOURCE != /* ]] && SOURCE="$BIN/$SOURCE" done BIN="$( cd -P "$( dirname "$SOURCE" )" && pwd )" +JVM_MODULE_OPTIONS="${BIN}/jvm-module.options" +if [[ ! -r "${JVM_MODULE_OPTIONS}" ]]; then + echo "Missing or unreadable JVM module options file: ${JVM_MODULE_OPTIONS}" >&2 + exit 1 +fi # Set $CFG to $BIN/../conf cd -P $BIN/../conf CFG=$(pwd) @@ -47,6 +52,7 @@ CP="$CP":$(find -L $LIB -name 'slf4j-log4j12*.jar' | sort | tr '\n' ':') CP="$CP":$(find -L $LIB -name 'hugegraph*.jar' | sort | tr '\n' ':') # Add the remaining jars in $BIN/../lib. CP="$CP":$(find -L $LIB -name '*.jar' \ + \! -path "$LIB/arthas/*" \ \! -name 'hugegraph*' \ \! -name 'slf4j-log4j12*.jar' | sort | tr '\n' ':') # Add the jars in $BIN/../ext (at any subdirectory depth) @@ -122,4 +128,4 @@ if [ -n "$SCRIPT_DEBUG" ]; then fi # Start the JVM, execute the application, and return its exit code -exec $JAVA $JAVA_OPTIONS $MAIN_CLASS "$@" +exec $JAVA @"${JVM_MODULE_OPTIONS}" $JAVA_OPTIONS $MAIN_CLASS "$@" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh index caffedc482..3e0276ec0b 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh @@ -42,10 +42,21 @@ LOGS="$TOP/logs" OUTPUT=${LOGS}/hugegraph-server.log GITHUB="https://github.com" +# Launcher errors and warnings go to ${OUTPUT} (hugegraph-server.log) when it can +# be written, and to stderr, which reaches the terminal or the container log. The +# file comes first so the message is kept even if writing to stderr kills the +# script (a closed pipe, for example). +report_error() { + if [[ -w "${OUTPUT}" || ( ! -e "${OUTPUT}" && -w "${LOGS}" ) ]]; then + (printf '%s\n' "$1" >> "${OUTPUT}") 2>/dev/null || true + fi + printf '%s\n' "$1" >&2 +} + export HUGEGRAPH_HOME="$TOP" . "${BIN}"/util.sh -configure_riscv64_libatomic || exit 1 +configure_riscv64_libatomic report_error || exit 1 # Parse the server arguments in array way SERVER_ARGS=("$@") @@ -57,17 +68,18 @@ USER_OPTION="${SERVER_ARGS[3]:-}" GC_OPTION="${SERVER_ARGS[4]:-}" OPEN_TELEMETRY="${SERVER_ARGS[5]:-}" -ensure_path_writable "$LOGS" -ensure_path_writable "$PLUGINS" +ensure_path_writable "$LOGS" report_error +ensure_path_writable "$PLUGINS" report_error # The maximum and minimum heap memory that service can use MAX_MEM=$((32 * 1024)) MIN_MEM=$((1 * 512)) -MIN_JAVA_VERSION=11 +MIN_JAVA_VERSION=17 # JDK 24 removed the Security Manager (JEP 486): "-Djava.security.manager=allow" # is a fatal VM initialization error there and System.setSecurityManager() always # throws, so HugeSecurityManager cannot be installed on newer runtimes. MAX_SECURITY_JAVA_VERSION=23 +JVM_MODULE_OPTIONS="${BIN}/jvm-module.options" # Add the slf4j-log4j12 binding CP=$(find -L $LIB -name 'log4j-slf4j-impl*.jar' | sort | tr '\n' ':') @@ -75,6 +87,7 @@ CP=$(find -L $LIB -name 'log4j-slf4j-impl*.jar' | sort | tr '\n' ':') CP="$CP":$(find -L $LIB -name 'hugegraph*.jar' | sort | tr '\n' ':') # Add the remaining jars in lib. CP="$CP":$(find -L $LIB -name '*.jar' \ + \! -path "$LIB/arthas/*" \ \! -name 'hugegraph*' \ \! -name 'log4j-slf4j-impl*.jar' | sort | tr '\n' ':') # Add the jars in ext (at any subdirectory depth) @@ -85,7 +98,8 @@ CP="$CP":$(find -L $PLUGINS -name '*.jar' | sort | tr '\n' ':') # (Cygwin only) Use ; classpath separator and reformat paths for Windows ("C:\foo") [[ $(uname) = CYGWIN* ]] && CP="$(cygpath -p -w "$CP")" -export CLASSPATH="${CLASSPATH:-}:$CP" +source "$BIN/preload-topling.sh" || exit 1 +export CLASSPATH="${TOPLING_RUNTIME_CLASSPATH:+$TOPLING_RUNTIME_CLASSPATH:}${CLASSPATH:-}:$CP" # Change to $BIN's parent cd "${TOP}" || exit 1 @@ -109,8 +123,12 @@ JAVA_VERSION=$($JAVA -version 2>&1 | # Drop any pre-release suffix, e.g. "24-ea" -> "24" JAVA_VERSION="${JAVA_VERSION%%[!0-9]*}" if [[ -z $JAVA_VERSION || $JAVA_VERSION -lt $MIN_JAVA_VERSION ]]; then - echo "Make sure the JDK is installed and the version >= $MIN_JAVA_VERSION, current is $JAVA_VERSION" \ - >> "${OUTPUT}" + report_error "Make sure the JDK is installed and the version >= $MIN_JAVA_VERSION, current is $JAVA_VERSION" + exit 1 +fi + +if [[ ! -r ${JVM_MODULE_OPTIONS} ]]; then + report_error "Missing or unreadable JVM module options file: ${JVM_MODULE_OPTIONS}" exit 1 fi @@ -118,40 +136,37 @@ fi if [ "$JAVA_OPTIONS" = "" ]; then XMX=$(calc_xmx $MIN_MEM $MAX_MEM) if [ $? -ne 0 ]; then - echo "Failed to start HugeGraphServer, requires at least ${MIN_MEM}MB free memory" >> "${OUTPUT}" + report_error "Failed to start HugeGraphServer, requires at least ${MIN_MEM}MB free memory" exit 1 fi - JAVA_OPTIONS="-Xms${MIN_MEM}m -Xmx${XMX}m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGS} ${USER_OPTION}" + JAVA_OPTIONS="-Xms${MIN_MEM}m -Xmx${XMX}m ${USER_OPTION}" # Rolling out detailed GC logs #JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseGCLogFileRotation -XX:GCLogFileSize=10M -XX:NumberOfGCLogFiles=3 \ # -Xloggc:./logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps" fi -if [[ $JAVA_VERSION -gt 9 ]]; then - JAVA_OPTIONS="${JAVA_OPTIONS} --add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED \ - --add-modules=jdk.unsupported \ - --add-exports=java.base/sun.nio.ch=ALL-UNNAMED " -fi - -# Using G1GC as the default garbage collector (Recommended for large memory machines) -# mention: zgc is only available on ARM-Mac with java > 13 +# Keep JVM/caller GC selection by default; explicitly select G1 when requested. case "$GC_OPTION" in - "") - echo "Using G1GC as the default garbage collector" - JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+ParallelRefProcEnabled \ - -XX:InitiatingHeapOccupancyPercent=50 \ - -XX:G1RSetUpdatingPauseTimePercent=5" + ""|g1|G1) + if [[ "$GC_OPTION" == g1 || "$GC_OPTION" == G1 ]]; then + echo "Using G1GC" + JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseG1GC" + else + echo "Using JVM garbage collector configuration" + fi + JAVA_OPTIONS="-XX:+ParallelRefProcEnabled -XX:InitiatingHeapOccupancyPercent=50 \ + -XX:G1RSetUpdatingPauseTimePercent=5 ${JAVA_OPTIONS}" ;; zgc|ZGC) - echo "Using ZGC as the default garbage collector (Only support Java 11+)" + echo "Using ZGC as the default garbage collector (requires Java 17 or later)" JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseZGC -XX:+UnlockExperimentalVMOptions \ -XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 \ -XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 \ -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive" ;; *) - echo "Unrecognized gc option: '$GC_OPTION', default use g1, options only support 'ZGC' now" >> ${OUTPUT} + report_error "Unrecognized gc option: '$GC_OPTION', supported options: g1, ZGC" exit 1 esac @@ -166,8 +181,7 @@ Run the server on Java ${MAX_SECURITY_JAVA_VERSION} or lower, or start it with t disabled: 'start-hugegraph.sh -s false'. EOF ) - echo "${SECURITY_UNSUPPORTED_MSG}" >&2 - echo "${SECURITY_UNSUPPORTED_MSG}" >> "${OUTPUT}" + report_error "${SECURITY_UNSUPPORTED_MSG}" exit 1 fi @@ -195,12 +209,10 @@ EOF # The bootstrap validates the effective policy and refuses to start, but # its stderr goes to the stdout log in daemon mode. Name the cause here # so it also reaches the log start-hugegraph.sh points operators at. - cat >> "${OUTPUT}" < setting a finite positive -networkaddress.cache.ttl. -EOF +networkaddress.cache.ttl." fi JVM_OPTIONS="${JVM_OPTIONS} \ -Djava.security.properties=${SECURITY_PROPERTIES}" @@ -220,7 +232,7 @@ if [ "${OPEN_TELEMETRY}" == "true" ]; then "${GITHUB}/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.1.0/${OT_JAR}" if [[ ! -e "${OT_JAR_PATH}" ]]; then - echo "## Error: Failed to download ${OT_JAR}." >>${OUTPUT} + report_error "## Error: Failed to download ${OT_JAR}." exit 1 fi fi @@ -230,13 +242,15 @@ if [ "${OPEN_TELEMETRY}" == "true" ]; then actual_md5=$(md5sum "${OT_JAR_PATH}" | awk '{print $1}') if [[ "${expected_md5}" != "${actual_md5}" ]]; then - echo "## Error: MD5 checksum verification failed for ${OT_JAR_PATH}." >>${OUTPUT} - echo "## Tips: Remove the file and try again." >>${OUTPUT} + report_error "## Error: MD5 checksum verification failed for ${OT_JAR_PATH}." + report_error "## Tips: Remove the file and try again." exit 1 fi # Note: check carefully if multi "javeagent" params are set - export JAVA_TOOL_OPTIONS="-javaagent:${PLUGINS}/${OT_JAR}" + # Append, so the operator's JAVA_TOOL_OPTIONS stays; the crash-file defaults + # are put in front of all of it below. + export JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS:+${JAVA_TOOL_OPTIONS} }-javaagent:${PLUGINS}/${OT_JAR}" export OTEL_TRACES_EXPORTER=otlp export OTEL_METRICS_EXPORTER=none export OTEL_LOGS_EXPORTER=none @@ -248,23 +262,114 @@ if [ "${OPEN_TELEMETRY}" == "true" ]; then export OTEL_RESOURCE_ATTRIBUTES=service.name=server fi -if [[ "${STDOUT_MODE:-false}" != "true" ]]; then - # Daemon stderr only reaches hugegraph-server-stdout.log, so a bootstrap - # rejection of the effective DNS policy (a broken operator override - # included) would be invisible in the log start-hugegraph.sh points - # operators at. Let the bootstrap mirror its fatal errors there. - JVM_OPTIONS="${JVM_OPTIONS} -Dhugegraph.bootstrap.error.log=${OUTPUT}" -fi +# Let the bootstrap mirror its fatal errors (a rejected DNS policy, a broken +# operator override included) into hugegraph-server.log. In daemon mode its +# stderr only reaches hugegraph-server-stdout.log; in STDOUT_MODE it reaches the +# container log, which keeps a single earlier generation, so the file copy is +# what survives a crash loop on a mounted logs volume. +JVM_OPTIONS="${JVM_OPTIONS} -Dhugegraph.bootstrap.error.log=${OUTPUT}" + +# Keep heap dumps and JVM crash logs in $LOGS whatever JAVA_OPTIONS holds. The +# defaults come first in JAVA_TOOL_OPTIONS, ahead of the operator's own value. The +# JVM applies JAVA_TOOL_OPTIONS before JDK_JAVA_OPTIONS, the command line +# (JAVA_OPTIONS and -j) and _JAVA_OPTIONS, and the last occurrence of a flag wins, +# so any of those overrides a default. The JVM does the parsing itself, including +# quoted options, and @argfiles in JDK_JAVA_OPTIONS or on the command line. +# Child JVMs the Server starts (computer jobs, for example) inherit the +# environment but not the command line, so they get these defaults too, and every +# path must stay unique per JVM. ErrorFile expands %p to each JVM's PID. +# HeapDumpPath expands %p only from JDK 25, but on every version, when it names an +# existing directory, each JVM writes java_pid.hprof inside it, so it +# points at one directory per launch. HotSpot picks that name, so a child JVM that +# gets a PID reused within the same launch cannot write over an earlier child's +# dump, and its crash log truncates the earlier child's (ErrorFile only expands +# %p); the launcher cannot rename HotSpot's files. +# A restarted container often reuses the PID; HotSpot truncates an existing crash +# log (JDK 17+) and will not write a heap dump over an existing file. So the names +# carry the host name (the pod name on Kubernetes, so pods sharing one log volume +# do not collide), the launch time, and a counter. A plain mkdir claims each name +# atomically, so concurrent launches never share one. This runs after every +# preflight check, so a launch that fails before Java starts leaves nothing. +# Dump directories are never removed automatically: one stays empty until a JVM +# using it runs out of memory, and that JVM may be a computer job that outlives +# the Server, so the launcher cannot tell when a directory is safe to delete. +crash_name_taken() { + local name="$1" file restore_failglob taken=1 + [[ -e "${LOGS}/heapdump_${name}" || -L "${LOGS}/heapdump_${name}" ]] && return 0 + restore_failglob=$(shopt -p failglob) + shopt -u failglob + for file in "${LOGS}"/hs_err_pid*_"${name}".log; do + if [[ -e ${file} || -L ${file} ]]; then + taken=0 + break + fi + done + eval "${restore_failglob}" + return ${taken} +} +case "${LOGS}" in + *\"*|*%*) + # The defaults below cannot carry this path: they quote it with double quotes, + # and ErrorFile expands %. Dump into $LOGS itself instead, still from the front + # of JAVA_TOOL_OPTIONS so every operator source keeps overriding it, with the + # path in whichever quote character it does not contain, and no ErrorFile. + if [[ ${LOGS} != *"'"* ]]; then + DUMP_QUOTE="'" + elif [[ ${LOGS} != *\"* ]]; then + DUMP_QUOTE='"' + else + DUMP_QUOTE="" + fi + if [[ -n ${DUMP_QUOTE} ]]; then + report_error "WARN: ${LOGS} contains a double quote or %, so heap dumps go to ${LOGS}\ + without a per-launch directory and no crash log default is set; set -XX:ErrorFile yourself" + CRASH_OPTIONS="-XX:+HeapDumpOnOutOfMemoryError ${DUMP_QUOTE}-XX:HeapDumpPath=${LOGS}${DUMP_QUOTE}" + export JAVA_TOOL_OPTIONS="${CRASH_OPTIONS}${JAVA_TOOL_OPTIONS:+ ${JAVA_TOOL_OPTIONS}}" + else + report_error "WARN: ${LOGS} contains both quote characters, which JAVA_TOOL_OPTIONS\ + cannot carry, so no heap dump or crash log default is set; set -XX:HeapDumpPath yourself" + fi + ;; + *) + LAUNCH_HOST=$(printf '%s' "${HOSTNAME:-localhost}" | LC_ALL=C tr -c 'A-Za-z0-9._-' '_') + LAUNCH_HOST="${LAUNCH_HOST:0:64}" + LAUNCH_STAMP="${LAUNCH_HOST}_$(date +%Y%m%d-%H%M%S)" + LAUNCH_ID="${LAUNCH_STAMP}" + LAUNCH_SUFFIX=0 + HEAP_DUMP_PATH="" + while [[ -z ${HEAP_DUMP_PATH} ]]; do + if ! crash_name_taken "${LAUNCH_ID}" && + mkdir "${LOGS}/heapdump_${LAUNCH_ID}" 2>/dev/null; then + HEAP_DUMP_PATH="${LOGS}/heapdump_${LAUNCH_ID}" + elif [[ ${LAUNCH_SUFFIX} -ge 1000 ]]; then + report_error "WARN: heap dump names for ${LAUNCH_STAMP} are all taken; heap dumps go to ${LOGS}" + HEAP_DUMP_PATH="${LOGS}" + elif ! crash_name_taken "${LAUNCH_ID}"; then + # Not a name clash (a full disk, for example): keep starting, and dump + # into $LOGS itself rather than make diagnostics a startup requirement. + report_error "WARN: cannot create ${LOGS}/heapdump_${LAUNCH_ID}; heap dumps go to ${LOGS}" + HEAP_DUMP_PATH="${LOGS}" + else + LAUNCH_SUFFIX=$((LAUNCH_SUFFIX + 1)) + LAUNCH_ID="${LAUNCH_STAMP}-${LAUNCH_SUFFIX}" + fi + done + CRASH_OPTIONS="-XX:+HeapDumpOnOutOfMemoryError" + CRASH_OPTIONS="${CRASH_OPTIONS} \"-XX:HeapDumpPath=${HEAP_DUMP_PATH}\"" + CRASH_OPTIONS="${CRASH_OPTIONS} \"-XX:ErrorFile=${LOGS}/hs_err_pid%p_${LAUNCH_ID}.log\"" + export JAVA_TOOL_OPTIONS="${CRASH_OPTIONS}${JAVA_TOOL_OPTIONS:+ ${JAVA_TOOL_OPTIONS}}" + ;; +esac # Turn on security check if [[ "${STDOUT_MODE:-false}" == "true" ]]; then - exec ${JAVA} -Dname="HugeGraphServer" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ - ${SECURITY_MANAGER_OPTION} -cp ${CLASSPATH}: \ + exec ${JAVA} @"${JVM_MODULE_OPTIONS}" -Dname="HugeGraphServer" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ + ${SECURITY_MANAGER_OPTION} -cp "${CLASSPATH}:" \ org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap \ ${OPEN_SECURITY_CHECK} ${GREMLIN_SERVER_CONF} ${REST_SERVER_CONF} else - exec ${JAVA} -Dname="HugeGraphServer" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ - ${SECURITY_MANAGER_OPTION} -cp ${CLASSPATH}: \ + exec ${JAVA} @"${JVM_MODULE_OPTIONS}" -Dname="HugeGraphServer" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ + ${SECURITY_MANAGER_OPTION} -cp "${CLASSPATH}:" \ org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap \ ${OPEN_SECURITY_CHECK} ${GREMLIN_SERVER_CONF} ${REST_SERVER_CONF} \ >> ${LOGS}/hugegraph-server-stdout.log 2>&1 diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh index 74ec0bb731..754a74cd67 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh @@ -30,6 +30,7 @@ TOP="$(cd "${BIN}"/../ && pwd)" CONF="$TOP/conf" LIB="$TOP/lib" PLUGINS="$TOP/plugins" +JVM_MODULE_OPTIONS="${BIN}/jvm-module.options" . "${BIN}"/util.sh @@ -38,23 +39,27 @@ ensure_path_writable "${PLUGINS}" if [[ -n "$JAVA_HOME" ]]; then JAVA="$JAVA_HOME"/bin/java - EXT="$JAVA_HOME/jre/lib/ext:$LIB:$PLUGINS" else JAVA=java - EXT="$LIB:$PLUGINS" fi cd "${TOP}" || exit -DEFAULT_JAVA_OPTIONS="--add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED" +if [[ ! -r ${JVM_MODULE_OPTIONS} ]]; then + echo "Missing or unreadable JVM module options file: ${JVM_MODULE_OPTIONS}" >&2 + exit 1 +fi echo "Initializing HugeGraph Store..." # Build classpath with hugegraph*.jar first to avoid class loading conflicts CP=$(find -L "${LIB}" -name 'hugegraph*.jar' | sort | tr '\n' ':') -CP="$CP":$(find -L "${LIB}" -name '*.jar' \! -name 'hugegraph*' | sort | tr '\n' ':') +CP="$CP":$(find -L "${LIB}" -name '*.jar' \ + \! -path "${LIB}/arthas/*" \! -name 'hugegraph*' | sort | tr '\n' ':') CP="$CP":$(find -L "${PLUGINS}" -name '*.jar' | sort | tr '\n' ':') -$JAVA -cp $CP ${DEFAULT_JAVA_OPTIONS} \ +source "$BIN/preload-topling.sh" || exit 1 +CP="${TOPLING_RUNTIME_CLASSPATH:+$TOPLING_RUNTIME_CLASSPATH:}$CP" +"${JAVA}" @"${JVM_MODULE_OPTIONS}" -cp "$CP" \ org.apache.hugegraph.cmd.InitStore "${CONF}"/rest-server.properties INIT_STORE_STATUS=$? if [[ ${INIT_STORE_STATUS} -ne 0 ]]; then diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/jvm-module.options b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/jvm-module.options new file mode 100644 index 0000000000..9da05e1d8e --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/jvm-module.options @@ -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. +# + +--add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED +--add-modules=jdk.unsupported +--add-exports=java.base/sun.nio.ch=ALL-UNNAMED +# HBasePlatformDependent reflects on java.nio.Bits.unaligned() during initialization. +--add-opens=java.base/java.nio=ALL-UNNAMED +# SOFA RPC ProviderInvoker reflects on Throwable.cause during initialization. +--add-opens=java.base/java.lang=ALL-UNNAMED diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/preload-topling.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/preload-topling.sh new file mode 100755 index 0000000000..3f88216368 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/preload-topling.sh @@ -0,0 +1,102 @@ +#!/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. +# +# Sourced by launchers. Do not change the caller's shell flags or business config. +topling_remove_path() { + local value="$1" remove="$2" preserve_empty="${3:-false}" entry result="" separator="" + while true; do + entry="${value%%:*}" + if [ "$entry" != "$remove" ] && { [ -n "$entry" ] || [ "$preserve_empty" = true ]; }; then + result="$result$separator$entry" + separator=: + fi + [[ "$value" = *:* ]] || break + value="${value#*:}" + done + printf '%s\n' "$result" +} + +topling_select_runtime() { + local top provider native jar conf inherited entry + local -a preloads + top="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" || return 1 + provider="${TOPLINGDB_ROCKSDB_PROVIDER:-rocksdb}" + case "$provider" in rocksdb | topling) ;; *) + echo "Error: invalid TOPLINGDB_ROCKSDB_PROVIDER '$provider'" >&2; return 1 ;; + esac + if [ -n "${TOPLING_ACTIVE_NATIVE:-}" ]; then + LD_PRELOAD=$(topling_remove_path "${LD_PRELOAD:-}" "$TOPLING_ACTIVE_NATIVE") + LD_LIBRARY_PATH=$(topling_remove_path "${LD_LIBRARY_PATH:-}" "$(dirname "$TOPLING_ACTIVE_NATIVE")") + export LD_PRELOAD LD_LIBRARY_PATH + fi + if [ -n "${TOPLING_RUNTIME_CLASSPATH:-}" ] && [ "${CLASSPATH+x}" = x ]; then + CLASSPATH=$(topling_remove_path "$CLASSPATH" "$TOPLING_RUNTIME_CLASSPATH" true) + export CLASSPATH + fi + unset TOPLING_ACTIVE_NATIVE TOPLING_RUNTIME_CLASSPATH + if [ -n "${TOPLING_AUTO_MIGRATE_CONF:-}" ] && + [ "${TOPLINGDB_EASY_MIGRATE_CONF:-}" = "$TOPLING_AUTO_MIGRATE_CONF" ]; then + unset TOPLINGDB_EASY_MIGRATE_CONF + fi + unset TOPLING_AUTO_MIGRATE_CONF + native="$top/topling/librocksdbjni-linux64.so" + # Two RocksDB JNI libraries can conflict before the JVM even starts. + inherited="${LD_PRELOAD:-}" + inherited="${inherited//:/ }" + inherited="${inherited//$'\n'/ }" + read -r -a preloads <<< "$inherited" + for entry in "${preloads[@]-}"; do + case "${entry##*/}" in librocksdbjni*.so* | libfrocksdbjni*.so*) + if [ "$provider" != topling ] || [ "$entry" != "$native" ]; then + echo "Error: competing RocksDB JNI in LD_PRELOAD: $entry" >&2 + return 1 + fi ;; + esac + done + [ "$provider" = topling ] || return 0 + [ "$(uname -s)/$(uname -m)" = Linux/x86_64 ] || { + echo 'Error: Topling requires Linux x86_64' >&2; return 1; + } + jar="$top/topling/rocksdbjni.jar" + [ -r "$jar" ] && [ -r "$native" ] || { + echo "Error: run $top/bin/prepare-topling.sh before enabling Topling" >&2; return 1; + } + [ -r "$top/topling/easy-migrate.sha256" ] && + (cd "$top/topling" && sha256sum -c easy-migrate.sha256 >/dev/null) || { + echo "Error: unverified EasyMigrate runtime; prepare a fresh distribution with $top/bin/prepare-topling.sh" >&2 + return 1 + } + conf="${TOPLINGDB_EASY_MIGRATE_CONF:-}" + if [ -z "$conf" ]; then + conf="$top/conf/toplingdb.yaml" + export TOPLING_AUTO_MIGRATE_CONF="$conf" + fi + [ -r "$conf" ] || { echo "Error: unreadable Easy Migrate configuration: $conf" >&2; return 1; } + export TOPLINGDB_EASY_MIGRATE_CONF="$conf" + export LD_LIBRARY_PATH="$top/topling${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + # The selected JNI must precede inherited preloads during native symbol lookup. + export LD_PRELOAD="$native${LD_PRELOAD:+:$LD_PRELOAD}" + export TOPLING_ACTIVE_NATIVE="$native" TOPLING_RUNTIME_CLASSPATH="$jar" + echo "Selected Topling runtime: $jar; configuration: $conf" +} + +if topling_select_runtime; then + unset -f topling_select_runtime topling_remove_path +else + unset -f topling_select_runtime topling_remove_path + return 1 2>/dev/null || exit 1 +fi diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/prepare-topling.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/prepare-topling.sh new file mode 100755 index 0000000000..847924c464 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/prepare-topling.sh @@ -0,0 +1,126 @@ +#!/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. +# +# Run only after stopping this component. Runtime preparation never changes data. +set -Eeuo pipefail +TOP="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +[ "$(uname -s)/$(uname -m)" = Linux/x86_64 ] || { + echo 'Error: Topling requires Linux x86_64' >&2; exit 1; +} +[[ "${TOPLING_JNI_JAR:-}" = /* ]] && [ -r "$TOPLING_JNI_JAR" ] || { + echo 'Error: TOPLING_JNI_JAR must be an absolute readable file' >&2; exit 1; +} +[[ "${TOPLING_JNI_SHA256:-}" =~ ^[[:xdigit:]]{64}$ ]] || { + echo 'Error: TOPLING_JNI_SHA256 must contain 64 hexadecimal characters' >&2; exit 1; +} +[ ! -e "$TOP/topling" ] && [ ! -L "$TOP/topling" ] || { + echo 'Error: runtime already prepared; use a fresh stopped distribution to replace it' >&2; exit 1; +} +staging=$(mktemp -d "$TOP/.topling-prepare.XXXXXX") +trap 'rm -rf "$staging"' EXIT +cp "$TOPLING_JNI_JAR" "$staging/rocksdbjni.jar" +expected=$(printf '%s' "$TOPLING_JNI_SHA256" | tr 'A-F' 'a-f') +actual=$(sha256sum "$staging/rocksdbjni.jar" | awk '{print $1}') +[ "$actual" = "$expected" ] || { echo 'Error: Topling JNI SHA-256 mismatch' >&2; exit 1; } +entries=$(unzip -Z1 "$staging/rocksdbjni.jar") +[ "$(grep -cx 'org/rocksdb/SidePluginRepo.class' <<< "$entries" || true)" = 1 ] && +[ "$(grep -cx 'librocksdbjni-linux64.so' <<< "$entries" || true)" = 1 ] && +[ "$(grep -Ec '(^|/)librocksdbjni-linux64[.]so$' <<< "$entries" || true)" = 1 ] || { + echo 'Error: expected Topling Java marker and exactly one Linux x86_64 JNI entry' >&2; exit 1; +} +unzip -p "$staging/rocksdbjni.jar" librocksdbjni-linux64.so > "$staging/librocksdbjni-linux64.so" +header=$(od -An -tx1 -N20 "$staging/librocksdbjni-linux64.so" | tr -d ' \n') +[[ "$header" = 7f454c460201* ]] && [ "${header:36:4}" = 3e00 ] || { + echo 'Error: JNI is not a little-endian Linux x86_64 ELF library' >&2; exit 1; +} +# Ubuntu's t64 transition renamed libaio; keep compatibility inside this component. +if [ ! -e /usr/lib/x86_64-linux-gnu/libaio.so.1 ] && + [ -e /usr/lib/x86_64-linux-gnu/libaio.so.1t64 ]; then + ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 "$staging/libaio.so.1" +fi +ldd_output=$(LD_LIBRARY_PATH="$staging${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + ldd "$staging/librocksdbjni-linux64.so") +if grep -q 'not found' <<< "$ldd_output"; then + printf 'Error: install the missing native dependencies first:\n%s\n' "$ldd_output" >&2 + exit 1 +fi +# Verify EasyMigrate behavior in a disposable DB before touching business data. +# Class markers and hashes identify a library, but do not prove this capability. +cat > "$staging/probe.yaml" <<'YAML' +http: {auto_start_http: false} +CFOptions: + default: + write_buffer_size: 17M +DBOptions: + default: + create_if_missing: true + memtable_as_log_index: false +YAML +cat > "$staging/ToplingRuntimeProbe.java" <<'JAVA' +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Pattern; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; + +class ToplingRuntimeProbe { + public static void main(String[] args) throws Exception { + if (!"17".equals(System.getProperty("java.specification.version"))) { + throw new IllegalStateException("Prepare Topling with Java 17"); + } + RocksDB.loadLibrary(); + Path database = Path.of(args[0]); + try (Options options = new Options().setCreateIfMissing(true); + RocksDB db = RocksDB.open(options, database.toString())) { + db.put(new byte[]{1}, new byte[]{2}); + if (db.get(new byte[]{1})[0] != 2) { + throw new IllegalStateException("Topling capability probe could not read its write"); + } + } + Pattern configured = Pattern.compile("(?m)^\\s*write_buffer_size\\s*=\\s*17825792\\s*$"); + boolean applied = false; + try (var paths = Files.list(database)) { + for (Path path : paths.filter(p -> p.getFileName().toString().startsWith("OPTIONS-")).toList()) { + applied |= configured.matcher(Files.readString(path)).find(); + } + } + if (!applied) { + throw new IllegalStateException("Selected JNI does not apply EasyMigrate configuration"); + } + } +} +JAVA +if ! LD_LIBRARY_PATH="$staging${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + LD_PRELOAD="$staging/librocksdbjni-linux64.so" \ + TOPLINGDB_EASY_MIGRATE_CONF="$staging/probe.yaml" \ + java --add-opens=java.base/java.nio=ALL-UNNAMED --source 17 \ + --class-path "$staging/rocksdbjni.jar" "$staging/ToplingRuntimeProbe.java" "$staging/probe-db"; then + echo 'Error: Java 17 JDK / Topling EasyMigrate capability probe failed' >&2 + exit 1 +fi +native_hash=$(sha256sum "$staging/librocksdbjni-linux64.so" | awk '{print $1}') +printf '%s rocksdbjni.jar\n%s librocksdbjni-linux64.so\n' "$actual" "$native_hash" > "$staging/easy-migrate.sha256" +rm -rf "$staging/probe-db" "$staging/probe.yaml" "$staging/ToplingRuntimeProbe.java" +mkdir "$staging/rocksdb_resource" +status=0 +unzip -j "$staging/rocksdbjni.jar" '*.html' '*.css' -d "$staging/rocksdb_resource" >/dev/null 2>&1 || status=$? +[ "$status" = 0 ] || [ "$status" = 11 ] || { echo 'Error: cannot extract native web resources' >&2; exit 1; } +# Publish readable runtime assets for a service UID different from the preparer. +find "$staging" -type d -exec chmod 0755 {} + +find "$staging" -type f -exec chmod 0644 {} + +mv -T "$staging" "$TOP/topling" +echo "Prepared component-local Topling runtime: $TOP/topling" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh index 2c99238327..28f36eb427 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh @@ -99,6 +99,24 @@ if [[ $PRELOAD == "true" ]]; then sed -i -e '/registerBackends/d; /serverStarted/d' "${SCRIPTS}/${EXAMPLE_SCRIPT}" fi +function forward_signal_and_wait() { + local signal="$1" + local exit_code="$2" + + # A process-group signal and the entrypoint's forwarding can arrive twice. + # Keep the wrapper alive until it has reaped its child in either case. + trap '' SIGHUP SIGINT SIGQUIT SIGTERM + if kill -0 "$PID" 2>/dev/null; then + kill "-$signal" "$PID" 2>/dev/null || true + # The foreground wait is interrupted before the trap runs, so retry + # until the child has exited and been reaped. + while kill -0 "$PID" 2>/dev/null; do + wait "$PID" 2>/dev/null || true + done + fi + exit "$exit_code" +} + if [[ $DAEMON == "true" ]]; then echo "Starting HugeGraphServer in daemon mode..." "${BIN}"/hugegraph-server.sh "${CONF}/${GREMLIN_SERVER_CONF}" "${CONF}"/rest-server.properties \ @@ -112,7 +130,7 @@ if [[ $DAEMON == "true" ]]; then wait_for_startup ${PID} 'HugeGraphServer' "$REST_SERVER_URL/graphs" "${SERVER_STARTUP_TIMEOUT_S}" || { if [[ "${STDOUT_MODE:-false}" == "true" ]]; then - echo "See 'docker logs' for HugeGraphServer log output." >&2 + echo "See the container logs ('docker logs' or 'kubectl logs') for HugeGraphServer log output." >&2 else echo "See $LOGS/hugegraph-server.log for HugeGraphServer log output." >&2 fi @@ -133,7 +151,14 @@ else PID="$!" # Write pid to file echo "$PID" > "$PID_FILE" - trap 'kill $PID; wait $PID; exit $?' SIGHUP SIGINT SIGQUIT SIGTERM + trap 'forward_signal_and_wait HUP 129' SIGHUP + # The background JVM can inherit an ignored SIGINT disposition, so use + # SIGTERM to guarantee that Ctrl-C shuts it down while retaining exit 130. + trap 'forward_signal_and_wait TERM 130' SIGINT + # Forward TERM instead of QUIT: the JVM only dumps threads on SIGQUIT + # and keeps running, which would leave the wait loop below stuck. + trap 'forward_signal_and_wait TERM 131' SIGQUIT + trap 'forward_signal_and_wait TERM 143' SIGTERM wait $PID exit $? fi diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh index 570d07b545..ffe1a2243d 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh @@ -31,7 +31,7 @@ TOP="$(cd $BIN/../ && pwd)" . $BIN/util.sh if [ "$JAVA_HOME" == "" ]; then - echo "Must set JAVA_HOME environment variable and installed jdk >= 1.8" + echo "Must set JAVA_HOME environment variable and install JDK >= 17" exit 1 fi diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh index 8ae46e88a4..65720021eb 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh @@ -25,6 +25,7 @@ function command_available() { } function configure_riscv64_libatomic() { + local error_handler="${1:-}" if [[ "$(uname -s)" != "Linux" || "$(uname -m)" != "riscv64" ]]; then return 0 fi @@ -56,7 +57,12 @@ function configure_riscv64_libatomic() { fi if [[ -z "$libatomic" ]]; then - echo "RISC-V RocksDB requires libatomic.so.1; install libatomic1" >&2 + local message="RISC-V RocksDB requires libatomic.so.1; install libatomic1" + if [[ -n "$error_handler" ]]; then + "$error_handler" "$message" + else + echo "$message" >&2 + fi return 1 fi @@ -407,6 +413,172 @@ function wait_for_startup() { return 1 } +# Return the remaining memory (MiB) under visible cgroup hard limits. Keep +# missing/unlimited controllers empty so ordinary host sizing stays unchanged. +function cgroup_free_memory() { + local root="${1:-/sys/fs/cgroup}" + local membership="${2:-/proc/self/cgroup}" + local mountinfo="${3:-/proc/self/mountinfo}" + local base="" path="" limit_file="" usage_file="" + local version="" + local id controllers member + if [[ -r "$membership" ]]; then + while IFS=: read -r id controllers member; do + if [[ ",$controllers," == *,memory,* ]]; then + base="$root/memory" + path="$member" + limit_file="memory.limit_in_bytes" + usage_file="memory.usage_in_bytes" + version="cgroup" + break + elif [[ "$id" == 0 && -z "$controllers" ]]; then + base="$root" + path="$member" + limit_file="memory.max" + usage_file="memory.current" + version="cgroup2" + fi + done < "$membership" + fi + [[ -n "$base" ]] || return 0 + + # Mount points and exposed roots need not use the conventional hierarchy. + # The kernel escapes whitespace/backslashes in mountinfo path fields. + local mount_root="" mountpoint="" mapped_path="" + local fallback_base="" selected_root="" selected_path="" + if [[ -r "$mountinfo" ]]; then + while read -r mount_root mountpoint; do + printf -v mountpoint '%b' "$mountpoint" + printf -v mount_root '%b' "$mount_root" + if [[ -z "$fallback_base" ]]; then + fallback_base="$mountpoint" + fi + if [[ "$mount_root" == / ]]; then + mapped_path="$path" + elif [[ "$path" == "$mount_root" ]]; then + mapped_path=/ + elif [[ "$path" == "$mount_root/"* ]]; then + mapped_path="${path#"$mount_root"}" + else + continue + fi + [[ -d "$mountpoint$mapped_path" ]] || continue + # Prefer the widest visible hierarchy so a delegated bind mount + # cannot hide a tighter ancestor exposed by another matching mount. + [[ -z "$selected_root" || ${#mount_root} -lt ${#selected_root} ]] || continue + base="$mountpoint" + selected_root="$mount_root" + selected_path="$mapped_path" + done < <(awk -v version="$version" ' + { + split($0, parts, " - "); + split(parts[2], tail, " "); + if (tail[1] == version && (version == "cgroup2" || + index("," tail[3] ",", ",memory,") > 0)) { + print $4, $5; + } + }' "$mountinfo") + # Only use a namespace root after all membership mappings were tried. + if [[ -n "$selected_root" ]]; then + path="$selected_path" + elif [[ -n "$fallback_base" ]]; then + base="$fallback_base" + path=/ + fi + fi + + # A cgroup namespace can expose its own root while /proc still names a + # host-relative membership. Fall back to the visible mount root in that + # case; never follow a membership path out of the controller mount. + local dir="$base" + if [[ "$path" == /* && "$path" != *"/../"* && "$path" != */.. && + -d "$base$path" ]]; then + dir="$base$path" + dir="${dir%/}" + fi + local limit usage inactive available smallest="" + local child child_min + local cache_key=inactive_file + local unlimited="" kernel_machine="" page_size="" + if [[ "$version" == cgroup ]]; then + cache_key=total_inactive_file + # v1 exposes PAGE_COUNTER_MAX * PAGE_SIZE instead of "max". + # LONG_BIT describes userspace, not the kernel. Personality can also + # mask uname as 32-bit, so infer unlimited only from known 64-bit names. + kernel_machine=$(uname -m 2>/dev/null) || true + page_size=$(getconf PAGESIZE 2>/dev/null) || true + if [[ "$page_size" =~ ^[1-9][0-9]*$ ]]; then + case "$kernel_machine" in + x86_64|aarch64|riscv64) + unlimited=$((9223372036854775807 / page_size * page_size)) ;; + esac + fi + fi + while :; do + limit="" + usage="" + [[ ! -r "$dir/$limit_file" ]] || read -r limit < "$dir/$limit_file" || true + if [[ -n "$unlimited" && "$limit" =~ ^[0-9]+$ ]] && + awk -v limit="$limit" -v unlimited="$unlimited" 'BEGIN {exit !(limit >= unlimited)}'; then + limit="" + fi + if [[ "$limit" =~ ^[0-9]+$ ]]; then + # Unknown usage under a known limit must not fall back to host memory. + if [[ ! -r "$dir/$usage_file" ]] || ! read -r usage < "$dir/$usage_file" || + [[ ! "$usage" =~ ^[0-9]+$ ]]; then + echo 0 + return 0 + fi + # Inactive file cache can be reclaimed on a warm restart. For v1, + # use the hierarchical total so ancestor budgets include children. + inactive=0 + if [[ -r "$dir/memory.stat" ]]; then + inactive=$(awk -v key="$cache_key" '$1 == key {print $2; exit}' "$dir/memory.stat") + fi + [[ "$inactive" =~ ^[0-9]+$ ]] || inactive=0 + if [[ "$version" == cgroup2 && "$inactive" != 0 ]]; then + # A child's protected cache cannot be reclaimed by this limit. + # The target's own memory.min does not protect against its own reclaim. + if [[ ! -r "$dir" || ! -x "$dir" ]]; then + # Known files can be readable while child enumeration is denied. + inactive=0 + else + for child in "$dir"/* "$dir"/.[!.]* "$dir"/..?*; do + [[ -d "$child" ]] || continue + # An unsearchable directory can hide a protected cgroup. + if [[ ! -x "$child" ]]; then + inactive=0 + break + fi + [[ -e "$child/memory.current" && -e "$child/memory.max" ]] || continue + child_min="" + [[ ! -r "$child/memory.min" ]] || read -r child_min < "$child/memory.min" || true + if [[ "$child_min" != 0 ]]; then + inactive=0 + break + fi + done + fi + fi + # Inconsistent snapshots receive no cache credit. awk also avoids + # shell integer overflow for v1's unlimited sentinel. + available=$(awk -v limit="$limit" -v usage="$usage" -v inactive="$inactive" 'BEGIN { + if (inactive > usage) inactive = 0; + remaining = limit - (usage - inactive); + printf "%.0f\n", (remaining > 0 ? int(remaining / 1048576) : 0); + }') + if [[ -z "$smallest" ]] || awk -v a="$available" -v b="$smallest" \ + 'BEGIN {exit !(a < b)}'; then + smallest="$available" + fi + fi + [[ "$dir" != "$base" && "$dir" == "$base/"* ]] || break + dir="${dir%/*}" + done + [[ -z "$smallest" ]] || echo "$smallest" + return 0 +} + function free_memory() { local free="" local os=$(uname) @@ -420,6 +592,11 @@ function free_memory() { fi free=$(expr "$mem_free" + "$mem_buffer" + "$mem_cached") free=$(expr "$free" / 1024) + local cgroup_free=$(cgroup_free_memory) + if [[ -n "$cgroup_free" ]] && awk -v a="$cgroup_free" -v b="$free" \ + 'BEGIN {exit !(a < b)}'; then + free="$cgroup_free" + fi elif [ "$os" == "Darwin" ]; then local pages_free=$(vm_stat | awk '/Pages free/{print $0}' | awk -F'[:.]+' '{print $2}' | tr -d " ") local pages_inactive=$(vm_stat | awk '/Pages inactive/{print $0}' | awk -F'[:.]+' '{print $2}' | tr -d " ") @@ -473,13 +650,19 @@ function remove_with_prompt() { function ensure_path_writable() { local path=$1 + local error_handler="${2:-}" # Ensure input path exist if [ ! -d "${path}" ]; then mkdir -p "${path}" fi # Check for write permission if [ ! -w "${path}" ]; then - echo "No write permission on directory ${path}" + local message="No write permission on directory ${path}" + if [ -n "${error_handler}" ]; then + "${error_handler}" "${message}" + else + echo "${message}" + fi exit 1 fi } @@ -655,6 +838,10 @@ function kill_process_and_wait() { } function exit_with_usage_help(){ - echo "USAGE: $0 [-d true|false] [-g g1] [-m true|false] [-p true|false] [-s true|false] [-j java_options] [-t timeout] [-y true|false]" + echo "USAGE: $0 [-d true|false] [-g g1|G1|zgc|ZGC] [-m true|false] [-p true|false]" \ + "[-s true|false] [-j java_options] [-t timeout] [-y true|false]" + echo "Without -g, JVM/caller collector selection applies. -j applies when JAVA_OPTIONS is empty." + echo "Default/G1 tuning precedes caller options, so caller tuning values win." + echo "Do not combine -g with a different collector in JAVA_OPTIONS or -j." exit 1 } diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-partition.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-partition.sh index d14bc90244..014784d5bf 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-partition.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-partition.sh @@ -19,6 +19,21 @@ set -euo pipefail : "${STORE_REST:?STORE_REST not set}" +CHECK_PID="" +stop_check() { + local exit_code="$1" + trap '' TERM INT + if [[ -n "$CHECK_PID" ]]; then + # GNU timeout forwards TERM to the command group it owns, including + # the polling shell and its current curl/sleep, then reaps the shell. + kill -TERM "$CHECK_PID" 2>/dev/null || true + wait "$CHECK_PID" 2>/dev/null || true + fi + exit "$exit_code" +} +trap 'stop_check 143' TERM +trap 'stop_check 130' INT + timeout "${WAIT_PARTITION_TIMEOUT_S:-120}s" bash -c ' until curl -fsS "http://${STORE_REST}" 2>/dev/null | \ grep -q "\"partitionCount\":[1-9]" @@ -26,9 +41,19 @@ do echo "Waiting for partition assignment..." sleep 5 done -' +' & +CHECK_PID=$! +STATUS=0 +wait "$CHECK_PID" || STATUS=$? +CHECK_PID="" +[[ "$STATUS" -eq 0 ]] || exit "$STATUS" echo "Partitions detected:" URL="http://${STORE_REST}/v1/partitions" echo "$URL" -curl -v "$URL" +# The final diagnostic needs its own bound after partition polling succeeds. +timeout "${WAIT_PARTITION_TIMEOUT_S:-120}s" curl -v "$URL" & +CHECK_PID=$! +STATUS=0 +wait "$CHECK_PID" || STATUS=$? +exit "$STATUS" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh index 168efe8979..c5bc9c83de 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh @@ -29,7 +29,12 @@ function abs_path() { BIN=$(abs_path) TOP="$(cd "$BIN"/../ && pwd)" GRAPH_CONF="$TOP/conf/graphs/hugegraph.properties" -WAIT_STORAGE_TIMEOUT_S=300 +WAIT_STORAGE_TIMEOUT_S=${WAIT_STORAGE_TIMEOUT_S-300} +if [[ ! "${WAIT_STORAGE_TIMEOUT_S}" =~ ^[1-9][0-9]{0,4}$ ]] || \ + (( WAIT_STORAGE_TIMEOUT_S > 86400 )); then + echo "[wait-storage] ERROR: WAIT_STORAGE_TIMEOUT_S must be an integer from 1 to 86400 seconds" >&2 + exit 2 +fi WAIT_STORAGE_PD_CONNECT_TIMEOUT_S=2 WAIT_STORAGE_PD_MAX_TIMEOUT_S=3 diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-driver-settings.yaml b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-driver-settings.yaml index 55f38ab97d..f51a308e59 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-driver-settings.yaml +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-driver-settings.yaml @@ -17,9 +17,10 @@ hosts: [localhost] port: 8182 serializer: { - className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0, + className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1, config: { serializeResultToString: false, + builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml index 32135163fd..6cfdf61aef 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml @@ -28,7 +28,7 @@ graphs: { scriptEngines: { gremlin-groovy: { staticImports: [ - org.opencypher.gremlin.process.traversal.CustomPredicates.*', + org.apache.hugegraph.opencypher.CypherGremlinPredicates.*, org.opencypher.gremlin.traversal.CustomFunctions.* ], plugins: { @@ -37,7 +37,7 @@ scriptEngines: { org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: { classImports: [ java.lang.Math, - org.apache.hugegraph.backend.id.IdGenerator, + org.apache.hugegraph.id.IdGenerator, org.apache.hugegraph.type.define.Directions, org.apache.hugegraph.type.define.NodeRole, org.apache.hugegraph.masterelection.GlobalMasterInfo, @@ -67,11 +67,11 @@ scriptEngines: { org.apache.hugegraph.traversal.optimize.Text, org.apache.hugegraph.traversal.optimize.TraversalUtil, org.opencypher.gremlin.traversal.CustomFunctions, - org.opencypher.gremlin.traversal.CustomPredicate + org.apache.hugegraph.opencypher.CypherGremlinPredicates ], methodImports: [ java.lang.Math#*, - org.opencypher.gremlin.traversal.CustomPredicate#*, + org.apache.hugegraph.opencypher.CypherGremlinPredicates#*, org.opencypher.gremlin.traversal.CustomFunctions#* ] }, @@ -82,30 +82,49 @@ scriptEngines: { } } serializers: - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1, + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1, config: { serializeResultToString: false, + builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0, + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1, + config: { + serializeResultToString: false, + ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry, + org.apache.hugegraph.io.HugeGraphSONV1MessageIoRegistry] + } + } + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV2, + config: { + serializeResultToString: false, + ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] + } + } + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV3, config: { serializeResultToString: false, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV2d0, + # Keep untyped GraphSON before typed GraphSON so application/json stays + # mapped to untyped V1. Typed V1 is disabled because it accepts Java class names. + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV2, config: { serializeResultToString: false, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0, + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { serializeResultToString: false, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } +# Typed GraphSON remains fallback scope for File/Id/simple typed values. +# Full HugeGraph internal object typed GraphSON for schema/Vertex/Edge/Path +# remains follow-up scope; keep untyped serializers for normal remote objects. metrics: { consoleReporter: {enabled: false, interval: 180000}, csvReporter: {enabled: false, interval: 180000, fileName: ./metrics/gremlin-server-metrics.csv}, diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/log4j2.xml b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/log4j2.xml index 6286314d7b..bc40d2955e 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/log4j2.xml +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/log4j2.xml @@ -108,18 +108,23 @@ + + + + + diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote-objects.yaml b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote-objects.yaml index 39679d8c30..b8115773fe 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote-objects.yaml +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote-objects.yaml @@ -17,7 +17,7 @@ hosts: [localhost] port: 8182 serializer: { - className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0, + className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1, config: { serializeResultToString: false, # The duplication of HugeGraphIoRegistry is meant to fix a bug in the @@ -28,3 +28,5 @@ serializer: { ] } } +# Keep untyped V1 for the Cypher HTTP JSON path. The bundled Server does not +# register typed V1 because class-name deserialization can run before authentication. diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote.yaml b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote.yaml index 55f38ab97d..f51a308e59 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote.yaml +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/remote.yaml @@ -17,9 +17,10 @@ hosts: [localhost] port: 8182 serializer: { - className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0, + className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1, config: { serializeResultToString: false, + builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties index daeb092b77..be7b82a3b0 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties @@ -12,10 +12,20 @@ batch.max_write_ratio=80 batch.max_write_threads=0 # configuration of arthas +# Legacy aliases: arthas.telnetPort, arthas.httpPort, arthas.disabledCommands. +# The snake_case key takes precedence when both forms are configured. +# Different values for both forms produce a warning when the agent is activated. arthas.telnet_port=8562 arthas.http_port=8561 +# Defaults to local access. External binding (e.g. 0.0.0.0) exposes diagnostic commands; +# enable Arthas native authentication and restrict network access before exposing it. +# For API activation, set -Darthas.username=... -Darthas.password=... on the Server JVM. +# These credentials also apply to bin/arthas.sh; pass matching --username/--password for authenticated cleanup. +# Native loopback auth bypass is controlled by -Darthas.localConnectionNonAuth=false. +# Restart to apply JVM options; passwords in JVM arguments may appear in process listings. arthas.ip=127.0.0.1 -arthas.disabled_commands=jad +# Optional comma-separated disabled commands, for example jad,mc; empty allows all commands. +arthas.disabled_commands= # authentication configs #auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/toplingdb.yaml b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/toplingdb.yaml new file mode 100644 index 0000000000..3067b378f1 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/toplingdb.yaml @@ -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. +# +# common parameters +http: + # normally parent path of db path + document_root: ./topling/rocksdb_resource + listening_ports: '127.0.0.1:2011' + auto_start_http: false +Cache: + lru_cache: + class: LRUCache + params: + capacity: 8G + num_shard_bits: -1 + strict_capacity_limit: false + high_pri_pool_ratio: 0.5 + use_adaptive_mutex: false + metadata_charge_policy: kFullChargeCacheMetadata +Statistics: + stat: + class: default + params: + discard_tickers: + - rocksdb.block.cache + - rocksdb.block.cachecompressed + - rocksdb.block + - rocksdb.memtable.payload.bytes.at.flush + - rocksdb.memtable.garbage.bytes.at.flush + - rocksdb.txn + - rocksdb.blobdb + - rocksdb.row.cache + - rocksdb.number.block + - rocksdb.bloom.filter + - rocksdb.persistent + - rocksdb.sim.block.cache + discard_histograms: + # comment: .... + - rocksdb.blobdb + - rocksdb.bytes.compressed + - rocksdb.bytes.decompressed + - rocksdb.num.index.and.filter.blocks.read.per.level + - rocksdb.num.data.blocks.read.per.level + - rocksdb.compression.times.nanos + - rocksdb.decompression.times.nanos + - rocksdb.read.block.get.micros + - rocksdb.write.raw.block.micros + # comment end of array + #stats_level: kAll + stats_level: kDisableAll +MemTableRepFactory: + cspp: + class: cspp + params: + mem_cap: 16G + use_vm: false + token_use_idle: true + chunk_size: 16K + convert_to_sst: kFileMmap + sync_sst_file: false + skiplist: + class: SkipList + params: + lookahead: 0 +TableFactory: + cspp_memtab_sst: + class: CSPPMemTabTable + params: # empty params + bb: + class: BlockBasedTable + params: + checksum: kCRC32c + block_size: 4K + block_restart_interval: 16 + index_block_restart_interval: 1 + metadata_block_size: 4K + enable_index_compression: true + block_cache: "${lru_cache}" + readers: + BlockBasedTable: bb + CSPPMemTabTable: cspp_memtab_sst + block_cache_compressed: + persistent_cache: + filter_policy: + dispatch: + class: DispatcherTable + params: + default: bb + readers: + BlockBasedTable: bb + CSPPMemTabTable: cspp_memtab_sst + level_writers: [ bb, bb, bb, bb, bb, bb ] +CFOptions: + default: + max_write_buffer_number: 6 + memtable_factory: "${cspp}" + write_buffer_size: 128M + # set target_file_size_base as small as 512K is to make many SST files, + # thus key prefix cache can present efficiency + target_file_size_base: 64M + target_file_size_multiplier: 1 + table_factory: dispatch + max_bytes_for_level_base: 512M + max_bytes_for_level_multiplier: 10 + level_compaction_dynamic_level_bytes: false + level0_slowdown_writes_trigger: 20 + level0_stop_writes_trigger: 36 + level0_file_num_compaction_trigger: 2 + merge_operator: uint64add # support merge + level_compaction_dynamic_file_size: true + optimize_filters_for_hits: true + allow_merge_memtables: true + min_write_buffer_number_to_merge: 2 + compression_per_level: + - kNoCompression + - kNoCompression + - kSnappyCompression + - kSnappyCompression + - kSnappyCompression + - kSnappyCompression + - kSnappyCompression +DBOptions: + log: + create_if_missing: true + create_missing_column_families: true + default: + create_if_missing: true + create_missing_column_families: false # this is important, must be false to hugegraph + max_background_compactions: -1 + max_subcompactions: 4 + max_level1_subcompactions: 0 + inplace_update_support: false + WAL_size_limit_MB: 0 + statistics: "${stat}" + max_manifest_file_size: 100M + max_background_jobs: 8 + # Java org.rocksdb.WriteBatch has no Topling mmap WAL. With + # memtable_as_log_index enabled, every batch write fails: + # "WriteBatch has no mmap wal". Keep the option false for this client. + # convert_to_sst is still Topling-specific. Do not open data written by + # this profile with the standard RocksDB provider; restore a snapshot + # taken before migration instead. + compaction_readahead_size: 0 + memtable_as_log_index: false diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/check-java-runtime-logs.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/check-java-runtime-logs.sh new file mode 100755 index 0000000000..1d51d0ccaa --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/check-java-runtime-logs.sh @@ -0,0 +1,78 @@ +#!/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. +# + +set -uo pipefail + +# Inspect only logs from a positive run, never a negative-test fixture or a +# whole workspace. Libraries can print these exceptions and still return 0. +PATTERN='java\.lang\.reflect\.InaccessibleObjectException:' +PATTERN+='|java\.lang\.(IllegalAccessError|IllegalAccessException):.*module .*does not' +PATTERN+='|Unrecognized VM option|Unrecognized option:' +PATTERN+='|Ignoring option .*support was removed|VM option .*is (not supported|obsolete|removed)' +PATTERN+='|Unable to inspect cluster runtime logs:' +if [[ "${1:-}" == --native ]]; then + # Keep native-linkage checks out of suites with intentional missing classes. + PATTERN+='|undefined symbol|UnsatisfiedLinkError|UnsupportedClassVersionError|NoClassDefFoundError' + shift +fi +if [[ $# -eq 0 ]]; then + echo "USAGE: $0 [--native] LOG_FILE_OR_DIRECTORY..." >&2 + exit 2 +fi + +STATUS=0 +check_file() { + local file="$1" matches code + if [[ ! -f "$file" || ! -r "$file" ]]; then + echo "Cannot read runtime log: $file" >&2 + STATUS=2 + return + fi + # Scan text logs even when logged keys contain NUL bytes. + matches=$(LC_ALL=C grep -Eina -- "$PATTERN" "$file" 2>&1) + code=$? + if [[ $code -eq 0 ]]; then + echo "Java runtime compatibility error in $file:" >&2 + printf '%s\n' "$matches" >&2 + [[ $STATUS -eq 2 ]] || STATUS=1 + elif [[ $code -ne 1 ]]; then + printf '%s\n' "$matches" >&2 + STATUS=2 + fi +} + +for INPUT in "$@"; do + if [[ -d "$INPUT" ]]; then + # Exclude heap dumps and compressed rotations, even if they contain + # exception class names. Explicit file inputs can use any text suffix. + FILES=$(find "$INPUT" -type f \ + \( -name '*.log' -o -name '*.log.[0-9]*' -o -name '*.out' -o -name '*.err' -o -name '*.txt' \) \ + ! -name '*.gz' ! -name '*.zip' ! -name '*.bz2' ! -name '*.xz' ! -name '*.hprof*' -print) + if [[ $? -ne 0 || -z "$FILES" ]]; then + echo "Cannot collect runtime logs: $INPUT" >&2 + STATUS=2 + continue + fi + while IFS= read -r FILE; do + check_file "$FILE" + done <<<"$FILES" + else + check_file "$INPUT" + fi +done +exit "$STATUS" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/ci-service-utils.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/ci-service-utils.sh new file mode 100644 index 0000000000..6fc0069c0a --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/ci-service-utils.sh @@ -0,0 +1,214 @@ +#!/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. +# + +function dump_service_diagnostics() { + local service_dir="$1" + local service_name="$2" + local log_dir="${service_dir}/logs" + + echo "::group::${service_name} diagnostics" + echo "[ci] service dir: ${service_dir}" + echo "[ci] java processes:" + ps -ef | grep -E "HugeGraph|hg-|java" | grep -v grep || true + echo "[ci] listening tcp ports:" + (ss -ltnp || netstat -ltnp || true) 2>&1 + + if [ -d "${log_dir}" ]; then + # Heap dumps under logs/heapdump_*/ are binary and can be gigabytes; + # list them instead of printing them. + find "${log_dir}" -maxdepth 2 -type f -name '*.hprof*' -exec ls -l {} + || true + find "${log_dir}" -maxdepth 2 -type f ! -name '*.hprof*' | sort | while read -r log_file; do + echo "--- tail -n 200 ${log_file} ---" + tail -n 200 "${log_file}" || true + done + else + echo "[ci] log dir not found: ${log_dir}" + fi + echo "::endgroup::" +} + +function wait_for_tcp_port() { + local service_name="$1" + local host="$2" + local port="$3" + local pid_file="$4" + local service_dir="$5" + local timeout_seconds="${6:-90}" + + echo "[ci] waiting for ${service_name} at ${host}:${port}" + for second in $(seq 1 "${timeout_seconds}"); do + if bash -c "echo > /dev/tcp/${host}/${port}" >/dev/null 2>&1; then + echo "[ci] ${service_name} is listening on ${host}:${port}" + return 0 + fi + + if [ -f "${pid_file}" ]; then + local pid + pid="$(cat "${pid_file}")" + if [ -n "${pid}" ] && ! kill -0 "${pid}" >/dev/null 2>&1; then + echo "[ci] ${service_name} process ${pid} exited before readiness" + dump_service_diagnostics "${service_dir}" "${service_name}" + return 1 + fi + fi + + if [ "$((second % 10))" -eq 0 ]; then + echo "[ci] still waiting for ${service_name} (${second}s)" + fi + sleep 1 + done + + echo "[ci] timeout waiting for ${service_name} at ${host}:${port}" + dump_service_diagnostics "${service_dir}" "${service_name}" + return 1 +} + +function http_status_is_accepted() { + local status="$1" + local accepted_statuses="$2" + + case ",${accepted_statuses}," in + *",${status},"*) + return 0 + ;; + *) + return 1 + ;; + esac +} + +function wait_for_http_status() { + local service_name="$1" + local url="$2" + local pid_file="$3" + local service_dir="$4" + local timeout_seconds="${5:-90}" + local accepted_statuses="${6:-200}" + local connect_timeout_seconds=2 + local max_request_seconds=5 + local started_at="${SECONDS}" + local deadline=$((started_at + timeout_seconds)) + local next_log_at=10 + + echo "[ci] waiting for ${service_name} HTTP readiness at ${url}" + echo "[ci] accepted HTTP statuses: ${accepted_statuses}" + while (( SECONDS < deadline )); do + local remaining=$((deadline - SECONDS)) + local request_timeout="${max_request_seconds}" + if (( remaining < request_timeout )); then + request_timeout="${remaining}" + fi + if (( request_timeout < 1 )); then + break + fi + + local status + status="$(curl -s -o /dev/null -w "%{http_code}" \ + --connect-timeout "${connect_timeout_seconds}" \ + --max-time "${request_timeout}" \ + "${url}" 2>/dev/null)" || status="000" + if http_status_is_accepted "${status}" "${accepted_statuses}"; then + echo "[ci] ${service_name} is HTTP ready at ${url}" \ + "(status ${status})" + return 0 + fi + + if [ -f "${pid_file}" ]; then + local pid + pid="$(cat "${pid_file}")" + if [ -n "${pid}" ] && ! kill -0 "${pid}" >/dev/null 2>&1; then + echo "[ci] ${service_name} process ${pid} exited before" \ + "HTTP readiness" + dump_service_diagnostics "${service_dir}" "${service_name}" + return 1 + fi + fi + + local elapsed=$((SECONDS - started_at)) + if (( elapsed >= next_log_at )); then + echo "[ci] still waiting for ${service_name} HTTP readiness" \ + "(${elapsed}s, last status ${status})" + next_log_at=$((next_log_at + 10)) + fi + if (( SECONDS >= deadline )); then + break + fi + sleep 1 + done + + echo "[ci] timeout waiting for ${service_name} HTTP readiness at ${url}" + dump_service_diagnostics "${service_dir}" "${service_name}" + return 1 +} + +function process_is_running() { + local pid="$1" + local state + + if [[ ! "${pid}" =~ ^[0-9]+$ ]]; then + return 1 + fi + + if ! kill -0 "${pid}" 2>/dev/null; then + return 1 + fi + + state="$(ps -o stat= -p "${pid}" 2>/dev/null | tr -d '[:space:]')" || + state="" + [[ "${state}" != Z* ]] +} + +function wait_for_process_exit() { + local pid="$1" + local timeout_seconds="${2:-10}" + local deadline=$((SECONDS + timeout_seconds)) + + while process_is_running "${pid}"; do + if (( SECONDS >= deadline )); then + return 1 + fi + sleep 1 + done + return 0 +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + command="$1" + shift || true + case "${command}" in + dump) + dump_service_diagnostics "$@" + exit $? + ;; + wait) + wait_for_tcp_port "$@" + exit $? + ;; + wait-http) + wait_for_http_status "$@" + exit $? + ;; + *) + echo "Usage: $0 dump SERVICE_DIR SERVICE_NAME" + echo " $0 wait SERVICE_NAME HOST PORT PID_FILE SERVICE_DIR [TIMEOUT_SECONDS]" + echo " $0 wait-http SERVICE_NAME URL PID_FILE SERVICE_DIR" \ + "[TIMEOUT_SECONDS] [ACCEPTED_STATUSES]" + exit 2 + ;; + esac +fi diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml index 7e10eb52b0..37f5735897 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml @@ -33,7 +33,7 @@ scriptEngines: { org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: { classImports: [ java.lang.Math, - org.apache.hugegraph.backend.id.IdGenerator, + org.apache.hugegraph.id.IdGenerator, org.apache.hugegraph.type.define.Directions, org.apache.hugegraph.type.define.NodeRole, org.apache.hugegraph.masterelection.GlobalMasterInfo, @@ -63,9 +63,10 @@ scriptEngines: { org.apache.hugegraph.traversal.optimize.Text, org.apache.hugegraph.traversal.optimize.TraversalUtil, org.opencypher.gremlin.traversal.CustomFunctions, - org.opencypher.gremlin.traversal.CustomPredicate + org.apache.hugegraph.opencypher.CypherGremlinPredicates ], - methodImports: [java.lang.Math#*] + methodImports: [java.lang.Math#*, + org.apache.hugegraph.opencypher.CypherGremlinPredicates#*] }, org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: { files: [scripts/empty-sample.groovy] @@ -74,25 +75,40 @@ scriptEngines: { } } serializers: - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1, + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1, config: { serializeResultToString: false, + builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0, + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1, + config: { + serializeResultToString: false, + ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry, + org.apache.hugegraph.io.HugeGraphSONV1MessageIoRegistry] + } + } + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV2, + config: { + serializeResultToString: false, + ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] + } + } + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV3, config: { serializeResultToString: false, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV2d0, + # Typed V1 is disabled because it accepts Java class names. + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV2, config: { serializeResultToString: false, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0, + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { serializeResultToString: false, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml index 5f097f91bc..45a95ad7f8 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml @@ -33,7 +33,7 @@ scriptEngines: { org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: { classImports: [ java.lang.Math, - org.apache.hugegraph.backend.id.IdGenerator, + org.apache.hugegraph.id.IdGenerator, org.apache.hugegraph.type.define.Directions, org.apache.hugegraph.type.define.NodeRole, org.apache.hugegraph.masterelection.GlobalMasterInfo, @@ -63,9 +63,10 @@ scriptEngines: { org.apache.hugegraph.traversal.optimize.Text, org.apache.hugegraph.traversal.optimize.TraversalUtil, org.opencypher.gremlin.traversal.CustomFunctions, - org.opencypher.gremlin.traversal.CustomPredicate + org.apache.hugegraph.opencypher.CypherGremlinPredicates ], - methodImports: [java.lang.Math#*] + methodImports: [java.lang.Math#*, + org.apache.hugegraph.opencypher.CypherGremlinPredicates#*] }, org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: { files: [scripts/empty-sample.groovy] @@ -74,25 +75,40 @@ scriptEngines: { } } serializers: - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1, + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1, config: { serializeResultToString: false, + builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0, + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1, + config: { + serializeResultToString: false, + ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry, + org.apache.hugegraph.io.HugeGraphSONV1MessageIoRegistry] + } + } + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV2, + config: { + serializeResultToString: false, + ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] + } + } + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV3, config: { serializeResultToString: false, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV2d0, + # Typed V1 is disabled because it accepts Java class names. + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV2, config: { serializeResultToString: false, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0, + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { serializeResultToString: false, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml index 3f50c64778..4e61f9674a 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml @@ -33,7 +33,7 @@ scriptEngines: { org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: { classImports: [ java.lang.Math, - org.apache.hugegraph.backend.id.IdGenerator, + org.apache.hugegraph.id.IdGenerator, org.apache.hugegraph.type.define.Directions, org.apache.hugegraph.type.define.NodeRole, org.apache.hugegraph.masterelection.GlobalMasterInfo, @@ -63,9 +63,10 @@ scriptEngines: { org.apache.hugegraph.traversal.optimize.Text, org.apache.hugegraph.traversal.optimize.TraversalUtil, org.opencypher.gremlin.traversal.CustomFunctions, - org.opencypher.gremlin.traversal.CustomPredicate + org.apache.hugegraph.opencypher.CypherGremlinPredicates ], - methodImports: [java.lang.Math#*] + methodImports: [java.lang.Math#*, + org.apache.hugegraph.opencypher.CypherGremlinPredicates#*] }, org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: { files: [scripts/empty-sample.groovy] @@ -74,25 +75,40 @@ scriptEngines: { } } serializers: - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1, + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1, config: { serializeResultToString: false, + builder: org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0, + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1, + config: { + serializeResultToString: false, + ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry, + org.apache.hugegraph.io.HugeGraphSONV1MessageIoRegistry] + } + } + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV2, + config: { + serializeResultToString: false, + ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] + } + } + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV3, config: { serializeResultToString: false, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV2d0, + # Typed V1 is disabled because it accepts Java class names. + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV2, config: { serializeResultToString: false, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] } } - - {className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0, + - {className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { serializeResultToString: false, ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry] diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/hbase-ci-site.xml b/hugegraph-server/hugegraph-dist/src/assembly/travis/hbase-ci-site.xml new file mode 100644 index 0000000000..e1ff693e1c --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/hbase-ci-site.xml @@ -0,0 +1,76 @@ + + + + + + hbase.rootdir + file:///tmp/hbase + + + hbase.zookeeper.property.dataDir + /tmp/zookeeper + + + hbase.zookeeper.property.maxClientCnxns + 0 + + + zookeeper.session.timeout + 1200000 + + + zookeeper.znode.parent + /hbase + + + hbase.zookeeper.property.tickTime + 6000 + + + hbase.cluster.distributed + false + + + hbase.unsafe.stream.capability.enforce + false + + + hbase.zookeeper.quorum + localhost + + + hbase.master.hostname + localhost + + + hbase.regionserver.hostname + localhost + + + hbase.master.ipc.address + 0.0.0.0 + + + hbase.regionserver.ipc.address + 0.0.0.0 + + + hbase.wal.provider + filesystem + + diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/install-backend.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/install-backend.sh index ba1edb3234..97132044f4 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/install-backend.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/install-backend.sh @@ -31,7 +31,6 @@ fi case $BACKEND in hbase) - # TODO: replace it with hbase2.3+ to avoid java8 env "$TRAVIS_DIR"/install-hbase.sh ;; hstore) diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/install-hbase.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/install-hbase.sh index caadec7f81..47e5031d92 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/install-hbase.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/install-hbase.sh @@ -15,29 +15,110 @@ # See the License for the specific language governing permissions and # limitations under the License. # -set -ev +set -euo pipefail -TRAVIS_DIR=$(dirname $0) -HBASE_DOWNLOAD_ADDRESS="https://archive.apache.org/dist/hbase" -HBASE_VERSION="2.6.5" -HBASE_PACKAGE="hbase-${HBASE_VERSION}" -HBASE_TAR="${HBASE_PACKAGE}-bin.tar.gz" +TRAVIS_DIR=$(cd "$(dirname "$0")" && pwd) +HBASE_IMAGE="openeuler/hbase:2.6.5-oe2403sp3@sha256:9451eaec6ba241106d32421a6ab00e3b728a43689249b57f1f16b653b639f73f" +HBASE_CONTAINER="hg-ci-hbase-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}-${GITHUB_JOB:-test}" +HBASE_LABEL="hugegraph.ci.hbase=${HBASE_CONTAINER}" -# download hbase -if [ ! -f $HOME/downloads/${HBASE_TAR} ]; then - sudo wget -q -O $HOME/downloads/${HBASE_TAR} ${HBASE_DOWNLOAD_ADDRESS}/${HBASE_VERSION}/${HBASE_TAR} -fi +cleanup() { + # Remove only the container owned by this run/job, never other Docker resources. + local container_id + container_id=$(docker ps -aq --filter "label=${HBASE_LABEL}") + if [[ -n "$container_id" ]]; then + docker logs --tail 100 "$container_id" || true + docker rm -f "$container_id" + fi +} + +case "${1:-}" in + stop) + cleanup + exit 0 + ;; + '') + # The complete cold pull/start/read-write readiness path shares one budget. + exec timeout --signal=TERM --kill-after=10s 300s bash "$0" start + ;; + start) ;; + *) echo "Usage: $0 [stop]" >&2; exit 2 ;; +esac -# decompress hbase -sudo cp $HOME/downloads/${HBASE_TAR} ${HBASE_TAR} && tar xzf ${HBASE_TAR} +ready=false +finish() { + if [[ "$ready" != true ]]; then + cleanup || true + fi +} +trap finish EXIT +trap 'exit 143' TERM +trap 'exit 130' INT -# using tmpfs for the Hbase data directory reduces travis test runtime -sudo mkdir /mnt/ramdisk -sudo mount -t tmpfs -o size=1024m tmpfs /mnt/ramdisk -sudo ln -s /mnt/ramdisk /tmp/hbase +started=$SECONDS +docker pull "$HBASE_IMAGE" +pull_seconds=$((SECONDS - started)) +version=$(docker run --rm --entrypoint /usr/local/hbase/bin/hbase "$HBASE_IMAGE" version) +[[ "$version" == *"HBase 2.6.5"* ]] || { echo "Unexpected HBase version: $version" >&2; exit 1; } -# config hbase -sudo cp -f $TRAVIS_DIR/hbase-site.xml ${HBASE_PACKAGE}/conf +# CI runs on Linux: host networking keeps the advertised localhost RPC endpoints +# reachable by the host JVM without a separate ZooKeeper, RegionServer or HDFS. +docker run -d --name "$HBASE_CONTAINER" --label "$HBASE_LABEL" \ + --network host --memory 2g --env HBASE_HEAPSIZE=1024 \ + --mount "type=bind,source=${TRAVIS_DIR}/hbase-ci-site.xml,target=/usr/local/hbase/conf/hbase-site.xml,readonly" \ + --entrypoint /usr/local/hbase/bin/hbase "$HBASE_IMAGE" master start -# start hbase service -sudo ${HBASE_PACKAGE}/bin/start-hbase.sh +# Real HBase API read/write readiness, rather than accepting an open TCP port. +# Each attempt is bounded; the outer timeout also includes JVM startup and pull. +while true; do + [[ "$(docker inspect -f '{{.State.Running}}' "$HBASE_CONTAINER")" == true ]] || exit 1 + if timeout --signal=TERM --kill-after=2s 30s docker exec -i "$HBASE_CONTAINER" \ + /usr/local/hbase/bin/hbase shell -n <<'RUBY' +java_import org.apache.hadoop.hbase.HBaseConfiguration +java_import org.apache.hadoop.hbase.TableName +java_import org.apache.hadoop.hbase.client.ConnectionFactory +java_import org.apache.hadoop.hbase.client.TableDescriptorBuilder +java_import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder +java_import org.apache.hadoop.hbase.client.Put +java_import org.apache.hadoop.hbase.client.Get +java_import org.apache.hadoop.hbase.util.Bytes +configuration = HBaseConfiguration.create +configuration.setInt('hbase.client.operation.timeout', 10000) +configuration.setInt('hbase.rpc.timeout', 5000) +configuration.setInt('hbase.client.retries.number', 2) +connection = ConnectionFactory.createConnection(configuration) +admin = connection.getAdmin +name = TableName.valueOf('hugegraph_ci_probe') +family = Bytes.toBytes('f') +unless admin.tableExists(name) + descriptor = TableDescriptorBuilder.newBuilder(name) + .setColumnFamily(ColumnFamilyDescriptorBuilder.of(family)).build + admin.createTable(descriptor) +end +table = connection.getTable(name) +row = Bytes.toBytes('row') +column = Bytes.toBytes('value') +put = Put.new(row) +put.addColumn(family, column, Bytes.toBytes('ready')) +table.put(put) +value = table.get(Get.new(row)).getValue(family, column) +raise 'HBase read/write probe failed' unless Bytes.toString(value) == 'ready' +table.close +admin.disableTable(name) +admin.deleteTable(name) +admin.close +connection.close +exit +RUBY + then + break + fi + sleep 2 +done +ready=true +setup_seconds=$((SECONDS - started)) +echo "HBase cold pull: ${pull_seconds}s; complete preparation: ${setup_seconds}s" +if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then + printf '### HBase environment\n\n- Pinned HBase 2.6.5 image\n- Image pull: %ss\n- Pull, startup and read/write probe: %ss (300s budget)\n' \ + "$pull_seconds" "$setup_seconds" >> "$GITHUB_STEP_SUMMARY" +fi diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-api-test-for-raft.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-api-test-for-raft.sh index a48894728e..e884727ac7 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-api-test-for-raft.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-api-test-for-raft.sh @@ -65,3 +65,6 @@ $TRAVIS_DIR/build-report.sh $BACKEND $JACOCO_PORT $REPORT_FILE $TRAVIS_DIR/stop-server.sh $RAFT1_DIR $TRAVIS_DIR/stop-server.sh $RAFT2_DIR $TRAVIS_DIR/stop-server.sh $RAFT3_DIR + +# Inspect service output after shutdown so background failures cannot pass the suite. +bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RAFT1_DIR/logs" "$RAFT2_DIR/logs" "$RAFT3_DIR/logs" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-api-test.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-api-test.sh index 7ffa6b8685..0cd3d5a008 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-api-test.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-api-test.sh @@ -64,14 +64,51 @@ CONF=$SERVER_DIR/conf/graphs/hugegraph.properties REST_SERVER_CONF=$SERVER_DIR/conf/rest-server.properties GREMLIN_SERVER_CONF=$SERVER_DIR/conf/gremlin-server.yaml JACOCO_PORT=36320 +SERVER_STARTED=false -mvn package -Dmaven.test.skip=true -ntp +function stop_server() { + "$TRAVIS_DIR/stop-server.sh" "$SERVER_DIR" || return $? + SERVER_STARTED=false +} + +function cleanup() { + local status=$? + trap - EXIT + if [ "$SERVER_STARTED" == "true" ]; then + if ! stop_server; then + status=1 + fi + fi + exit "$status" +} + +trap cleanup EXIT + +mvn package -pl hugegraph-server/hugegraph-dist -am -Dmaven.test.skip=true -ntp if [[ ! -e "$SERVER_DIR/lib/ikanalyzer-2012_u6.jar" ]]; then download_to_dir "$SERVER_DIR/lib/" \ "https://raw.githubusercontent.com/apache/hugegraph-doc/ik_binary/dist/server/ikanalyzer-2012_u6.jar" fi +# Exercise the anonymous HTTP-to-Gremlin path before enabling authentication. +if grep -Eq '^[[:space:]]*auth\.authenticator=.' "$REST_SERVER_CONF" || \ + grep -Eq '^[[:space:]]*authentication[[:space:]]*:' "$GREMLIN_SERVER_CONF" || \ + grep -q '^gremlin.graph=.*HugeFactoryAuthProxy' "$CONF"; then + echo "Anonymous API tests require the packaged unauthenticated configuration" >&2 + exit 1 +fi +SERVER_STARTED=true +"$TRAVIS_DIR/start-server.sh" "$SERVER_DIR" "$BACKEND" "$JACOCO_PORT" || \ + (cat "$SERVER_DIR/logs/hugegraph-server.log" && exit 1) +mvn test -pl hugegraph-server/hugegraph-test -am -P api-test,"$BACKEND" \ + -Dtest=CypherAnonymousApiTest || \ + (cat "$SERVER_DIR/logs/hugegraph-server.log" && exit 1) +# Preserve this process's probes before the authenticated Server replaces it. +mvn -f hugegraph-server/hugegraph-test/pom.xml jacoco:dump@pull-test-data \ + -Dapp.host=localhost -Dapp.port="$JACOCO_PORT" -Dskip.dump=false -Djacoco.append=true +stop_server + # config rest-server sed_in_place '/^#*auth\.authenticator=/d' "$REST_SERVER_CONF" sed_in_place '/^#*auth\.admin_token=/d' "$REST_SERVER_CONF" @@ -90,6 +127,7 @@ authentication: { }" >> $GREMLIN_SERVER_CONF # start server +SERVER_STARTED=true $TRAVIS_DIR/start-server.sh $SERVER_DIR $BACKEND $JACOCO_PORT || (cat $SERVER_DIR/logs/hugegraph-server.log && exit 1) # run api-test @@ -103,4 +141,7 @@ fi $TRAVIS_DIR/build-report.sh $BACKEND $JACOCO_PORT $REPORT_FILE # stop server -$TRAVIS_DIR/stop-server.sh $SERVER_DIR +stop_server + +# Inspect service output after shutdown so background failures cannot pass the suite. +bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$SERVER_DIR/logs" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-gremlin-console-smoke-test.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-gremlin-console-smoke-test.sh index c2b693aff5..7380a73cc6 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-gremlin-console-smoke-test.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-gremlin-console-smoke-test.sh @@ -20,6 +20,7 @@ set -euo pipefail SERVER_DIR=${1:?Usage: run-gremlin-console-smoke-test.sh } SMOKE_MARKER="gremlin-console-smoke-ok" REMOTE_CONFIG="$SERVER_DIR/conf/remote.yaml" +TRAVIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) if [ ! -d "$SERVER_DIR" ]; then echo "Server directory does not exist: $SERVER_DIR" @@ -60,6 +61,9 @@ EOF cat > "$SMOKE_SCRIPT" <&1 | tee "$SMOKE_LOG" +bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$SMOKE_LOG" +grep -q "gremlin-console-auth-ok" "$SMOKE_LOG" grep -q "$SMOKE_MARKER" "$SMOKE_LOG" +grep -q "gremlin-console-gstring-ok" "$SMOKE_LOG" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-native-runtime-smoke-test.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-native-runtime-smoke-test.sh index 7cf09e30ee..f5c8d697ca 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-native-runtime-smoke-test.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-native-runtime-smoke-test.sh @@ -26,21 +26,21 @@ TRAVIS_DIR=$(cd "$(dirname "$0")" && pwd) SERVER_DIR=$(cd "$1" && pwd) SERVER_URL=${SERVER_URL:-http://127.0.0.1:8080} EXPECTED_ARCH=${EXPECTED_ARCH:-riscv64} -EXPECTED_JAVA_MAJOR=${EXPECTED_JAVA_MAJOR:-11} SERVER_START_ATTEMPTED=false +SERVER_START_COUNT=0 SERVER_STARTUP_TIMEOUT=${SERVER_STARTUP_TIMEOUT:-300} SERVER_START_COMMAND_TIMEOUT=$((SERVER_STARTUP_TIMEOUT + 30)) STORE_DUMP_TIMEOUT=${STORE_DUMP_TIMEOUT:-120} WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/hugegraph-native-runtime-smoke.XXXXXX") RUN_ID="$(date +%s)_$$" -export EXPECTED_ARCH EXPECTED_JAVA_MAJOR +export EXPECTED_ARCH cleanup() { local status=$? trap - EXIT if [[ "$SERVER_START_ATTEMPTED" == "true" ]]; then - "$SERVER_DIR/bin/stop-hugegraph.sh" -m false >/dev/null 2>&1 || status=1 + "$SERVER_DIR/bin/stop-hugegraph.sh" -m false || status=1 fi rm -rf "$WORK_DIR" exit "$status" @@ -48,18 +48,32 @@ cleanup() { trap cleanup EXIT start_server() { - if ! command -v timeout >/dev/null 2>&1; then - echo "Required command is unavailable: timeout" >&2 + if command -v timeout >/dev/null 2>&1; then + TIMEOUT_COMMAND=timeout + elif command -v gtimeout >/dev/null 2>&1; then + TIMEOUT_COMMAND=gtimeout + else + echo "Required command is unavailable: timeout or gtimeout" >&2 return 1 fi - timeout --foreground --kill-after=15s \ + local start_options=(-t "$SERVER_STARTUP_TIMEOUT") + if [[ -n "${SERVER_JAVA_OPTIONS:-}" ]]; then + start_options+=(-j "$SERVER_JAVA_OPTIONS") + fi + SERVER_START_COUNT=$((SERVER_START_COUNT + 1)) + local startup_log="$WORK_DIR/startup-$SERVER_START_COUNT.log" status=0 + # Daemon stdout must go to a file so a pipe reader does not wait for JVM exit. + "$TIMEOUT_COMMAND" --foreground --kill-after=15s \ "${SERVER_START_COMMAND_TIMEOUT}s" \ - "$SERVER_DIR/bin/start-hugegraph.sh" -t "$SERVER_STARTUP_TIMEOUT" + "$SERVER_DIR/bin/start-hugegraph.sh" "${start_options[@]}" \ + >"$startup_log" 2>&1 || status=$? + cat "$startup_log" + return "$status" } -"$TRAVIS_DIR/run-rocksdb-jni-smoke-test.sh" "$SERVER_DIR" +"$TRAVIS_DIR/run-rocksdb-jni-smoke-test.sh" "$SERVER_DIR" 2>&1 | tee "$WORK_DIR/jni.log" -"$SERVER_DIR/bin/init-store.sh" +"$SERVER_DIR/bin/init-store.sh" 2>&1 | tee "$WORK_DIR/init-store.log" SERVER_START_ATTEMPTED=true start_server "$TRAVIS_DIR/run-server-e2e-smoke-test.sh" "$SERVER_URL" create "$RUN_ID" | \ @@ -67,8 +81,8 @@ start_server grep -q '^server-e2e-smoke-create-ok$' "$WORK_DIR/create.log" "$SERVER_DIR/bin/stop-hugegraph.sh" -m false -timeout --foreground --kill-after=15s "${STORE_DUMP_TIMEOUT}s" \ - "$SERVER_DIR/bin/dump-store.sh" | tee "$WORK_DIR/store-dump.log" +"$TIMEOUT_COMMAND" --foreground --kill-after=15s "${STORE_DUMP_TIMEOUT}s" \ + "$SERVER_DIR/bin/dump-store.sh" 2>&1 | tee "$WORK_DIR/store-dump.log" grep -q 'Dump table VERTEX (offset 0 limit 20):' "$WORK_DIR/store-dump.log" grep -Fq "riscv-smoke-v1-$RUN_ID" "$WORK_DIR/store-dump.log" grep -Fq "riscv-smoke-v2-$RUN_ID" "$WORK_DIR/store-dump.log" @@ -79,13 +93,11 @@ start_server tee "$WORK_DIR/verify.log" grep -q '^server-e2e-smoke-verify-ok$' "$WORK_DIR/verify.log" -if [[ -d "$SERVER_DIR/logs" ]] && \ - grep -Eirq 'undefined symbol|UnsatisfiedLinkError|UnsupportedClassVersionError|NoClassDefFoundError' \ - "$SERVER_DIR/logs"; then - echo "Native linkage or Java compatibility error found in $SERVER_DIR/logs" >&2 - grep -Eirn 'undefined symbol|UnsatisfiedLinkError|UnsupportedClassVersionError|NoClassDefFoundError' \ - "$SERVER_DIR/logs" >&2 - exit 1 +LOG_INPUTS=("$WORK_DIR") +if [[ "${STDOUT_MODE:-false}" != true ]] || \ + [[ -n "$(find "$SERVER_DIR/logs" -type f -name '*.log' -print -quit 2>/dev/null)" ]]; then + LOG_INPUTS+=("$SERVER_DIR/logs") fi +"$TRAVIS_DIR/check-java-runtime-logs.sh" --native "${LOG_INPUTS[@]}" echo "native-runtime-smoke-ok" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-rocksdb-jni-smoke-test.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-rocksdb-jni-smoke-test.sh index 5603431d5d..0e1575f6a4 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-rocksdb-jni-smoke-test.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-rocksdb-jni-smoke-test.sh @@ -25,7 +25,11 @@ fi TRAVIS_DIR=$(cd "$(dirname "$0")" && pwd) SERVER_DIR=$(cd "$1" && pwd) EXPECTED_ARCH=${EXPECTED_ARCH:-} -EXPECTED_JAVA_MAJOR=${EXPECTED_JAVA_MAJOR:-11} +EXPECTED_JAVA_MAJOR=${EXPECTED_JAVA_MAJOR:-17} +# Callers may supply a full setup-java version; this check enforces its major. +EXPECTED_JAVA_MAJOR=${EXPECTED_JAVA_MAJOR%%.*} +EXPECTED_RISCV64_JAVA_VERSION=${EXPECTED_RISCV64_JAVA_VERSION:-} +EXPECTED_RISCV64_JAVA_VENDOR=${EXPECTED_RISCV64_JAVA_VENDOR:-} ACTUAL_ARCH=$(uname -m) if [[ -n "${JAVA_HOME:-}" ]]; then @@ -88,8 +92,8 @@ fi configure_riscv64_libatomic if [[ "$ACTUAL_ARCH" == "riscv64" ]]; then - EXPECTED_RISCV64_JAVA_VERSION=${EXPECTED_RISCV64_JAVA_VERSION:-11.0.31.28} - if [[ "$JAVA_VERSION" != "$EXPECTED_RISCV64_JAVA_VERSION" ]]; then + if [[ -n "$EXPECTED_RISCV64_JAVA_VERSION" && \ + "$JAVA_VERSION" != "$EXPECTED_RISCV64_JAVA_VERSION" ]]; then echo "Expected RISC-V Java $EXPECTED_RISCV64_JAVA_VERSION, got $JAVA_VERSION" >&2 exit 1 fi @@ -97,8 +101,10 @@ if [[ "$ACTUAL_ARCH" == "riscv64" ]]; then echo "Expected RISC-V Server VM, got $JAVA_VM_NAME" >&2 exit 1 fi - if [[ "$JAVA_VM_VENDOR" != "Alibaba" ]]; then - echo "Expected RISC-V Java vendor Alibaba, got $JAVA_VM_VENDOR" >&2 + if [[ -n "$EXPECTED_RISCV64_JAVA_VENDOR" && \ + "$JAVA_VM_VENDOR" != "$EXPECTED_RISCV64_JAVA_VENDOR" ]]; then + echo "Expected RISC-V Java vendor $EXPECTED_RISCV64_JAVA_VENDOR," \ + "got $JAVA_VM_VENDOR" >&2 exit 1 fi if [[ "$JAVA_VM_INFO" != *"mixed mode"* ]]; then diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-server-e2e-smoke-test.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-server-e2e-smoke-test.sh index 9762e4fa26..a29ae76bae 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-server-e2e-smoke-test.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-server-e2e-smoke-test.sh @@ -144,7 +144,16 @@ verify_graph() { wait_for_server request GET /versions 200 -assert_json 'type == "object" and length > 0' +ROOT_DIR=$(cd "$(dirname "$0")/../../../../.." && pwd) +EXPECTED_CORE_VERSION=$(sed -n 's:.*\([^<]*\).*:\1:p' "$ROOT_DIR/pom.xml") +EXPECTED_GREMLIN_VERSION=$(sed -n 's:.*\([^<]*\).*:\1:p' \ + "$ROOT_DIR/hugegraph-server/pom.xml") +EXPECTED_API_VERSION=$(sed -n 's:.*\([^<]*\).*:\1:p' \ + "$ROOT_DIR/hugegraph-server/hugegraph-api/pom.xml") +assert_json '.versions.version == "v1" and .versions.core == $core and + .versions.gremlin == $gremlin and .versions.api == $api' \ + --arg core "$EXPECTED_CORE_VERSION" --arg gremlin "$EXPECTED_GREMLIN_VERSION" \ + --arg api "$EXPECTED_API_VERSION" if [[ "$MODE" == "create" ]]; then request POST "$GRAPH_PATH/schema/propertykeys" 202 \ diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-tinkerpop-test.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-tinkerpop-test.sh index 87d60c8880..1e8de0caf5 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/run-tinkerpop-test.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/run-tinkerpop-test.sh @@ -17,13 +17,81 @@ # set -ev +if [[ $# -ne 2 ]]; then + echo "Usage: $0 BACKEND {structure|process|process-standard|process-feature|tinkerpop}" + exit 2 +fi + BACKEND=$1 SUITE=$2 +REPORT_DIR=hugegraph-server/hugegraph-test/target/surefire-reports -if [[ "$SUITE" == "structure" || "$SUITE" == "tinkerpop" ]]; then - mvn test -pl hugegraph-server/hugegraph-test -am -P tinkerpop-structure-test,$BACKEND -fi +function run_structure_test() { + run_selected_test tinkerpop-structure-test "StructureStandardTest" "StructureStandardTest" +} -if [[ "$SUITE" == "process" || "$SUITE" == "tinkerpop" ]]; then - mvn test -pl hugegraph-server/hugegraph-test -am -P tinkerpop-process-test,$BACKEND -fi +function run_process_test() { + run_selected_test tinkerpop-process-test \ + "ProcessStandardTest,HugeGraphFeatureTest,HugeGraphProviderLifecycleTest" \ + "ProcessStandardTest" "HugeGraphFeatureTest" "HugeGraphProviderLifecycleTest" +} + +function run_selected_test() { + local profile=$1 + local tests=$2 + shift 2 + if [[ $# -eq 0 ]]; then + echo "At least one expected Surefire report is required" + exit 2 + fi + local expected_reports=("$@") + local expected_report + local report + + for expected_report in "${expected_reports[@]}"; do + report="$REPORT_DIR/TEST-org.apache.hugegraph.tinkerpop.$expected_report.xml" + rm -f "$report" + done + mvn test -pl hugegraph-server/hugegraph-test -am \ + -P "$profile,$BACKEND" \ + -Dtest="$tests" \ + -Dsurefire.failIfNoSpecifiedTests=false + + for expected_report in "${expected_reports[@]}"; do + report="$REPORT_DIR/TEST-org.apache.hugegraph.tinkerpop.$expected_report.xml" + python3 - "$report" <<'PY' +import sys +import xml.etree.ElementTree as ET + +report = ET.parse(sys.argv[1]).getroot() +if int(report.attrib["tests"]) <= int(report.attrib.get("skipped", "0")): + sys.exit("Expected executed tests in Surefire report: " + sys.argv[1]) +PY + done +} + +case "$SUITE" in + structure) + run_structure_test + ;; + process) + run_process_test + ;; + process-standard) + run_selected_test tinkerpop-process-test \ + "ProcessStandardTest,HugeGraphProviderLifecycleTest" \ + "ProcessStandardTest" \ + "HugeGraphProviderLifecycleTest" + ;; + process-feature) + run_selected_test tinkerpop-process-test "HugeGraphFeatureTest" "HugeGraphFeatureTest" + ;; + tinkerpop) + run_structure_test + run_process_test + ;; + *) + echo "Unsupported TinkerPop suite: $SUITE" + exit 2 + ;; +esac diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/start-pd.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/start-pd.sh index 94d4f82fed..dc199321bd 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/start-pd.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/start-pd.sh @@ -30,6 +30,9 @@ else fi PD_DIR=$HOME_DIR/hugegraph-pd/apache-hugegraph-pd-$VersionInBash +TRAVIS_DIR=$(dirname "$0") + +source "$TRAVIS_DIR"/ci-service-utils.sh pushd $PD_DIR # conf/application.yml ships auth.secret-key empty on purpose, so PD would @@ -42,5 +45,6 @@ pushd $PD_DIR export SPRING_APPLICATION_JSON='{"auth":{"secret-key":"pd-ci-test-secret-not-for-production"}}' . bin/start-hugegraph-pd.sh ) -sleep 10 +wait_for_http_status HugeGraphPD http://127.0.0.1:8620/v1/ready \ + "$PD_DIR"/bin/pid "$PD_DIR" 90 200 popd diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/start-server.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/start-server.sh index b7b70e8cc9..05bf87e9c9 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/start-server.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/start-server.sh @@ -42,6 +42,15 @@ function sed_in_place() { esac } +# run-api-test.sh starts the Server twice on the same configuration, and a +# key appended twice is read back as a list, so append each line only once +function append_once() { + local line=$1 + local file=$2 + + grep -qxF "$line" "$file" || echo "$line" >> "$file" +} + case "$BACKEND" in memory) SERIALIZER=text @@ -64,18 +73,18 @@ write_property "$CONF" "serializer" "$SERIALIZER" # Set timeout for hbase if [ "$BACKEND" == "hbase" ]; then - echo "restserver.request_timeout=200" >> $REST_CONF - echo "gremlinserver.timeout=200" >> $REST_CONF + append_once "restserver.request_timeout=200" "$REST_CONF" + append_once "gremlinserver.timeout=200" "$REST_CONF" sed_in_place 's/evaluationTimeout.*/evaluationTimeout: 200000/' "$GREMLIN_CONF" fi # Set usePD=true for hstore if [ "$BACKEND" == "hstore" ]; then - echo "usePD=true" >> $REST_CONF + append_once "usePD=true" "$REST_CONF" fi # Append schema.sync_deletion=true to config file -echo "schema.sync_deletion=true" >> $CONF +append_once "schema.sync_deletion=true" "$CONF" JACOCO_OPTION="" if [ -n "$JACOCO_PORT" ]; then diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/start-store.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/start-store.sh index 353034201a..0f45483cb5 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/start-store.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/start-store.sh @@ -30,8 +30,12 @@ else fi STORE_DIR=$HOME_DIR/hugegraph-store/apache-hugegraph-store-$VersionInBash +TRAVIS_DIR=$(dirname "$0") + +source "$TRAVIS_DIR"/ci-service-utils.sh pushd $STORE_DIR . bin/start-hugegraph-store.sh -sleep 10 +wait_for_http_status HugeGraphStore http://127.0.0.1:8520/v1/health \ + "$STORE_DIR"/bin/pid "$STORE_DIR" 90 200,401 popd diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-arthas-cli.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-arthas-cli.sh new file mode 100644 index 0000000000..b4428ebff4 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-arthas-cli.sh @@ -0,0 +1,404 @@ +#!/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. +# +# Usage: bash test-arthas-cli.sh +# Real native attachment; all processes and files below belong to this test. +set -euo pipefail +[ "$#" -eq 1 ] || { echo "Supply a built Server or Store distribution" >&2; exit 1; } +python3 - "$1" <<'PY' +import os +import pathlib +import pty +import re +import select +import shlex +import shutil +import signal +import socket +import subprocess +import sys +import tempfile +import time +import zipfile + +built = pathlib.Path(sys.argv[1]).resolve() +store = (built / "bin/start-hugegraph-store.sh").is_file() +component = "Store" if store else "Server" +startup = "start-hugegraph-store.sh" if store else "start-hugegraph.sh" +other_startup = "start-hugegraph.sh" if store else "start-hugegraph-store.sh" +assert (built / "bin" / startup).is_file(), f"Not a Server or Store distribution: {built}" +java_home = os.environ.get("JAVA_HOME") +java = str(pathlib.Path(java_home) / "bin/java") if java_home else "java" +javac = str(pathlib.Path(java_home) / "bin/javac") if java_home else "javac" +with tempfile.TemporaryDirectory(prefix="arthas-test-") as work: + root = (pathlib.Path(work) / "native fixture").resolve() + for name in ("bin", "lib", "classes", "client-temp"): + (root / name).mkdir(parents=True) + for name in ("arthas.sh", "ArthasAttach.java"): + shutil.copy(built / "bin" / name, root / "bin" / name) + (root / "lib/arthas").symlink_to(built / "lib/arthas", target_is_directory=True) + (root / "bin" / startup).touch() + main = ("org.apache.hugegraph.store.node.StoreNodeApplication" if store else + "org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap") + package, name = main.rsplit(".", 1) + source = root / f"{name}.java" + source.write_text(f"package {package}; public class {name} {{ " + "public static void main(String[] args) throws Exception { " + "Thread.sleep(180000); }}") + subprocess.run([javac, "-d", str(root / "classes"), str(source)], check=True) + if store: + # Store starts from an executable jar; exercise that PID validation path + # using the built distribution's real bundled agent and attach helper. + fixture_jar = root / "lib/hg-store-node-fixture.jar" + with zipfile.ZipFile(fixture_jar, "w") as jar: + jar.writestr("META-INF/MANIFEST.MF", f"Manifest-Version: 1.0\nMain-Class: {main}\n\n") + for compiled in (root / "classes").rglob("*.class"): + jar.write(compiled, compiled.relative_to(root / "classes")) + launch = ["-jar", str(fixture_jar)] + else: + launch = ["-cp", str(root / "classes"), main] + reservations = [socket.socket() for _ in range(3)] + for reservation in reservations: + reservation.bind(("127.0.0.1", 0)) + safe_port, unsafe_telnet, unsafe_http = [s.getsockname()[1] for s in reservations] + for reservation in reservations: + reservation.close() + tunnel = socket.socket() + tunnel.bind(("127.0.0.1", 0)) + tunnel.listen() + tunnel.settimeout(0.5) + tunnel_url = f"ws://127.0.0.1:{tunnel.getsockname()[1]}/ws" + unsafe = root / "unsafe.properties" + unsafe.write_text(f"arthas.config.overrideAll=true\narthas.ip=0.0.0.0\n" + f"arthas.telnetPort={unsafe_telnet}\narthas.httpPort={unsafe_http}\n" + f"arthas.mcpEndpoint=/mcp\narthas.tunnelServer={tunnel_url}\n" + "arthas.disabledCommands=stop,session\n") + target_env = dict(os.environ, ARTHAS_CONFIG_LOCATION=str(unsafe), + ARTHAS_CONFIG_OVERRIDE_ALL="true", ARTHAS_CONFIG_OVERRIDEALL="true", ARTHAS_IP="0.0.0.0", + ARTHAS_TUNNELSERVER=tunnel_url, ARTHAS_TUNNEL_SERVER=tunnel_url, + ARTHAS_DISABLEDCOMMANDS="stop,session") + target_env["arthas.config.location"] = str(unsafe) + target_env["arthas.tunnelServer"] = tunnel_url + client_env = dict(os.environ, TMPDIR=str(root / "client-temp"), JAVA_TOOL_OPTIONS=f'-Djava.io.tmpdir="{root / "client-temp"}"') + script = ["bash", str(root / "bin/arthas.sh")] + client = script + ["--telnet-port", str(safe_port)] + + def run(arguments, success=True): + result = subprocess.run(arguments, env=client_env, text=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, input="", timeout=45) + assert (result.returncode == 0) == success, result.stdout + return result.stdout + + def can_connect(host, port): + try: + with socket.create_connection((host, port), timeout=1): + return True + except OSError: + return False + + def wait_port(opened, host="127.0.0.1"): + deadline = time.monotonic() + 15 + while can_connect(host, safe_port) != opened: + assert time.monotonic() < deadline, f"Listener did not become {'open' if opened else 'closed'}" + time.sleep(0.1) + + class Terminal: + # fork gives the helper a real controlling terminal: closing its master + # reproduces terminal hangup, including the kernel's process-group HUP. + def __init__(self, shell=False, options=()): + self.pid, self.master = pty.fork() + if self.pid == 0: + arguments = ["bash", "--noprofile", "--norc", "-i"] if shell else client + list(options) + os.execvpe(arguments[0], arguments, client_env) + self.returncode = None + self.output = "" + if shell: + os.write(self.master, (shlex.join(client) + "\n").encode()) + + def poll(self): + if self.returncode is None: + child, status = os.waitpid(self.pid, os.WNOHANG) + if child: + self.returncode = os.waitstatus_to_exitcode(status) + return self.returncode + + def drain(self): + if self.master is not None and select.select([self.master], [], [], 0)[0]: + try: + output = os.read(self.master, 65536).decode(errors="replace") + self.output += output + log.write(output) + log.flush() + except OSError: + pass # The slave closes when the helper exits. + + def ready(self): + deadline = time.monotonic() + 15 + while "[arthas@" not in self.output: + self.drain() + assert self.poll() is None and time.monotonic() < deadline, self.output + time.sleep(0.02) + + def wait(self, timeout=30): + deadline = time.monotonic() + timeout + while self.poll() is None: + self.drain() + assert time.monotonic() < deadline, "Interactive helper did not exit" + time.sleep(0.02) + self.drain() + if self.master is not None: + os.close(self.master) + self.master = None + return self.returncode + + interactive = None + with (root / "jvm.log").open("w") as log: + process = subprocess.Popen([java, "-Darthas.config.location=" + str(unsafe), + "-Darthas.config.overrideAll=true", "-Darthas.ip=0.0.0.0", + "-Darthas.tunnelServer=" + tunnel_url, + "-Darthas.disabledCommands=stop,session", + *launch, "fixture"], env=target_env, stdout=log, stderr=subprocess.STDOUT) + try: + (root / "bin/pid").write_text(str(process.pid)) + time.sleep(1) + for options in (["--pid", "-1"], ["--pid", "1; unsafe"], ["--pid", "9999999999"], + ["--telnet-port", "65536"], ["--target-ip", "::not-an-ip"], + ["--username", "line\nbreak"], + ["--http-port", "8563"], ["--tunnel-server", tunnel_url]): + output = run(script + options + ["--command", "version"], success=False) + if options[0] == "--username": + assert "single-line values" in output and "Waiting for PID" not in output, output + assert not can_connect("127.0.0.1", safe_port) + # EOF and piped input must fail before attaching; batch callers need --command. + for input_text in ("", "version\n"): + result = subprocess.run(client, env=client_env, input=input_text, text=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10) + assert result.returncode != 0 and "use --command" in result.stdout, result.stdout + assert not can_connect("127.0.0.1", safe_port) + # A live PID from a different component must not receive the agent. + (root / "bin" / startup).unlink() + (root / "bin" / other_startup).touch() + run(client + ["--command", "version"], success=False) + (root / "bin" / other_startup).unlink() + (root / "bin" / startup).touch() + output = run(client + ["--pid", str(process.pid), "--command", "version"]) + assert re.search(r"\n4\.1\.5\s*\n", output), output + assert (f"Process {process.pid} already using port {safe_port}" in output or + f"The target process already listen port {safe_port}, skip attach." in output), output + wait_port(False) + assert process.poll() is None, "One-time diagnosis stopped the application JVM" + # Keep a real interactive client open while inspecting the live agent. + interactive = Terminal() + wait_port(True) + assert not can_connect("127.0.0.1", unsafe_telnet) + assert not can_connect("127.0.0.1", unsafe_http) + try: + connection, address = tunnel.accept() + except socket.timeout: + pass + else: + connection.close() + raise AssertionError(f"Arthas contacted the overridden tunnel endpoint: {address}") + # Inspect actual bind addresses; VPNs may redirect connection probes. + if sys.platform.startswith("linux"): + inodes = set() + for descriptor in pathlib.Path(f"/proc/{process.pid}/fd").iterdir(): + try: + link = os.readlink(descriptor) + except FileNotFoundError: + continue + if link.startswith("socket:["): + inodes.add(link[8:-1]) + addresses = [] + for table in ("tcp", "tcp6"): + for line in pathlib.Path(f"/proc/{process.pid}/net/{table}").read_text().splitlines()[1:]: + fields = line.split() + address, port = fields[1].split(":") + if fields[3] == "0A" and fields[9] in inodes: + addresses.append((address, int(port, 16))) + assert len(addresses) == 1 and addresses[0][1] == safe_port, addresses + assert addresses[0][0] in ("0100007F", "0000000000000000FFFF00000100007F"), addresses + else: + listeners = subprocess.check_output(["lsof", "-nP", "-a", "-p", str(process.pid), + "-iTCP", "-sTCP:LISTEN"], text=True).splitlines()[1:] + assert len(listeners) == 1 and f"127.0.0.1:{safe_port} " in listeners[0], listeners + os.write(interactive.master, b"quit\n") + assert interactive.wait(45) == 0, (root / "jvm.log").read_text() + wait_port(False) + # Interactive Bash propagates the terminal HUP to its foreground job. + # Closing a direct helper PTY alone does not reproduce that signal sequence. + interactive = Terminal(shell=True) + wait_port(True) + interactive.ready() + os.close(interactive.master) + interactive.master = None + status = interactive.wait() + assert status in (-signal.SIGHUP, 128 + signal.SIGHUP), status + wait_port(False) + assert process.poll() is None, "Terminal hangup stopped the application JVM" + # Signals sent only to the helper must also clean up its child/client. + for termination in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT): + interactive = Terminal() + if termination == signal.SIGTERM: + # Terminate during source compilation/attachment, before a listener exists. + time.sleep(0.2) + else: + wait_port(True) + interactive.ready() + os.kill(interactive.pid, termination) + if termination != signal.SIGTERM: + # Repeated/mixed signals during cleanup must not interrupt stop_agent. + for repeat in (signal.SIGHUP, signal.SIGINT, signal.SIGTERM) * 5: + time.sleep(0.02) + if interactive.poll() is None: + os.kill(interactive.pid, repeat) + assert interactive.wait() == 128 + termination, (root / "jvm.log").read_text() + wait_port(False) + assert process.poll() is None, "Terminal/signal cleanup stopped the application JVM" + # Linux mktemp honors TMPDIR, exposing only this fixture's marker. + # macOS mktemp ignores it; the other terminal/signal cases run there. + if sys.platform.startswith("linux"): + # Pause after validation so loadAgent cannot finish before the + # signal reaches both the helper and its attach JVM. + for termination in (signal.SIGTERM, signal.SIGHUP): + interactive = Terminal() + deadline = time.monotonic() + 15 + while not list((root / "client-temp").glob("tmp.*/validated")): + interactive.drain() + assert interactive.poll() is None and time.monotonic() < deadline, interactive.output + time.sleep(0.001) + os.kill(process.pid, signal.SIGSTOP) + try: + assert not can_connect("127.0.0.1", safe_port), "Agent started before the attach pause" + # Queue the attach request before signaling the group. + time.sleep(0.1) + os.killpg(interactive.pid, termination) + if termination == signal.SIGHUP: + # Kernel and interactive Bash can deliver HUP almost together. + time.sleep(0.00006) + os.killpg(interactive.pid, termination) + time.sleep(1) + assert interactive.poll() is None, "Group signal interrupted in-flight attachment" + finally: + os.kill(process.pid, signal.SIGCONT) + assert interactive.wait() == 128 + termination, interactive.output + wait_port(False) + assert process.poll() is None, "Group signal stopped the application JVM" + # macOS requires an explicit interface alias for 127.0.0.2; Linux + # routes all of 127/8 locally and exercises that address in CI. + hosts = ("127.0.0.2", "::1", "localhost") if sys.platform.startswith("linux") else ("::1", "localhost") + for host in (*hosts, "0.0.0.0", "::"): + output = run(client + ["--target-ip", host, "--command", "version"]) + assert re.search(r"\n4\.1\.5\s*\n", output), output + connect_host = {"localhost": "127.0.0.1", "0.0.0.0": "127.0.0.1", "::": "127.0.0.1"}.get(host, host) + endpoint = f"[{connect_host}]:{safe_port}" if ":" in connect_host else f"{connect_host}:{safe_port}" + assert output.count(f"Arthas listener closed at {endpoint}") == 1, output + wait_port(False, connect_host) + run(client + ["--command", "stop"]) + wait_port(False) + # Disable native local authentication bypass for the following + # attachments; earlier signal cases exercise native no-auth defaults. + run(client + ["--command", "sysprop arthas.localConnectionNonAuth false"]) + wait_port(False) + password = "p a;ss'\"\\word" + auth = ["--username", "operator name", "--password", password] + for options in (auth, ["--password", password]): + output = run(client + options + ["--command", "version"]) + assert re.search(r"\n4\.1\.5\s*\n", output), output + assert password not in output and "auth --username" not in output, output + wait_port(False) + assert process.poll() is None, "Authenticated command stopped the application JVM" + interactive = Terminal(options=auth) + wait_port(True) + interactive.ready() + # An independent unauthenticated loopback client must be rejected. + unauthenticated = run([java, "-jar", str(root / "lib/arthas/arthas-client.jar"), + "-c", "version", "127.0.0.1", str(safe_port)]) + assert "command not permitted" in unauthenticated, unauthenticated + assert not re.search(r"\n4\.1\.5\s*\n", unauthenticated), unauthenticated + # A running agent retains its credentials; invalid credentials must + # fail diagnosis/cleanup without stopping the existing listener. + wrong = run(client + ["--username", "operator name", "--password", "incorrect ; secret", + "--command", "version"], success=False) + assert "command not permitted" in wrong and "incorrect ; secret" not in wrong, wrong + assert "credentials rejected" in wrong and "agent left running" in wrong, wrong + wait_port(True) + quoted_password = "'" + password.replace("'", "'\\''") + "'" + os.write(interactive.master, + f'auth --username "operator name" {quoted_password}\nversion\nquit\n'.encode()) + assert interactive.wait(45) == 0, interactive.output + assert re.search(r"[\r\n]4\.1\.5\s*[\r\n]", interactive.output), interactive.output + wait_port(False) + assert process.poll() is None, "Authenticated quit stopped the application JVM" + # Authentication echoes must not let one JVM impersonate another + # agent's session row. Both JVMs are valid targets in this fixture. + other = subprocess.Popen([java, "-Darthas.localConnectionNonAuth=false", *launch, "other fixture"], + env=target_env, stdout=log, stderr=subprocess.STDOUT) + try: + time.sleep(1) + impersonating = ["--username", f"prefix JAVA_PID {process.pid} suffix", + "--password", "mismatch ; secret"] + interactive = Terminal(options=["--pid", str(other.pid), *impersonating]) + wait_port(True) + interactive.ready() + missing = run(script + ["--pid", str(other.pid), "--telnet-port", str(unsafe_http), + *impersonating, "--command", "version"], success=False) + assert f"no Arthas listener at 127.0.0.1:{unsafe_http}" in missing, missing + assert "API default 8562" in missing and "retry with --telnet-port" in missing, missing + assert "Arthas listener closed at" not in missing, missing + assert can_connect("127.0.0.1", safe_port), "Missing-port diagnosis stopped the existing agent" + mismatch = run(client + ["--pid", str(process.pid), *impersonating, + "--command", "version"], success=False) + assert f"belongs to PID {other.pid}, expected {process.pid}" in mismatch, mismatch + assert can_connect("127.0.0.1", safe_port), "PID mismatch stopped another JVM's agent" + assert process.poll() is None and other.poll() is None + # This helper owns the other JVM. The misleading username must + # also not prevent its legitimate exit cleanup. + os.kill(interactive.pid, signal.SIGTERM) + assert interactive.wait() == 128 + signal.SIGTERM, interactive.output + wait_port(False) + assert "Arthas listener closed at" in interactive.output, interactive.output + assert other.poll() is None, "Agent cleanup stopped the other application JVM" + finally: + if interactive is not None and interactive.poll() is None: + os.kill(interactive.pid, signal.SIGKILL) + interactive.wait(10) + other.terminate() + try: + other.wait(timeout=10) + except subprocess.TimeoutExpired: + other.kill() + other.wait() + assert not can_connect("127.0.0.1", safe_port) + assert not list((root / "client-temp").glob("hugegraph-arthas-*.properties")) + assert not list((root / "client-temp").glob("tmp.*")), "Helper state was not removed" + print(f"PASS: {component} PID/argument boundaries, hostile config, default loopback bind, no tunnel, " + "command/quit/terminal/group/repeated-signal cleanup, explicit binds/authentication without restart") + finally: + if interactive is not None and interactive.poll() is None: + os.kill(interactive.pid, signal.SIGKILL) + interactive.wait(10) + tunnel.close() + if process.poll() is None: + os.kill(process.pid, signal.SIGCONT) + process.terminate() + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + process.kill() + process.wait() +PY diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-cgroup-memory.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-cgroup-memory.sh new file mode 100755 index 0000000000..860bbffe81 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-cgroup-memory.sh @@ -0,0 +1,385 @@ +#!/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. + +# Verify hard-limit sizing without starting HugeGraph or requiring root. +set -euo pipefail +UTIL=${1:?Usage: $0 UTIL_SH} +# The fixture supplies util.sh; calc_xmx invokes the free_memory hooks below. +# shellcheck disable=SC1090,SC2329 +source "$UTIL" +FIXTURE=$(mktemp -d) +trap 'chmod -R u+rwX "$FIXTURE"; rm -rf "$FIXTURE"' EXIT +ROOT="$FIXTURE/cgroup" +MEMBERSHIP="$FIXTURE/membership" +MOUNTINFO="$FIXTURE/mountinfo" +mkdir -p "$ROOT" +printf '0\n' > "$ROOT/memory.min" +assert_memory() { + local expected=$1 actual + actual=$(cgroup_free_memory "$ROOT" "$MEMBERSHIP" "$MOUNTINFO") + [[ "$actual" == "$expected" ]] || { + echo "expected cgroup MiB '$expected', got '$actual'" >&2 + exit 1 + } +} +# Verify unknown usage and its zero-budget path through the actual launcher helpers. +assert_unknown_usage() { + local usage_file=$1 valid_usage=$2 + printf 'invalid\n' > "$usage_file" + assert_memory 0 + : > "$usage_file" + assert_memory 0 + rm "$usage_file" + assert_memory 0 + ( + # Only supply host data; the cgroup helper still reads the real fixture. + # shellcheck disable=SC2329 + uname() { [[ "${1:-}" != -m ]] && echo Linux || echo x86_64; } + # shellcheck disable=SC2329 + cat() { + if [[ "$1" == /proc/meminfo ]]; then + printf 'MemFree: 1048576 kB\nBuffers: 0 kB\nCached: 0 kB\n' + else + command cat "$@" + fi + } + # shellcheck disable=SC2329 + cgroup_free_memory() { + bash -c 'source "$1"; cgroup_free_memory "$2" "$3" "$4"' fixture \ + "$UTIL" "$ROOT" "$MEMBERSHIP" "$MOUNTINFO" + } + [[ $(free_memory) == 0 ]] + if (calc_xmx 512 32768); then + echo 'unknown cgroup usage fell back to host memory' >&2 + exit 1 + fi + ) + printf '%s\n' "$valid_usage" > "$usage_file" + chmod 000 "$usage_file" + if [[ ! -r "$usage_file" ]]; then + assert_memory 0 + fi + chmod 600 "$usage_file" +} +# No controller keeps the original host fallback. +assert_memory "" +printf '0::/\n' > "$MEMBERSHIP" +printf 'max\n' > "$ROOT/memory.max" +printf '12345\n' > "$ROOT/memory.current" +assert_memory "" +# Four GiB limit, one GiB already used: three GiB remain, not host RAM. +printf '4294967296\n' > "$ROOT/memory.max" +printf '1073741824\n' > "$ROOT/memory.current" +assert_memory 3072 +assert_unknown_usage "$ROOT/memory.current" 1073741824 +assert_memory 3072 +# Usage at/over limit must not become a negative or wrapped number. +printf '4294967296\n' > "$ROOT/memory.current" +assert_memory 0 +printf '5368709120\n' > "$ROOT/memory.current" +assert_memory 0 +# A finite ancestor constrains an unlimited child. +mkdir -p "$ROOT/team/service" +printf '0\n' > "$ROOT/team/memory.min" +printf '0\n' > "$ROOT/team/service/memory.min" +printf '0::/team/service\n' > "$MEMBERSHIP" +printf 'max\n' > "$ROOT/team/service/memory.max" +printf '0\n' > "$ROOT/team/service/memory.current" +printf '2147483648\n' > "$ROOT/team/memory.max" +printf '536870912\n' > "$ROOT/team/memory.current" +printf '1073741824\n' > "$ROOT/memory.current" +assert_memory 1536 +# A stricter child wins; usage is per-level, not subtracted twice. +printf '1073741824\n' > "$ROOT/team/service/memory.max" +printf '268435456\n' > "$ROOT/team/service/memory.current" +assert_memory 768 +# Host-relative membership in a private namespace falls back to mount root. +printf '0::/not-visible/container\n' > "$MEMBERSHIP" +assert_memory 3072 +# Malformed limits do not introduce arithmetic errors or an artificial zero. +printf 'invalid\n' > "$ROOT/memory.max" +assert_memory "" +# The v1 fixture represents a known 64-bit Linux kernel, independently of the +# test host's userspace or machine name (the checks also run on macOS). +# shellcheck disable=SC2329 +uname() { [[ "${1:-}" != -m ]] && command uname "$@" || echo x86_64; } +export -f uname +PAGE_SIZE=$(getconf PAGESIZE) +V1_UNLIMITED=$((9223372036854775807 / PAGE_SIZE * PAGE_SIZE)) +mkdir -p "$ROOT/memory/job" +printf '5:cpu,cpuacct:/job\n6:memory:/job\n' > "$MEMBERSHIP" +printf '%s\n' "$V1_UNLIMITED" > "$ROOT/memory/memory.limit_in_bytes" +printf '0\n' > "$ROOT/memory/memory.usage_in_bytes" +printf '2147483648\n' > "$ROOT/memory/job/memory.limit_in_bytes" +printf '536870912\n' > "$ROOT/memory/job/memory.usage_in_bytes" +assert_memory 1536 +assert_unknown_usage "$ROOT/memory/job/memory.usage_in_bytes" 536870912 +assert_memory 1536 +# A v1 unlimited child must not reject unreadable usage or hide a finite ancestor. +printf '%s\n' "$V1_UNLIMITED" > "$ROOT/memory/job/memory.limit_in_bytes" +printf '2147483648\n' > "$ROOT/memory/memory.limit_in_bytes" +printf '536870912\n' > "$ROOT/memory/memory.usage_in_bytes" +printf 'invalid\n' > "$ROOT/memory/job/memory.usage_in_bytes" +assert_memory 1536 +# The existing 64-bit/4-KiB sentinel is also an unlimited boundary. +printf '9223372036854771712\n' > "$ROOT/memory/job/memory.limit_in_bytes" +assert_memory 1536 +printf '%s\n' "$V1_UNLIMITED" > "$ROOT/memory/job/memory.limit_in_bytes" +rm "$ROOT/memory/job/memory.usage_in_bytes" +assert_memory 1536 +printf '536870912\n' > "$ROOT/memory/job/memory.usage_in_bytes" +chmod 000 "$ROOT/memory/job/memory.usage_in_bytes" +assert_memory 1536 +chmod 600 "$ROOT/memory/job/memory.usage_in_bytes" +# Native-query failures must keep unknown numeric-limit usage conservative. +rm "$ROOT/memory/job/memory.usage_in_bytes" +# The page immediately below the sentinel is finite and still needs usage. +printf '%s\n' "$((V1_UNLIMITED - PAGE_SIZE))" > "$ROOT/memory/job/memory.limit_in_bytes" +assert_memory 0 +printf '%s\n' "$V1_UNLIMITED" > "$ROOT/memory/job/memory.limit_in_bytes" +( + # shellcheck disable=SC2329 + getconf() { return 127; } + assert_memory 0 +) +( + # shellcheck disable=SC2329 + getconf() { echo invalid; } + assert_memory 0 +) +( + # shellcheck disable=SC2329 + getconf() { [[ "$1" != PAGESIZE ]] || return 0; } + assert_memory 0 +) +# A 32-bit userspace cannot turn an 8-TiB finite limit into unlimited. +( + # shellcheck disable=SC2329 + getconf() { [[ "$1" != LONG_BIT ]] && echo 4096 || echo 32; } + printf '8796093022208\n' > "$ROOT/memory/memory.limit_in_bytes" + printf '8795824586752\n' > "$ROOT/memory/memory.usage_in_bytes" + printf '9223372036854771712\n' > "$ROOT/memory/job/memory.limit_in_bytes" + for machine in x86_64 aarch64 riscv64; do + # shellcheck disable=SC2329 + uname() { echo "$machine"; } + assert_memory 256 + done + # Unknown/32-bit-looking names, including personality-masked uname, must + # retain finite limits rather than infer a 32-bit unlimited sentinel. + printf '8796093022208\n' > "$ROOT/memory/job/memory.limit_in_bytes" + printf '8795824586752\n' > "$ROOT/memory/job/memory.usage_in_bytes" + for machine in unknown i686 armv8l; do + assert_memory 256 + rm "$ROOT/memory/job/memory.usage_in_bytes" + assert_memory 0 + printf '9223372036854771712\n' > "$ROOT/memory/job/memory.limit_in_bytes" + assert_memory 0 + printf '8796093022208\n' > "$ROOT/memory/job/memory.limit_in_bytes" + printf '8795824586752\n' > "$ROOT/memory/job/memory.usage_in_bytes" + done + # shellcheck disable=SC2329 + uname() { return 127; } + assert_memory 256 + rm "$ROOT/memory/job/memory.usage_in_bytes" + assert_memory 0 + printf '9223372036854771712\n' > "$ROOT/memory/job/memory.limit_in_bytes" + assert_memory 0 +) +# Restore the original finite-ancestor/unlimited-child fixture. +printf '2147483648\n' > "$ROOT/memory/memory.limit_in_bytes" +printf '536870912\n' > "$ROOT/memory/memory.usage_in_bytes" +printf '%s\n' "$V1_UNLIMITED" > "$ROOT/memory/job/memory.limit_in_bytes" +# With no finite limits, unknown unlimited usage still permits host sizing. +printf '%s\n' "$V1_UNLIMITED" > "$ROOT/memory/memory.limit_in_bytes" +rm "$ROOT/memory/memory.usage_in_bytes" +assert_memory "" +( + # Supply host data; the actual helper reads the unlimited fixture. + # shellcheck disable=SC2329 + uname() { [[ "${1:-}" != -m ]] && echo Linux || echo x86_64; } + # shellcheck disable=SC2329 + cat() { + if [[ "$1" == /proc/meminfo ]]; then + printf 'MemFree: 1048576 kB\nBuffers: 0 kB\nCached: 0 kB\n' + else + command cat "$@" + fi + } + # shellcheck disable=SC2329 + cgroup_free_memory() { + bash -c 'source "$1"; cgroup_free_memory "$2" "$3" "$4"' fixture \ + "$UTIL" "$ROOT" "$MEMBERSHIP" "$MOUNTINFO" + } + [[ $(free_memory) == 1024 ]] + [[ $(calc_xmx 512 32768) == 512 ]] +) +printf '0\n' > "$ROOT/memory/memory.usage_in_bytes" +printf '2147483648\n' > "$ROOT/memory/job/memory.limit_in_bytes" +printf '536870912\n' > "$ROOT/memory/job/memory.usage_in_bytes" +assert_memory 1536 +# Keep launcher min/max semantics; insufficient effective memory fails. +printf '0::/unified\n6:memory:/job\n' > "$MEMBERSHIP" +assert_memory 1536 +# A combined v1 controller can live outside /sys/fs/cgroup/memory. +CUSTOM="$FIXTURE/combined" +mkdir -p "$CUSTOM/job" +cp "$ROOT/memory/job/"memory.* "$CUSTOM/job/" +printf '29 23 0:26 / %s rw - cgroup cgroup rw,cpu,memory\n' "$CUSTOM" > "$MOUNTINFO" +assert_memory 1536 +# The mounted controller root may already be a delegated ancestor. +printf '6:memory:/delegated/job\n' > "$MEMBERSHIP" +printf '29 23 0:26 /delegated %s rw - cgroup cgroup rw,memory\n' "$CUSTOM" > "$MOUNTINFO" +assert_memory 1536 +# Mountinfo escapes spaces, including custom controller mount points. +SPACED="$FIXTURE/combined controller" +mkdir -p "$SPACED/job" +cp "$CUSTOM/job/"memory.* "$SPACED/job/" +ESCAPED=${SPACED// /\\040} +printf '29 23 0:26 /delegated %s rw - cgroup cgroup rw,memory\n' "$ESCAPED" > "$MOUNTINFO" +assert_memory 1536 +# Skip unrelated bind mounts before applying namespace fallback. +UNRELATED="$FIXTURE/unrelated" +mkdir -p "$UNRELATED" +printf '0\n' > "$UNRELATED/memory.min" +printf '17179869184\n' > "$UNRELATED/memory.max" +printf '0\n' > "$UNRELATED/memory.current" +printf '0::/team/service\n' > "$MEMBERSHIP" +printf '29 23 0:26 /other %s rw - cgroup2 cgroup rw\n' "$UNRELATED" > "$MOUNTINFO" +printf '30 23 0:26 / %s rw - cgroup2 cgroup rw\n' "$ROOT" >> "$MOUNTINFO" +assert_memory 768 +# A matching delegated view must not hide a tighter ancestor in a wider view. +printf '29 23 0:26 /team/service %s rw - cgroup2 cgroup rw\n' "$ROOT/team/service" > "$MOUNTINFO" +printf '30 23 0:26 / %s rw - cgroup2 cgroup rw\n' "$ROOT" >> "$MOUNTINFO" +printf '536870912\n' > "$ROOT/team/memory.max" +printf '0\n' > "$ROOT/team/memory.current" +assert_memory 512 +printf '2147483648\n' > "$ROOT/team/memory.max" +printf '536870912\n' > "$ROOT/team/memory.current" +# The same selection rule applies to v1 and decoded delegated roots. +printf '6:memory:/delegated group/job\n' > "$MEMBERSHIP" +printf '29 23 0:26 /other %s rw - cgroup cgroup rw,memory\n' "$UNRELATED" > "$MOUNTINFO" +printf '30 23 0:26 /delegated\\040group %s rw - cgroup cgroup rw,memory\n' "$ESCAPED" >> "$MOUNTINFO" +assert_memory 1536 +# Warm inactive file cache is reclaimable; active usage still consumes budget. +printf '0::/\n' > "$MEMBERSHIP" +: > "$MOUNTINFO" +printf '4294967296\n' > "$ROOT/memory.max" +printf '4026531840\n' > "$ROOT/memory.current" +printf 'inactive_file 4026531840\n' > "$ROOT/memory.stat" +assert_memory 4096 +# shellcheck disable=SC2329 +free_memory() { cgroup_free_memory "$ROOT" "$MEMBERSHIP" "$MOUNTINFO"; } +[[ $(calc_xmx 512 32768) == 2048 ]] +# Inconsistent snapshots and malformed statistics receive no cache credit. +printf 'inactive_file 8589934592\n' > "$ROOT/memory.stat" +assert_memory 256 +printf 'inactive_file invalid\n' > "$ROOT/memory.stat" +assert_memory 256 +rm "$ROOT/memory.stat" +assert_memory 256 +# Reclaimable cache is checked independently at every ancestor. +printf '0::/team/service\n' > "$MEMBERSHIP" +printf 'inactive_file 268435456\n' > "$ROOT/team/service/memory.stat" +printf 'inactive_file 536870912\n' > "$ROOT/team/memory.stat" +printf 'inactive_file 4026531840\n' > "$ROOT/memory.stat" +assert_memory 1024 +# v1's total statistic includes descendants; the local statistic does not. +printf '6:memory:/job\n' > "$MEMBERSHIP" +printf 'inactive_file 0\ntotal_inactive_file 536870912\n' > "$ROOT/memory/job/memory.stat" +assert_memory 2048 +# A separate v2 hierarchy preserves ordinary dual/inherited warm limits. +ROOT="$FIXTURE/v2-warm" +mkdir -p "$ROOT/target" "$ROOT/sibling" "$ROOT/notes" +for dir in "$ROOT" "$ROOT/target" "$ROOT/sibling"; do + printf '0\n' > "$dir/memory.min" + printf 'max\n' > "$dir/memory.max" + printf '0\n' > "$dir/memory.current" + printf 'inactive_file 0\n' > "$dir/memory.stat" +done +printf '0::/target\n' > "$MEMBERSHIP" +: > "$MOUNTINFO" +printf '2147483648\n' > "$ROOT/memory.max" +printf '1610612736\n' > "$ROOT/memory.current" +printf 'inactive_file 1610612736\n' > "$ROOT/memory.stat" +printf '2147483648\n' > "$ROOT/target/memory.max" +printf '1610612736\n' > "$ROOT/target/memory.current" +printf 'inactive_file 1610612736\n' > "$ROOT/target/memory.stat" +# A directory without controller files is not a cgroup child. +printf '1610612736\n' > "$ROOT/notes/memory.min" +assert_memory 2048 +printf 'max\n' > "$ROOT/target/memory.max" +assert_memory 2048 +# Protection is relative to the reclaim target: its own min is ignored. +printf 'max\n' > "$ROOT/memory.max" +printf '2147483648\n' > "$ROOT/target/memory.max" +printf '1610612736\n' > "$ROOT/target/memory.min" +assert_memory 2048 +printf '0\n' > "$ROOT/target/memory.min" +printf '2147483648\n' > "$ROOT/memory.max" +# Move the warm cache to a protected sibling; the ancestor must not credit it. +printf '0\n' > "$ROOT/target/memory.current" +printf 'inactive_file 0\n' > "$ROOT/target/memory.stat" +printf '1610612736\n' > "$ROOT/sibling/memory.current" +printf 'inactive_file 1610612736\n' > "$ROOT/sibling/memory.stat" +printf '1610612736\n' > "$ROOT/sibling/memory.min" +assert_memory 512 +printf 'invalid\n' > "$ROOT/sibling/memory.min" +assert_memory 512 +rm "$ROOT/sibling/memory.min" +assert_memory 512 +printf '0\n' > "$ROOT/sibling/memory.min" +chmod 000 "$ROOT/sibling/memory.min" +if [[ ! -r "$ROOT/sibling/memory.min" ]]; then + assert_memory 512 +fi +chmod 600 "$ROOT/sibling/memory.min" +assert_memory 2048 +# Hidden child cgroups (as used by systemd) have the same protection semantics. +mv "$ROOT/sibling" "$ROOT/.sibling" +printf '1610612736\n' > "$ROOT/.sibling/memory.min" +assert_memory 512 +# Actual filesystem permissions must not disguise a cgroup as an ordinary dir. +printf '0\n' > "$ROOT/.sibling/memory.min" +chmod 000 "$ROOT/.sibling" +if [[ ! -x "$ROOT/.sibling" ]]; then + assert_memory 512 +else + # Root with DAC override can still read the actual zero protection value. + assert_memory 2048 +fi +chmod 700 "$ROOT/.sibling" +# Search permission alone permits known files but cannot enumerate children. +chmod 111 "$ROOT" +if [[ ! -r "$ROOT" ]]; then + assert_memory 512 +else + # Root with DAC override can enumerate the unprotected children. + assert_memory 2048 +fi +chmod 700 "$ROOT" +# shellcheck disable=SC2329 +free_memory() { echo 3072; } +[[ $(calc_xmx 512 32768) == 1536 ]] +# shellcheck disable=SC2329 +free_memory() { echo 256; } +if (calc_xmx 512 32768); then + echo 'heap selection accepted memory below its minimum' >&2 + exit 1 +fi +free_memory() { echo 100000; } +[[ $(calc_xmx 512 32768) == 32768 ]] +printf 'cgroup memory sizing checks passed\n' diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh index 133026e594..dffc44324e 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh @@ -370,12 +370,12 @@ def goals(plugin): def check_module(module, test_module): parent = ET.parse(ROOT / module / "pom.xml").getroot() parent_plugin = jacoco_plugin(parent.find(NS + "build")) - assert child_text(parent_plugin, "version") == "0.8.8" + assert child_text(parent_plugin, "version") == "", "inherit the managed JaCoCo version" assert child_text(parent_plugin.find(NS + "configuration"), "append") == "true" test = ET.parse(ROOT / module / test_module / "pom.xml").getroot() default_plugin = jacoco_plugin(test.find(NS + "build")) - assert child_text(default_plugin, "version") == "0.8.8" + assert child_text(default_plugin, "version") == "", "inherit the managed JaCoCo version" assert "report-aggregate" not in goals(default_plugin) profile = None @@ -385,7 +385,7 @@ def check_module(module, test_module): break assert profile is not None profile_plugin = jacoco_plugin(profile.find(NS + "build")) - assert child_text(profile_plugin, "version") == "0.8.8" + assert child_text(profile_plugin, "version") == "", "inherit the managed JaCoCo version" executions = profile_plugin.findall(".//" + NS + "execution") aggregates = [execution for execution in executions if "report-aggregate" in goals(execution)] @@ -393,6 +393,11 @@ def check_module(module, test_module): assert child_text(aggregates[0], "phase") == "verify" +root_pom = ET.parse(ROOT / "pom.xml").getroot() +managed_plugin = jacoco_plugin(root_pom.find(NS + "build/" + NS + "pluginManagement")) +assert child_text(managed_plugin, "version") == "${jacoco.maven.plugin.version}" +assert child_text(root_pom.find(NS + "properties"), "jacoco.maven.plugin.version") == "0.8.15" + check_module("hugegraph-pd", "hg-pd-test") check_module("hugegraph-store", "hg-store-test") @@ -425,8 +430,8 @@ def validation_command(job): def reports_for_option(job, option): pattern = re.escape(option) + ( - r'\s+\\?\s*"\$TEST_REPORT_DIR/' - r'(TEST-[A-Za-z0-9_.]+SuiteTest[.]xml)"' + r'\s+\\?\s*"[^"]*/' + r'(TEST-[A-Za-z0-9_.]+Test[.]xml)"' ) return set(re.findall(pattern, validation_command(job))) @@ -483,13 +488,17 @@ assert required_modules(pd_job) == { assert_order(store_job, [ "mvn clean package", "mvn editorconfig:check -pl hugegraph-store/hg-store-test -am -ntp", + "-Dtest=StoredRowIngressTest,ArthasSecurityTest", "-P store-common-test -Djacoco.sessionId=store-common-test", "-P store-client-test -Djacoco.sessionId=store-client-test", "-P store-rocksdb-test -Djacoco.sessionId=store-rocksdb-test", "-P store-raftcore-test -Djacoco.sessionId=store-raftcore-test", + "-P store-core-test -Djacoco.sessionId=store-core-test", + "-P store-server-test -Djacoco.sessionId=store-server-test", "mvn verify", "--require-session store-common-test", "--require-session store-client-test", "--require-session store-rocksdb-test", - "--require-session store-raftcore-test", "codecov/codecov-action", + "--require-session store-raftcore-test", "--require-session store-core-test", + "--require-session store-server-test", "codecov/codecov-action", ]) assert store_job.count("mvn clean") == 1 assert "hugegraph-store/hg-store-test/target/site/jacoco/jacoco.xml" in store_job @@ -499,7 +508,7 @@ assert "mvn verify -pl hugegraph-store/hg-store-test -am -P jacoco \\ " \ "-DskipTests -Deditorconfig.skip=true -ntp" in " ".join(store_job.split()) assert selected_profiles(store_job, "store") == { "store-common-test", "store-client-test", "store-rocksdb-test", - "store-raftcore-test", "store-core-test", + "store-raftcore-test", "store-core-test", "store-server-test", } assert reports_for_option(store_job, "--require-test-report") == { "TEST-org.apache.hugegraph.store.common.CommonSuiteTest.xml", @@ -507,8 +516,12 @@ assert reports_for_option(store_job, "--require-test-report") == { "TEST-org.apache.hugegraph.store.rocksdb.RocksDbSuiteTest.xml", "TEST-org.apache.hugegraph.store.raftcore.RaftSuiteTest.xml", "TEST-org.apache.hugegraph.store.core.CoreSuiteTest.xml", + "TEST-org.apache.hugegraph.store.service.ServerSuiteTest.xml", + "TEST-org.apache.hugegraph.store.business.StoredRowIngressTest.xml", + "TEST-org.apache.hugegraph.store.node.controller.ArthasSecurityTest.xml", } assert not reports_for_option(store_job, "--require-suite-report") +assert values_for_option(store_job, "--require-session") == (selected_profiles(store_job, "store") | {"store-node-test"}) assert values_for_option(store_job, "--require-covered-group") == { "hg-store-common", "hg-store-client", "hg-store-rocksdb", "hg-store-core", } diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-java-runtime-logs.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-java-runtime-logs.sh new file mode 100755 index 0000000000..7fb8efa0e1 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-java-runtime-logs.sh @@ -0,0 +1,91 @@ +#!/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. +# +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +CHECKER="$SCRIPT_DIR/check-java-runtime-logs.sh" +WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/java-runtime-logs.XXXXXX") +trap 'rm -rf "$WORK_DIR"' EXIT + +check_case() { + local expected="$1" status=0 + shift + "$CHECKER" "$@" >"$WORK_DIR/result" 2>&1 || status=$? + if [[ $status -ne $expected ]]; then + cat "$WORK_DIR/result" >&2 + echo "FAIL: expected $expected, got $status for $*" >&2 + exit 1 + fi +} + +printf '%s\n' '[ERROR] Expected validation error: invalid input' \ + 'java.lang.IllegalAccessException: expected business access failure' \ + 'InaccessibleObjectException is a class name in this message' >"$WORK_DIR/normal.log" +check_case 0 "$WORK_DIR/normal.log" +touch "$WORK_DIR/empty.log" +check_case 0 "$WORK_DIR/empty.log" + +for MESSAGE in \ + 'java.lang.reflect.InaccessibleObjectException: module java.base does not "opens java.lang"' \ + 'java.lang.IllegalAccessError: class A cannot access class B because module java.base does not export p' \ + 'java.lang.IllegalAccessException: module java.base does not export p to unnamed module' \ + "Unrecognized VM option 'OldGCOption'" \ + 'Unrecognized option: --unknown-option' \ + 'OpenJDK warning: Ignoring option --illegal-access=permit; support was removed in 17.0' \ + 'Unable to inspect cluster runtime logs: Node still running or startup log unavailable'; do + # A child returning success after logging only to stderr reproduces the + # failure mode that result assertions and exit-code checks cannot catch. + bash -c 'printf "%s\n" "$1" >&2' bash "$MESSAGE" >"$WORK_DIR/positive.log" 2>&1 + check_case 1 "$WORK_DIR/positive.log" + grep -q '^1:' "$WORK_DIR/result" +done + +printf '%s\n' 'java.lang.NoClassDefFoundError: intentionally missing test dependency' >"$WORK_DIR/native.log" +check_case 0 "$WORK_DIR/native.log" +check_case 1 --native "$WORK_DIR/native.log" +printf '%s\n' 'undefined symbol: rocksdb_open' >"$WORK_DIR/native.log" +check_case 1 --native "$WORK_DIR/native.log" + +mkdir -p "$WORK_DIR/logs with spaces/nested" "$WORK_DIR/no-logs" +cp "$WORK_DIR/normal.log" "$WORK_DIR/logs with spaces/application.log" +printf '%s\n' 'java.lang.reflect.InaccessibleObjectException: module java.base does not opens p' \ + >"$WORK_DIR/logs with spaces/nested/application.log.1" +check_case 1 "$WORK_DIR/logs with spaces" +rm "$WORK_DIR/logs with spaces/nested/application.log.1" +for SUFFIX in hprof log.gz log.zip log.bz2 log.xz; do + cp "$WORK_DIR/positive.log" "$WORK_DIR/logs with spaces/heap.$SUFFIX" +done +check_case 0 "$WORK_DIR/logs with spaces" +# NUL bytes from logged keys must not hide a denied-access line elsewhere in +# the same text log. Keep the NUL-free control and explicit/directory inputs. +printf '%s\n' 'java.lang.reflect.InaccessibleObjectException: module java.base does not opens p' \ + >"$WORK_DIR/logs with spaces/binary.log" +check_case 1 "$WORK_DIR/logs with spaces/binary.log" +printf 'INFO key=\0\0\1 written\n' >>"$WORK_DIR/logs with spaces/binary.log" +check_case 1 "$WORK_DIR/logs with spaces/binary.log" +grep -q '^1:' "$WORK_DIR/result" +check_case 1 "$WORK_DIR/logs with spaces" +printf '\0java.lang.reflect.InaccessibleObjectException:\0' >"$WORK_DIR/logs with spaces/binary.log" +check_case 1 "$WORK_DIR/logs with spaces" +printf 'INFO key=\0\0\1 written\n' >"$WORK_DIR/logs with spaces/binary.log" +check_case 0 "$WORK_DIR/logs with spaces/binary.log" +check_case 0 "$WORK_DIR/logs with spaces" +check_case 2 "$WORK_DIR/no-logs" +check_case 2 "$WORK_DIR/missing.log" +check_case 2 +echo 'PASS: Java runtime log checks reject compatibility errors without rejecting business failures' diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-port.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-port.sh index e8950355c7..8fd1ac5d4d 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-port.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-port.sh @@ -607,9 +607,11 @@ if [[ -n "$STORE_UTIL_SH" ]]; then } curl() { - [[ "$1" == "-fL" && "$2" == "-o" && "$4" == "--" ]] || return 64 + [[ "$1" == "-fL" && "$2" == "--connect-timeout" && "$3" == "5" && + "$4" == "--max-time" && "$5" == "30" && "$6" == "-o" && + "$8" == "--" ]] || return 64 if [[ "$CALLER" == "installer" ]]; then - echo "valid" > "$3" + echo "valid" > "$7" return 0 fi touch "$STALE_DOWNLOAD" @@ -660,6 +662,46 @@ if [[ -n "$STORE_UTIL_SH" ]]; then LEFTOVERS=$(find "$WORK" -name 'lib.so.*' -type f | wc -l | tr -d ' ') expect "concurrent replacement cleans private temp files" "0" "$LEFTOVERS" ) + + echo "" + echo "10. Store optional download timeout preserves the cache and cleans partial bytes" + ( + source "$STORE_UTIL_SH" + WORK=$(mktemp -d) + DEST="$WORK/lib.so" + CURL_CALLED="$WORK/curl-called" + trap 'rm -rf "$WORK"' EXIT + echo "cached" > "$DEST" + + md5sum() { echo "good $1"; } + curl() { touch "$CURL_CALLED"; return 28; } + if download_and_verify "https://example.com/lib.so" "$DEST" "good" >/dev/null 2>&1 && + [[ ! -e "$CURL_CALLED" ]]; then + pass "verified jemalloc cache requires no network" + else + fail "verified jemalloc cache requires no network" "cache was rejected or curl was called" + fi + + md5sum() { echo "bad $1"; } + curl() { + [[ "$1" == "-fL" && "$2" == "--connect-timeout" && "$3" == "5" && + "$4" == "--max-time" && "$5" == "30" && "$6" == "-o" && + "$8" == "--" ]] || return 64 + touch "$CURL_CALLED" + echo "partial" > "$7" + return 28 + } + if download_and_verify "https://example.com/lib.so" "$DEST" "good" >/dev/null 2>&1; then + fail "bounded jemalloc timeout is reported" "download incorrectly succeeded" + elif [[ -e "$CURL_CALLED" ]]; then + pass "bounded jemalloc timeout is reported" + else + fail "bounded jemalloc timeout is reported" "curl did not receive the required timeouts" + fi + expect "timed out replacement preserves the old destination" "cached" "$(cat "$DEST")" + LEFTOVERS=$(find "$WORK" -name 'lib.so.*' -type f | wc -l | tr -d ' ') + expect "jemalloc timeout discards partial bytes" "0" "$LEFTOVERS" + ) fi # -------------------------------------------------------------------------- diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-ci-service-utils.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-ci-service-utils.sh new file mode 100644 index 0000000000..a776591a1e --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-ci-service-utils.sh @@ -0,0 +1,69 @@ +#!/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. +# + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +UTILS="${1:-${SCRIPT_DIR}/ci-service-utils.sh}" +ACTIVE_PID="" + +cleanup() { + if [[ -n "${ACTIVE_PID}" ]]; then + kill "${ACTIVE_PID}" 2>/dev/null || true + wait "${ACTIVE_PID}" 2>/dev/null || true + fi +} +trap cleanup EXIT + +source "${UTILS}" + +if ! declare -F process_is_running >/dev/null || + ! declare -F wait_for_process_exit >/dev/null; then + echo "FAIL: process exit helpers are not available" + exit 1 +fi + +sleep 10 & +ACTIVE_PID=$! +if wait_for_process_exit "${ACTIVE_PID}" 1; then + echo "FAIL: a running process was reported as exited" + exit 1 +fi +kill "${ACTIVE_PID}" 2>/dev/null || true +wait "${ACTIVE_PID}" 2>/dev/null || true +ACTIVE_PID="" + +sleep 1 & +ACTIVE_PID=$! +if ! wait_for_process_exit "${ACTIVE_PID}" 5; then + echo "FAIL: a terminated process was reported as running" + exit 1 +fi +wait "${ACTIVE_PID}" 2>/dev/null || true +ACTIVE_PID="" + +ps() { + echo "Z" +} +if process_is_running "$$"; then + echo "FAIL: a zombie process was reported as running" + exit 1 +fi +unset -f ps + +echo "PASS: process exit helpers handle running, terminated, and zombie states" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-codecov-upload-config.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-codecov-upload-config.sh index 505b0cb36a..0a5137ad6f 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-codecov-upload-config.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-codecov-upload-config.sh @@ -37,10 +37,12 @@ expected_files = { "store": "${{ env.REPORT_FILE }}", "hstore": "${{ env.REPORT_DIR }}/*.xml", }, - ".github/workflows/server-ci.yml": { - "build-server": "${{ env.REPORT_DIR }}/*.xml", + ".github/workflows/server-tests.yml": { + "build-server": "${{ env.REPORT_DIR }}/*.xml,hugegraph-server/target/site/jacoco/jacoco.xml", }, } +expected_flags = {"build-commons": "commons", "pd": "pd", "store": "store", "hstore": "hstore", + "build-server": "server_${{ inputs.backend }}"} action_pattern = re.compile( r"^(?P\s*)(?P-\s+)?uses:\s*" r"(?:(?P['\"])codecov/codecov-action@" @@ -176,7 +178,7 @@ for workflow_path in workflow_paths: if not files_match(uploads, expected_workflow_files): errors.append(f"{relative_path}: unexpected Codecov files inputs") - for line_number, _, version, inputs in uploads: + for line_number, job, version, inputs in uploads: version_match = version_pattern.match(version) if version_match is None or int(version_match.group("major")) < 5: errors.append( @@ -189,6 +191,8 @@ for workflow_path in workflow_paths: f"{relative_path}:{line_number}: Codecov upload must pass " "secrets.CODECOV_TOKEN for trusted runs" ) + if inputs.get("flags") != expected_flags.get(job): + errors.append(f"{relative_path}:{line_number}: unexpected Codecov flag for {job}") if inputs.get("disable_search") != "true": errors.append( f"{relative_path}:{line_number}: Codecov upload must set " diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java-security-properties.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java-security-properties.sh index 796d69c83b..b5d74bc928 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java-security-properties.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java-security-properties.sh @@ -17,11 +17,22 @@ set -euo pipefail -SERVER_ROOT_INPUT="${1:?Usage: $0 PATH_TO_SERVER_DIST}" -SERVER_ROOT=$(cd "$SERVER_ROOT_INPUT" && pwd) +SERVER_ROOT_INPUT="${1:?Usage: $0 PATH_TO_SERVER_DIST [SOURCE_ROOT]}" +SOURCE_ROOT_INPUT="${2:-}" +# Resolve symlinks: the launcher resolves its own location the same way, and the +# paths it passes to the JVM are compared against this one. +SERVER_ROOT=$(cd "$SERVER_ROOT_INPUT" && pwd -P) SERVER_SCRIPT="${SERVER_ROOT}/bin/hugegraph-server.sh" CONF="${SERVER_ROOT}/conf" SECURITY_PROPERTIES="${CONF}/java-security.properties" +JVM_MODULE_OPTIONS="${SERVER_ROOT}/bin/jvm-module.options" +# The test sets every JVM option source itself; values inherited from the runner +# would change what a JVM settles on. +unset JAVA_TOOL_OPTIONS JDK_JAVA_OPTIONS _JAVA_OPTIONS +# Launcher runs create heap dump directories named after the host. A host name +# unique to this run lets the cleanup tell them from a real Server's directories. +TEST_HOST="hgtest-$$" +export HOSTNAME="$TEST_HOST" fail() { echo "FAIL: $1" >&2 @@ -35,6 +46,13 @@ assert_argument() { fail "missing JVM argument: $argument" } +assert_argument_matching() { + local pattern="$1" + local capture="$2" + grep -Eq -- "$pattern" "$capture" || \ + fail "missing JVM argument matching: $pattern" +} + assert_no_argument() { local pattern="$1" local capture="$2" @@ -43,12 +61,138 @@ assert_no_argument() { fi } +assert_source_consumer() { + local source_file="$1" + local expected="$2" + [[ -f "$source_file" ]] || fail "source consumer is missing: $source_file" + grep -Fq -- "$expected" "$source_file" || + fail "JVM module options consumer is not wired: $source_file" +} + +assert_surefire_arg_lines() { + local pom="$1" + local expected="$2" + local total + local wired + local jacoco_wired + read -r total wired jacoco_wired < <( + awk -v expected="$expected" ' + /maven-surefire-plugin<\/artifactId>/ { + in_surefire = 1 + } + in_surefire && /]*)?>/ { + in_arg_line = 1 + arg_line = "" + } + in_arg_line { + arg_line = arg_line $0 + } + in_arg_line && /<\/argLine>/ { + total++ + if (index(arg_line, expected) != 0) { + wired++ + } + if (index(arg_line, "@{argLine}") != 0) { + jacoco_wired++ + } + in_arg_line = 0 + } + in_surefire && /<\/plugin>/ { + in_surefire = 0 + } + END { + print total + 0, wired + 0, jacoco_wired + 0 + } + ' "$pom" + ) + if [[ "$total" -eq 0 || "$wired" -ne "$total" ]]; then + fail "all Surefire argLine values must use jvm-module.options: $pom" + fi + if [[ "$jacoco_wired" -ne "$total" ]]; then + fail "all Surefire argLine values must preserve @{argLine}: $pom" + fi +} + +assert_no_inline_module_options() { + local pattern + local source_file + pattern="--add-(exports|opens)([[:space:]]+|=)[\"']?java\\.base/|" + pattern="${pattern}--add-modules([[:space:]]+|=)[\"']?jdk\.unsupported" + for source_file in "$@"; do + [[ -f "$source_file" ]] || fail "source consumer is missing: $source_file" + done + if grep -En -- "$pattern" "$@"; then + fail "JVM module options must only be declared in jvm-module.options" + fi +} + if [[ ! -x "$SERVER_SCRIPT" ]]; then fail "server script is not executable: $SERVER_SCRIPT" fi if [[ ! -f "$SECURITY_PROPERTIES" ]]; then fail "security properties file is missing: $SECURITY_PROPERTIES" fi +if [[ ! -f "$JVM_MODULE_OPTIONS" ]]; then + fail "JVM module options file is missing: $JVM_MODULE_OPTIONS" +fi + +assert_argument "--add-exports=java.base/jdk.internal.reflect=ALL-UNNAMED" \ + "$JVM_MODULE_OPTIONS" +assert_argument "--add-modules=jdk.unsupported" "$JVM_MODULE_OPTIONS" +assert_argument "--add-exports=java.base/sun.nio.ch=ALL-UNNAMED" \ + "$JVM_MODULE_OPTIONS" +assert_argument "--add-opens=java.base/java.nio=ALL-UNNAMED" "$JVM_MODULE_OPTIONS" +assert_argument "--add-opens=java.base/java.lang=ALL-UNNAMED" "$JVM_MODULE_OPTIONS" + +if [[ -n "$SOURCE_ROOT_INPUT" ]]; then + if [[ ! -d "$SOURCE_ROOT_INPUT" ]]; then + fail "source root is not a directory: $SOURCE_ROOT_INPUT" + fi + SOURCE_ROOT=$(cd "$SOURCE_ROOT_INPUT" && pwd) + SERVER_DIST_SOURCE="${SOURCE_ROOT}/hugegraph-server/hugegraph-dist" + CLUSTER_SOURCE="${SOURCE_ROOT}/hugegraph-cluster-test/"\ +"hugegraph-clustertest-minicluster/src/main/java/org/apache/hugegraph/ct" + SERVER_LAUNCHER_SOURCE="${SERVER_DIST_SOURCE}/src/assembly/static/bin/"\ +"hugegraph-server.sh" + INIT_STORE_SOURCE="${SERVER_DIST_SOURCE}/src/assembly/static/bin/init-store.sh" + SUREFIRE_POM="${SOURCE_ROOT}/hugegraph-server/hugegraph-test/pom.xml" + TEST_JVM_MODULE_OPTIONS="${SOURCE_ROOT}/hugegraph-server/hugegraph-test/"\ +"conf/jvm-test-module.options" + COMMONS_POM="${SOURCE_ROOT}/hugegraph-commons/pom.xml" + CLUSTER_WRAPPER="${CLUSTER_SOURCE}/node/ServerNodeWrapper.java" + SERVER_DOCKERFILE="${SOURCE_ROOT}/hugegraph-server/Dockerfile" + HSTORE_DOCKERFILE="${SOURCE_ROOT}/hugegraph-server/Dockerfile-hstore" + SERVER_WORKFLOW="${SOURCE_ROOT}/.github/workflows/server-tests.yml" + DOCKER_WORKFLOW="${SOURCE_ROOT}/.github/workflows/docker-build-ci.yml" + UPGRADE_CONTRACT_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/"\ +"test-java17-upgrade-contracts.sh" + + [[ -x "$UPGRADE_CONTRACT_SCRIPT" ]] || \ + fail "Java 17 upgrade contract script is missing: $UPGRADE_CONTRACT_SCRIPT" + "$UPGRADE_CONTRACT_SCRIPT" "$SERVER_ROOT" "$SOURCE_ROOT" + + assert_source_consumer "$SERVER_LAUNCHER_SOURCE" '@"${JVM_MODULE_OPTIONS}"' + assert_source_consumer "$INIT_STORE_SOURCE" '@"${JVM_MODULE_OPTIONS}"' + assert_surefire_arg_lines "$SUREFIRE_POM" \ + '@${project.basedir}/../hugegraph-dist/src/assembly/static/bin/jvm-module.options' + [[ -f "$TEST_JVM_MODULE_OPTIONS" ]] || \ + fail "JVM test module options file is missing: $TEST_JVM_MODULE_OPTIONS" + assert_argument \ + "--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED" \ + "$TEST_JVM_MODULE_OPTIONS" + assert_argument "--add-opens=java.base/java.lang=ALL-UNNAMED" \ + "$TEST_JVM_MODULE_OPTIONS" + assert_surefire_arg_lines "$SUREFIRE_POM" \ + '@${project.basedir}/conf/jvm-test-module.options' + assert_surefire_arg_lines "$COMMONS_POM" \ + '@${project.parent.basedir}/../hugegraph-server/hugegraph-test/conf/jvm-test-module.options' + assert_source_consumer "$CLUSTER_WRAPPER" \ + 'moduleOptions(Paths.get(SERVER_PACKAGE_PATH, BIN_DIR,' + assert_no_inline_module_options \ + "$SERVER_LAUNCHER_SOURCE" "$INIT_STORE_SOURCE" "$SUREFIRE_POM" \ + "$COMMONS_POM" "$CLUSTER_WRAPPER" "$SERVER_DOCKERFILE" \ + "$HSTORE_DOCKERFILE" "$SERVER_WORKFLOW" "$DOCKER_WORKFLOW" +fi if [[ -n "${JAVA_HOME:-}" ]]; then JAVA_BIN="${JAVA_HOME}/bin/java" @@ -73,6 +217,8 @@ if [[ "$JAVA_MAJOR" -ge 18 ]]; then fi TEMP_DIR=$(mktemp -d) +CRASH_NAME_FIXTURES=() +OOM_DUMP_DIR="" SECURITY_PROPERTIES_BACKUP="${TEMP_DIR}/java-security.properties" cleanup() { @@ -83,6 +229,24 @@ cleanup() { ! -e "$SECURITY_PROPERTIES" ]]; then mv "$SECURITY_PROPERTIES_BACKUP" "$SECURITY_PROPERTIES" fi + # Fixtures the test adds to the distribution; a failed step must not leave + # them behind. + if [[ ${#CRASH_NAME_FIXTURES[@]} -gt 0 ]]; then + rm -rf "${CRASH_NAME_FIXTURES[@]}" + fi + if [[ -n "${OOM_DUMP_DIR:-}" ]]; then + rm -f "$OOM_DUMP_DIR"/java_pid*.hprof + fi + # Each launcher run creates a heap dump directory. Every host name this run + # uses is TEST_HOST or starts with "${TEST_HOST}-", so these globs match only + # this run's directories; remove the empty ones. + local dump_dir + for dump_dir in "${SERVER_ROOT}"/logs/heapdump_"${TEST_HOST}"_*/ \ + "${SERVER_ROOT}"/logs/heapdump_"${TEST_HOST}"-*_*/; do + if [[ -d "$dump_dir" && ! -L "${dump_dir%/}" ]]; then + rmdir "$dump_dir" 2>/dev/null || true + fi + done rm -rf "$TEMP_DIR" } @@ -136,7 +300,10 @@ assert_reached_server_startup() { assert_clean_bootstrap_error() { local error_file="$1" - if grep -Eq 'Log4j|NetUtils|UnknownHost|hostname' "$error_file"; then + # Every launcher run now makes the JVM print "Picked up JAVA_TOOL_OPTIONS:", + # whose paths may contain any host or directory name. + if awk '!/^Picked up / && /Log4j|NetUtils|UnknownHost|hostname/ { found = 1 } + END { exit !found }' "$error_file"; then fail "bootstrap initialized logging or hostname resolution" fi } @@ -409,10 +576,13 @@ if [[ " $* " == *" -version "* ]]; then if [[ -n "${MOCK_JAVA_PREAMBLE:-}" ]]; then echo "${MOCK_JAVA_PREAMBLE}" >&2 fi - echo "openjdk version \"${MOCK_JAVA_VERSION:-11}.0.0\"" >&2 + echo "openjdk version \"${MOCK_JAVA_VERSION:-17}.0.0\"" >&2 exit 0 fi printf '%s\n' "$@" > "$CAPTURE_FILE" +printf '%s\n' "${JAVA_TOOL_OPTIONS:-}" > "${CAPTURE_FILE}.tool-options" +printf '%s\n' "${JDK_JAVA_OPTIONS:-}" > "${CAPTURE_FILE}.jdk-java-options" +printf '%s\n' "${_JAVA_OPTIONS:-}" > "${CAPTURE_FILE}.underscore-java-options" MOCK chmod +x "${MOCK_JAVA_HOME}/bin/java" @@ -425,6 +595,7 @@ CAPTURE_FILE="$ENABLED_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ assert_argument \ "-Djava.security.properties=${SECURITY_PROPERTIES}" "$ENABLED_CAPTURE" +assert_argument "@${JVM_MODULE_OPTIONS}" "$ENABLED_CAPTURE" assert_no_argument '^-Djava\.security\.manager=' "$ENABLED_CAPTURE" assert_argument \ "org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap" "$ENABLED_CAPTURE" @@ -432,6 +603,384 @@ assert_argument "true" "$ENABLED_CAPTURE" assert_argument "-Doperator.marker=preserved" "$ENABLED_CAPTURE" assert_no_argument '^-D(networkaddress\.cache\.ttl|sun\.net\.inetaddr\.ttl)=' \ "$ENABLED_CAPTURE" +# Heap dump and crash log defaults go first in JAVA_TOOL_OPTIONS, so the JVM's +# own precedence lets every operator source override them. Check the values a +# real JVM settles on when started the way a captured launcher run would start +# it: the JAVA_TOOL_OPTIONS, JDK_JAVA_OPTIONS and _JAVA_OPTIONS the launcher +# passed on, and the crash-related -XX: arguments of its command line. +effective_flag() { + local capture="$1" + local flag="$2" + local args=() + local line + local flags + while IFS= read -r line; do + if [[ "$line" =~ ^-XX:([+-]HeapDumpOnOutOfMemoryError|HeapDumpPath=|ErrorFile=) ]]; then + args+=("$line") + fi + done < "$capture" + if ! flags=$(JAVA_TOOL_OPTIONS="$(cat "${capture}.tool-options")" \ + JDK_JAVA_OPTIONS="$(cat "${capture}.jdk-java-options")" \ + _JAVA_OPTIONS="$(cat "${capture}.underscore-java-options")" \ + "$JAVA_BIN" ${args[@]+"${args[@]}"} -XX:+PrintFlagsFinal -version \ + 2>"${capture}.flags.err"); then + fail "JVM rejected the options captured in ${capture}: $(cat "${capture}.flags.err")" + fi + # The value sits between "= " and the trailing "{origin}" columns and may + # contain spaces. + # A here-string, not a pipe: awk exits early, which would kill printf with + # SIGPIPE under pipefail. + awk -v flag="$flag" '$2 == flag { + sub(/^[^=]*= /, ""); sub(/ *(\{[^}]*\} *)+$/, ""); print; exit }' <<< "$flags" +} + +assert_effective_flag() { + local capture="$1" + local flag="$2" + local pattern="$3" + local value + value=$(effective_flag "$capture" "$flag") + [[ "$value" =~ $pattern ]] || + fail "effective ${flag} is '${value}', expected to match ${pattern}" +} + +LOGS_PATTERN=$(printf '%s' "${SERVER_ROOT}/logs" | sed 's/[][\.*^$+?(){}|]/\\&/g') +# The names carry the host name, the launch time and, if needed, a counter, +# since a restarted container often reuses the PID, pods may share one log +# volume, HotSpot truncates an existing crash log (JDK 17+), and it will not +# write a heap dump over an existing file. +# HeapDumpPath names a directory per launch, so each JVM writes its own +# java_pid.hprof there. +NAME_ID_PATTERN="[A-Za-z0-9._-]+_[0-9]{8}-[0-9]{6}(-[0-9]+)?" +HEAP_DUMP_PATTERN="^${LOGS_PATTERN}/heapdump_${NAME_ID_PATTERN}$" +ERROR_FILE_PATTERN="^${LOGS_PATTERN}/hs_err_pid%p_${NAME_ID_PATTERN}\.log$" + +assert_no_argument '^-XX:([+-]HeapDumpOnOutOfMemoryError|HeapDumpPath=|ErrorFile=)' \ + "$ENABLED_CAPTURE" +assert_effective_flag "$ENABLED_CAPTURE" HeapDumpOnOutOfMemoryError '^true$' +assert_effective_flag "$ENABLED_CAPTURE" HeapDumpPath "$HEAP_DUMP_PATTERN" +assert_effective_flag "$ENABLED_CAPTURE" ErrorFile "$ERROR_FILE_PATTERN" +ENABLED_DUMP_DIR=$(effective_flag "$ENABLED_CAPTURE" HeapDumpPath) +[[ -d "$ENABLED_DUMP_DIR" ]] || + fail "launcher did not create the heap dump directory ${ENABLED_DUMP_DIR}" + +# Child JVMs the Server starts inherit JAVA_TOOL_OPTIONS. Two JVMs started with +# the launcher's options, like the Server and a computer job, must each write +# their own heap dump into the launch directory instead of competing for one. +OOM_SOURCE="${TEMP_DIR}/OomCheck.java" +cat > "$OOM_SOURCE" <<'JAVA' +import java.util.ArrayList; +import java.util.List; + +public class OomCheck { + public static void main(String[] args) { + List hold = new ArrayList<>(); + while (true) { + hold.add(new long[1 << 20]); + } + } +} +JAVA +OOM_DUMP_DIR="$ENABLED_DUMP_DIR" +for OOM_RUN in 1 2; do + JAVA_TOOL_OPTIONS="$(cat "${ENABLED_CAPTURE}.tool-options")" \ + "$JAVA_BIN" -Xmx64m "$OOM_SOURCE" >"${TEMP_DIR}/oom-${OOM_RUN}.log" 2>&1 || true +done +OOM_DUMPS=$(find "$OOM_DUMP_DIR" -maxdepth 1 -name 'java_pid*.hprof' | wc -l) +if [[ "$OOM_DUMPS" -ne 2 ]]; then + ls -l "$OOM_DUMP_DIR" >&2 || true + cat "${TEMP_DIR}"/oom-*.log >&2 || true + fail "expected a heap dump per JVM in ${OOM_DUMP_DIR}, found ${OOM_DUMPS}" +fi +rm -f "$OOM_DUMP_DIR"/java_pid*.hprof + +# JAVA_OPTIONS replaces the default heap options; the crash defaults stay, and +# paths given there win. +CUSTOM_OPTIONS_CAPTURE="${TEMP_DIR}/custom-java-options.args" +CAPTURE_FILE="$CUSTOM_OPTIONS_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ + JAVA_OPTIONS="-Xmx1g -XX:ErrorFile=/operator/hs_err.log \ + -XX:HeapDumpPath=/operator/dumps" \ + STDOUT_MODE=true "$SERVER_SCRIPT" \ + "${CONF}/gremlin-server.yaml" "${CONF}/rest-server.properties" true >/dev/null + +assert_argument "-Xmx1g" "$CUSTOM_OPTIONS_CAPTURE" +assert_effective_flag "$CUSTOM_OPTIONS_CAPTURE" HeapDumpOnOutOfMemoryError '^true$' +assert_effective_flag "$CUSTOM_OPTIONS_CAPTURE" HeapDumpPath '^/operator/dumps$' +assert_effective_flag "$CUSTOM_OPTIONS_CAPTURE" ErrorFile '^/operator/hs_err\.log$' + +# The JVM, not the launcher, parses the operator's JAVA_TOOL_OPTIONS: a quoted +# opt-out works, and flag-like text inside a property value changes nothing. +QUOTED_TOOL_CAPTURE="${TEMP_DIR}/quoted-tool-options.args" +CAPTURE_FILE="$QUOTED_TOOL_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ + JAVA_TOOL_OPTIONS='"-XX:-HeapDumpOnOutOfMemoryError" "-Dmarker=-XX:ErrorFile=/nope"' \ + STDOUT_MODE=true "$SERVER_SCRIPT" \ + "${CONF}/gremlin-server.yaml" "${CONF}/rest-server.properties" true >/dev/null + +assert_effective_flag "$QUOTED_TOOL_CAPTURE" HeapDumpOnOutOfMemoryError '^false$' +assert_effective_flag "$QUOTED_TOOL_CAPTURE" ErrorFile "$ERROR_FILE_PATTERN" + +# JDK_JAVA_OPTIONS, including an @argfile, also overrides the defaults. +ARGFILE="${TEMP_DIR}/jdk-java-options.args" +echo '-XX:ErrorFile=/operator/argfile-hs_err.log' > "$ARGFILE" +ARGFILE_CAPTURE="${TEMP_DIR}/argfile.args" +CAPTURE_FILE="$ARGFILE_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ + JDK_JAVA_OPTIONS="@${ARGFILE}" \ + STDOUT_MODE=true "$SERVER_SCRIPT" \ + "${CONF}/gremlin-server.yaml" "${CONF}/rest-server.properties" true >/dev/null + +assert_effective_flag "$ARGFILE_CAPTURE" ErrorFile '^/operator/argfile-hs_err\.log$' +assert_effective_flag "$ARGFILE_CAPTURE" HeapDumpPath "$HEAP_DUMP_PATTERN" + +# _JAVA_OPTIONS comes after the command line, and -j lands on the command line +# when JAVA_OPTIONS is unset; both override the defaults. +UNDERSCORE_CAPTURE="${TEMP_DIR}/underscore-java-options.args" +CAPTURE_FILE="$UNDERSCORE_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ + _JAVA_OPTIONS="-XX:HeapDumpPath=/operator/underscore" \ + STDOUT_MODE=true "$SERVER_SCRIPT" \ + "${CONF}/gremlin-server.yaml" "${CONF}/rest-server.properties" true \ + "-XX:ErrorFile=/operator/user-option-hs_err.log" >/dev/null + +assert_effective_flag "$UNDERSCORE_CAPTURE" HeapDumpPath '^/operator/underscore$' +assert_effective_flag "$UNDERSCORE_CAPTURE" ErrorFile '^/operator/user-option-hs_err\.log$' + +# A launch never reuses a name already taken in logs/, even within the same +# second: with a fixed clock and host name, the counter moves past an existing +# heap dump directory, a crash log and a dangling symlink. The launcher never +# removes dump directories: an empty one from an earlier launch may still be the +# target of a computer-job JVM that outlived its Server. +MOCK_DATE_BIN="${TEMP_DIR}/mock-date-bin" +mkdir -p "$MOCK_DATE_BIN" +printf '#!/bin/bash\necho 20200101-000000\n' > "${MOCK_DATE_BIN}/date" +chmod +x "${MOCK_DATE_BIN}/date" +mkdir -p "${SERVER_ROOT}/logs" +# Only fixtures this run creates are recorded for removal, so an existing file +# with the same name in the supplied distribution is left alone. +add_crash_fixture() { + local path="$1" + local kind="$2" + if [[ ! -e "$path" && ! -L "$path" ]]; then + CRASH_NAME_FIXTURES+=("$path") + case "$kind" in + file) : > "$path" ;; + empty-dir) mkdir "$path" ;; + dump-dir) mkdir "$path" && : > "${path}/java_pid1.hprof" ;; + dangling-link) ln -s "${TEMP_DIR}/missing-target" "$path" ;; + esac + fi +} +USED_DUMP_DIR="${SERVER_ROOT}/logs/heapdump_${TEST_HOST}-a_20200101-000000" +OLD_EMPTY_DUMP_DIR="${SERVER_ROOT}/logs/heapdump_${TEST_HOST}-a_20191231-000000" +OTHER_HOST_DUMP_DIR="${SERVER_ROOT}/logs/heapdump_${TEST_HOST}-c_20191231-000000" +add_crash_fixture "$USED_DUMP_DIR" dump-dir +add_crash_fixture "${SERVER_ROOT}/logs/hs_err_pid1_${TEST_HOST}-a_20200101-000000-1.log" file +add_crash_fixture "${SERVER_ROOT}/logs/heapdump_${TEST_HOST}-a_20200101-000000-2" dangling-link +add_crash_fixture "$OLD_EMPTY_DUMP_DIR" empty-dir +add_crash_fixture "$OTHER_HOST_DUMP_DIR" empty-dir +UNIQUE_NAME_CAPTURE="${TEMP_DIR}/unique-name.args" +CAPTURE_FILE="$UNIQUE_NAME_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ + HOSTNAME="${TEST_HOST}-a" PATH="${MOCK_DATE_BIN}:${PATH}" STDOUT_MODE=true "$SERVER_SCRIPT" \ + "${CONF}/gremlin-server.yaml" "${CONF}/rest-server.properties" true >/dev/null + +assert_effective_flag "$UNIQUE_NAME_CAPTURE" HeapDumpPath \ + "^${LOGS_PATTERN}/heapdump_${TEST_HOST}-a_20200101-000000-3$" +assert_effective_flag "$UNIQUE_NAME_CAPTURE" ErrorFile \ + "^${LOGS_PATTERN}/hs_err_pid%p_${TEST_HOST}-a_20200101-000000-3\.log$" +[[ -f "${SERVER_ROOT}/logs/hs_err_pid1_${TEST_HOST}-a_20200101-000000-1.log" ]] || + fail "launcher removed an existing crash log" +[[ -d "$OLD_EMPTY_DUMP_DIR" ]] || + fail "launcher removed an empty heap dump directory from an earlier launch" +[[ -d "$OTHER_HOST_DUMP_DIR" ]] || + fail "launcher removed another host's heap dump directory" +[[ -f "${USED_DUMP_DIR}/java_pid1.hprof" ]] || + fail "launcher removed an existing heap dump" + +# Another pod sharing the volume gets its own names in the same second, and +# characters unsafe in a file name are replaced. +OTHER_HOST_CAPTURE="${TEMP_DIR}/other-host.args" +CAPTURE_FILE="$OTHER_HOST_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ + HOSTNAME="${TEST_HOST}-b/x" PATH="${MOCK_DATE_BIN}:${PATH}" STDOUT_MODE=true "$SERVER_SCRIPT" \ + "${CONF}/gremlin-server.yaml" "${CONF}/rest-server.properties" true >/dev/null + +assert_effective_flag "$OTHER_HOST_CAPTURE" HeapDumpPath \ + "^${LOGS_PATTERN}/heapdump_${TEST_HOST}-b_x_20200101-000000$" +if [[ ${#CRASH_NAME_FIXTURES[@]} -gt 0 ]]; then + rm -rf "${CRASH_NAME_FIXTURES[@]}" +fi +CRASH_NAME_FIXTURES=() + +# A launcher error before Java starts must reach stderr, which start-hugegraph.sh +# passes to the terminal or container log, as well as the server log, and the +# failed launch must not leave a heap dump directory behind. +: > "$SERVER_LOG" +PREFLIGHT_ERROR="${TEMP_DIR}/preflight.err" +PREFLIGHT_DUMP_DIR="${SERVER_ROOT}/logs/heapdump_${TEST_HOST}-preflight_20200101-000000" +if JAVA_HOME="$MOCK_JAVA_HOME" HOSTNAME="${TEST_HOST}-preflight" PATH="${MOCK_DATE_BIN}:${PATH}" \ + STDOUT_MODE=true "$SERVER_SCRIPT" \ + "${CONF}/gremlin-server.yaml" "${CONF}/rest-server.properties" true "" "bad-gc" \ + >/dev/null 2>"$PREFLIGHT_ERROR"; then + fail "launcher accepted an unknown GC option" +fi +grep -Fq "Unrecognized gc option: 'bad-gc'" "$PREFLIGHT_ERROR" || + fail "launcher preflight error did not reach stderr" +grep -Fq "Unrecognized gc option: 'bad-gc'" "$SERVER_LOG" || + fail "launcher preflight error did not reach the server log" +[[ ! -e "$PREFLIGHT_DUMP_DIR" ]] || + fail "a launch that failed before Java started left ${PREFLIGHT_DUMP_DIR}" + +# The riscv64 libatomic check runs before anything else, and its error must also +# reach the server log. Mock uname and ldconfig so no libatomic is found; skip if +# this machine has one at a fixed path the launcher also probes. +RISCV_LIBATOMIC_FOUND="false" +for RISCV_CANDIDATE in /lib/riscv64-linux-gnu/libatomic.so.1 \ + /usr/lib/riscv64-linux-gnu/libatomic.so.1 \ + /lib64/lp64d/libatomic.so.1 /usr/lib64/lp64d/libatomic.so.1 \ + /lib64/libatomic.so.1 /usr/lib64/libatomic.so.1; do + if [[ -r "$RISCV_CANDIDATE" ]]; then + RISCV_LIBATOMIC_FOUND="true" + fi +done +if [[ "$RISCV_LIBATOMIC_FOUND" == "false" ]]; then + MOCK_RISCV_BIN="${TEMP_DIR}/mock-riscv-bin" + mkdir -p "$MOCK_RISCV_BIN" + printf '#!/bin/bash\ncase "$1" in -s) echo Linux ;; -m) echo riscv64 ;; *) echo Linux ;; esac\n' \ + > "${MOCK_RISCV_BIN}/uname" + printf '#!/bin/bash\nexit 0\n' > "${MOCK_RISCV_BIN}/ldconfig" + chmod +x "${MOCK_RISCV_BIN}/uname" "${MOCK_RISCV_BIN}/ldconfig" + : > "$SERVER_LOG" + RISCV_ERROR="${TEMP_DIR}/riscv.err" + if JAVA_HOME="$MOCK_JAVA_HOME" PATH="${MOCK_RISCV_BIN}:${PATH}" LD_PRELOAD="" \ + STDOUT_MODE=true "$SERVER_SCRIPT" \ + "${CONF}/gremlin-server.yaml" "${CONF}/rest-server.properties" true \ + >/dev/null 2>"$RISCV_ERROR"; then + fail "launcher started on riscv64 without libatomic" + fi + grep -Fq "RISC-V RocksDB requires libatomic.so.1" "$RISCV_ERROR" || + fail "the riscv64 libatomic error did not reach stderr" + grep -Fq "RISC-V RocksDB requires libatomic.so.1" "$SERVER_LOG" || + fail "the riscv64 libatomic error did not reach the server log" +fi + +# A full or read-only logs volume must not stop the Server: when the dump +# directory cannot be created, the launcher warns and dumps into logs/ itself. +FAILING_MKDIR_BIN="${TEMP_DIR}/failing-mkdir-bin" +mkdir -p "$FAILING_MKDIR_BIN" +cat > "${FAILING_MKDIR_BIN}/mkdir" <<'MKDIR' +#!/bin/bash +for arg in "$@"; do + case "$arg" in + */heapdump_*) echo "mkdir: ${arg}: No space left on device" >&2; exit 1 ;; + esac +done +exec /bin/mkdir "$@" +MKDIR +chmod +x "${FAILING_MKDIR_BIN}/mkdir" +: > "$SERVER_LOG" +NO_DUMP_DIR_CAPTURE="${TEMP_DIR}/no-dump-dir.args" +NO_DUMP_DIR_ERROR="${TEMP_DIR}/no-dump-dir.err" +CAPTURE_FILE="$NO_DUMP_DIR_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ + PATH="${FAILING_MKDIR_BIN}:${PATH}" STDOUT_MODE=true "$SERVER_SCRIPT" \ + "${CONF}/gremlin-server.yaml" "${CONF}/rest-server.properties" true \ + >/dev/null 2>"$NO_DUMP_DIR_ERROR" || + fail "launcher did not start when the heap dump directory could not be created" +grep -Fq "WARN: cannot create ${SERVER_ROOT}/logs/heapdump_" "$NO_DUMP_DIR_ERROR" || + fail "launcher did not warn on stderr that the heap dump directory was not created" +grep -Fq "WARN: cannot create ${SERVER_ROOT}/logs/heapdump_" "$SERVER_LOG" || + fail "launcher did not log that the heap dump directory was not created" +assert_effective_flag "$NO_DUMP_DIR_CAPTURE" HeapDumpPath "^${LOGS_PATTERN}$" + +# A logs path with a double quote or % cannot use the per-launch defaults. Heap +# dumps must stay on there, still at the front of JAVA_TOOL_OPTIONS (quoted with +# the quote character the path lacks) so the documented JAVA_TOOL_OPTIONS opt-out +# keeps winning, with only the crash log default left out. A path with both quote +# characters gets the warning only. Each case runs in its own copy of bin/ and conf/. +for ODD_DIR_NAME in 'pct%dir' 'dq"dir' "sq'pct%dir" "both'q\"dir"; do + mkdir -p "${TEMP_DIR}/${ODD_DIR_NAME}/logs" "${TEMP_DIR}/${ODD_DIR_NAME}/plugins" + ODD_ROOT=$(cd "${TEMP_DIR}/${ODD_DIR_NAME}" && pwd -P) + cp -R "${SERVER_ROOT}/bin" "${SERVER_ROOT}/conf" "${ODD_ROOT}/" + for ODD_OPT_OUT in "" "-XX:-HeapDumpOnOutOfMemoryError"; do + ODD_CAPTURE="${TEMP_DIR}/odd-path.args" + ODD_ERROR="${TEMP_DIR}/odd-path.err" + CAPTURE_FILE="$ODD_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" STDOUT_MODE=true \ + JAVA_TOOL_OPTIONS="$ODD_OPT_OUT" "${ODD_ROOT}/bin/hugegraph-server.sh" \ + "${ODD_ROOT}/conf/gremlin-server.yaml" "${ODD_ROOT}/conf/rest-server.properties" true \ + >/dev/null 2>"$ODD_ERROR" || + fail "launcher failed with ${ODD_DIR_NAME} in the logs path: $(cat "$ODD_ERROR")" + assert_no_argument '^-XX:([+-]HeapDumpOnOutOfMemoryError|HeapDumpPath=|ErrorFile=)' \ + "$ODD_CAPTURE" + assert_effective_flag "$ODD_CAPTURE" ErrorFile '^$' + case "$ODD_DIR_NAME" in + both*) + grep -Fq "WARN: ${ODD_ROOT}/logs contains both quote characters" "$ODD_ERROR" || + fail "launcher did not warn about both quote characters in the logs path" + if grep -q -- '-XX:HeapDumpPath' "${ODD_CAPTURE}.tool-options"; then + fail "launcher set a heap dump path it cannot quote for ${ODD_DIR_NAME}" + fi + ;; + *) + grep -Fq "WARN: ${ODD_ROOT}/logs contains a double quote or %" "$ODD_ERROR" || + fail "launcher did not warn about ${ODD_DIR_NAME} in the logs path" + [[ "$(effective_flag "$ODD_CAPTURE" HeapDumpPath)" == "${ODD_ROOT}/logs" ]] || + fail "heap dumps do not go to ${ODD_ROOT}/logs for ${ODD_DIR_NAME}" + if [[ -z "$ODD_OPT_OUT" ]]; then + assert_effective_flag "$ODD_CAPTURE" HeapDumpOnOutOfMemoryError '^true$' + else + assert_effective_flag "$ODD_CAPTURE" HeapDumpOnOutOfMemoryError '^false$' + fi + ;; + esac + done +done + +# An unwritable logs/ is reported on stderr, where the container log keeps it. +# Root can write anywhere, so the check only runs for other users. +if [[ "$(id -u)" -ne 0 ]]; then + mkdir -p "${TEMP_DIR}/unwritable-logs-dist/logs" "${TEMP_DIR}/unwritable-logs-dist/plugins" + # The launcher reports the resolved path (TEMP_DIR is a symlink on macOS). + MINI_ROOT=$(cd "${TEMP_DIR}/unwritable-logs-dist" && pwd -P) + cp -R "${SERVER_ROOT}/bin" "${SERVER_ROOT}/conf" "${MINI_ROOT}/" + chmod 555 "${MINI_ROOT}/logs" + UNWRITABLE_ERROR="${TEMP_DIR}/unwritable-logs.err" + if JAVA_HOME="$MOCK_JAVA_HOME" STDOUT_MODE=true "${MINI_ROOT}/bin/hugegraph-server.sh" \ + "${MINI_ROOT}/conf/gremlin-server.yaml" "${MINI_ROOT}/conf/rest-server.properties" true \ + >/dev/null 2>"$UNWRITABLE_ERROR"; then + chmod 755 "${MINI_ROOT}/logs" + fail "launcher started with an unwritable logs directory" + fi + chmod 755 "${MINI_ROOT}/logs" + grep -Fq "No write permission on directory ${MINI_ROOT}/logs" "$UNWRITABLE_ERROR" || + fail "launcher did not report the unwritable logs directory on stderr" +fi + +# With telemetry on, the launcher appends its agent to JAVA_TOOL_OPTIONS and +# keeps both the defaults and the operator's flags there. The agent jar fixture +# lives in a copy of bin/ and conf/ under TEMP_DIR, so the test never writes into +# the supplied distribution's plugins/ (or through a symlink placed there). +OT_EXPECTED_MD5=$(grep -E '^ *expected_md5=' "$SERVER_SCRIPT" | cut -d'"' -f2) +MOCK_MD5_BIN="${TEMP_DIR}/mock-md5-bin" +mkdir -p "$MOCK_MD5_BIN" +printf '#!/bin/bash\necho "%s $1"\n' "$OT_EXPECTED_MD5" > "${MOCK_MD5_BIN}/md5sum" +chmod +x "${MOCK_MD5_BIN}/md5sum" +TELEMETRY_ROOT="${TEMP_DIR}/telemetry-dist" +mkdir -p "${TELEMETRY_ROOT}/logs" "${TELEMETRY_ROOT}/plugins" +cp -R "${SERVER_ROOT}/bin" "${SERVER_ROOT}/conf" "${TELEMETRY_ROOT}/" +: > "${TELEMETRY_ROOT}/plugins/opentelemetry-javaagent.jar" +TELEMETRY_CAPTURE="${TEMP_DIR}/telemetry.args" +CAPTURE_FILE="$TELEMETRY_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ + PATH="${MOCK_MD5_BIN}:${PATH}" \ + JAVA_TOOL_OPTIONS="-XX:ErrorFile=/operator/hs_err.log" \ + STDOUT_MODE=true "${TELEMETRY_ROOT}/bin/hugegraph-server.sh" \ + "${TELEMETRY_ROOT}/conf/gremlin-server.yaml" \ + "${TELEMETRY_ROOT}/conf/rest-server.properties" true "" "" true \ + >/dev/null 2>"${TEMP_DIR}/telemetry.err" || + fail "launcher failed with telemetry on: $(cat "${TEMP_DIR}/telemetry.err")" + +TELEMETRY_TOOL_OPTIONS=$(cat "${TELEMETRY_CAPTURE}.tool-options") +[[ "$TELEMETRY_TOOL_OPTIONS" == -XX:+HeapDumpOnOutOfMemoryError\ * ]] || + fail "telemetry agent setup dropped the crash-file defaults" +[[ "$TELEMETRY_TOOL_OPTIONS" == *" -XX:ErrorFile=/operator/hs_err.log "* ]] || + fail "telemetry agent setup dropped the operator's JAVA_TOOL_OPTIONS" +[[ "$TELEMETRY_TOOL_OPTIONS" =~ \ -javaagent:[^\ ]*/opentelemetry-javaagent\.jar$ ]] || + fail "telemetry agent was not added to JAVA_TOOL_OPTIONS" JDK21_CAPTURE="${TEMP_DIR}/jdk21.args" CAPTURE_FILE="$JDK21_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ @@ -503,10 +1052,10 @@ assert_argument "-Djava.security.manager=allow" "$AGENT_JDK21_CAPTURE" # ... and trip the JDK 24+ security guard when the agent version is high. HIGH_AGENT_PREAMBLE=$'Picked up JAVA_TOOL_OPTIONS: -javaagent:apm-agent.jar\nAPM agent version "24.0.1" is starting' -HIGH_AGENT_CAPTURE="${TEMP_DIR}/agent-preamble-jdk11.args" -HIGH_AGENT_ERROR="${TEMP_DIR}/agent-preamble-jdk11.err" +HIGH_AGENT_CAPTURE="${TEMP_DIR}/agent-preamble-jdk17.args" +HIGH_AGENT_ERROR="${TEMP_DIR}/agent-preamble-jdk17.err" CAPTURE_FILE="$HIGH_AGENT_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ - MOCK_JAVA_VERSION=11 MOCK_JAVA_PREAMBLE="$HIGH_AGENT_PREAMBLE" \ + MOCK_JAVA_VERSION=17 MOCK_JAVA_PREAMBLE="$HIGH_AGENT_PREAMBLE" \ STDOUT_MODE=true "$SERVER_SCRIPT" \ "${CONF}/gremlin-server.yaml" "${CONF}/rest-server.properties" true \ >/dev/null 2>"$HIGH_AGENT_ERROR" @@ -518,6 +1067,15 @@ assert_argument \ "org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap" "$HIGH_AGENT_CAPTURE" assert_no_argument '^-Djava\.security\.manager=' "$HIGH_AGENT_CAPTURE" +JDK11_ERROR="${TEMP_DIR}/jdk11.err" +if JAVA_HOME="$MOCK_JAVA_HOME" MOCK_JAVA_VERSION=11 STDOUT_MODE=true \ + "$SERVER_SCRIPT" "${CONF}/gremlin-server.yaml" \ + "${CONF}/rest-server.properties" false >/dev/null 2>"$JDK11_ERROR"; then + fail "launcher accepted a Java 11 runtime" +fi +grep -Fq "version >= 17, current is 11" "${SERVER_ROOT}/logs/hugegraph-server.log" || + fail "launcher did not report the Java 17 minimum" + JDK24_DISABLED_CAPTURE="${TEMP_DIR}/jdk24-disabled.args" CAPTURE_FILE="$JDK24_DISABLED_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ MOCK_JAVA_VERSION=24 STDOUT_MODE=true "$SERVER_SCRIPT" \ diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java17-upgrade-contracts.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java17-upgrade-contracts.sh new file mode 100755 index 0000000000..e51d369ce0 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-java17-upgrade-contracts.sh @@ -0,0 +1,262 @@ +#!/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. + +set -euo pipefail + +SERVER_ROOT_INPUT="${1:?Usage: $0 PATH_TO_SERVER_DIST PATH_TO_SOURCE_ROOT}" +SOURCE_ROOT_INPUT="${2:?Usage: $0 PATH_TO_SERVER_DIST PATH_TO_SOURCE_ROOT}" +SERVER_ROOT=$(cd "$SERVER_ROOT_INPUT" && pwd) +SOURCE_ROOT=$(cd "$SOURCE_ROOT_INPUT" && pwd) + +fail() { + echo "FAIL: $1" >&2 + exit 1 +} + +assert_argument() { + local argument="$1" + local capture="$2" + grep -Fxq -- "$argument" "$capture" || \ + fail "missing JVM argument: $argument" +} + +assert_default_test_is_tolerant() { + local pom="$1" + + python3 - "$pom" <<'PY' +import sys +import xml.etree.ElementTree as ET + +pom = sys.argv[1] +namespace = {"m": "http://maven.apache.org/POM/4.0.0"} +root = ET.parse(pom).getroot() +value = root.find( + "m:properties/m:surefire.failIfNoSpecifiedTests", namespace +) +if value is None or (value.text or "").strip() != "false": + raise SystemExit( + "{}: default-test must tolerate -Dtest misses in reactor modules".format(pom) + ) +PY +} + +assert_supported_java_contract() { + local pom="$1" + + python3 - "$pom" <<'PY' +import sys +import xml.etree.ElementTree as ET + +pom = sys.argv[1] +namespace = {"m": "http://maven.apache.org/POM/4.0.0"} +root = ET.parse(pom).getroot() +properties = root.find("m:properties", namespace) +if properties is None: + raise SystemExit("{}: Maven properties are missing".format(pom)) + +release = properties.find("m:maven.compiler.release", namespace) +if release is None or (release.text or "").strip() != "17": + raise SystemExit("{}: compiler release must remain 17".format(pom)) + +supported_range = properties.find("m:java.supported.version.range", namespace) +if supported_range is None or (supported_range.text or "").strip() != "[17,18)": + raise SystemExit("{}: supported JDK range must be [17,18)".format(pom)) + +expected_reference = "${java.supported.version.range}" +actual_references = [] +for plugin in root.findall("m:build/m:plugins/m:plugin", namespace): + artifact_id = plugin.find("m:artifactId", namespace) + if artifact_id is None or artifact_id.text != "maven-enforcer-plugin": + continue + for rule in plugin.findall( + "m:executions/m:execution/m:configuration/m:rules/m:requireJavaVersion", + namespace, + ): + version = rule.find("m:version", namespace) + if version is not None: + actual_references.append((version.text or "").strip()) + +if actual_references != [expected_reference]: + raise SystemExit( + "{}: requireJavaVersion must consume {} exactly once; found {}".format( + pom, expected_reference, actual_references + ) + ) +PY +} + +assert_surefire_execution_scope() { + local pom="$1" + shift + + python3 - "$pom" "$@" <<'PY' +import sys +import xml.etree.ElementTree as ET + +pom = sys.argv[1] +expected_ids = sys.argv[2:] +namespace = {"m": "http://maven.apache.org/POM/4.0.0"} +root = ET.parse(pom).getroot() + +properties = root.find("m:properties", namespace) +if properties is not None: + module_value = properties.find("m:surefire.failIfNoSpecifiedTests", namespace) + if module_value is not None and (module_value.text or "").strip() == "true": + raise SystemExit( + "{}: leaf-wide failIfNoSpecifiedTests=true breaks default-test".format(pom) + ) + +surefire = None +for plugin in root.findall("m:build/m:plugins/m:plugin", namespace): + artifact_id = plugin.find("m:artifactId", namespace) + if artifact_id is not None and artifact_id.text == "maven-surefire-plugin": + surefire = plugin + break + +if surefire is None: + raise SystemExit("{}: maven-surefire-plugin is missing".format(pom)) + +strict_executions = set() +for execution in surefire.findall("m:executions/m:execution", namespace): + execution_id = execution.find("m:id", namespace) + strict = execution.find("m:configuration/m:failIfNoSpecifiedTests", namespace) + if execution_id is None or strict is None: + continue + if (strict.text or "").strip() == "true": + strict_executions.add(execution_id.text) + +missing = sorted(set(expected_ids) - strict_executions) +if missing: + raise SystemExit( + "{}: named Surefire executions are not strict: {}".format( + pom, ", ".join(missing) + ) + ) +PY +} + +assert_supported_java_contract "${SOURCE_ROOT}/pom.xml" +assert_default_test_is_tolerant "${SOURCE_ROOT}/pom.xml" +assert_surefire_execution_scope \ + "${SOURCE_ROOT}/hugegraph-server/hugegraph-test/pom.xml" \ + core-test unit-test api-test tinkerpop-structure-test tinkerpop-process-test +assert_surefire_execution_scope \ + "${SOURCE_ROOT}/hugegraph-pd/hg-pd-test/pom.xml" \ + pd-client-test pd-core-test pd-common-test pd-rest-test +assert_surefire_execution_scope \ + "${SOURCE_ROOT}/hugegraph-store/hg-store-test/pom.xml" \ + store-client-test store-core-test store-common-test store-rocksdb-test \ + store-server-test store-raftcore-test + +MODULE_OPTIONS="${SERVER_ROOT}/bin/jvm-module.options" +SERVER_SCRIPT="${SERVER_ROOT}/bin/hugegraph-server.sh" +INIT_STORE_SCRIPT="${SERVER_ROOT}/bin/init-store.sh" +CONSOLE_SCRIPT="${SERVER_ROOT}/bin/gremlin-console.sh" +DUMP_STORE_SCRIPT="${SERVER_ROOT}/bin/dump-store.sh" +UTIL_SCRIPT="${SERVER_ROOT}/bin/util.sh" +PRELOAD_SCRIPT="${SERVER_ROOT}/bin/preload-topling.sh" +CONF_SOURCE="${SERVER_ROOT}/conf" + +for source_file in "$MODULE_OPTIONS" "$SERVER_SCRIPT" \ + "$INIT_STORE_SCRIPT" "$CONSOLE_SCRIPT" "$DUMP_STORE_SCRIPT" \ + "$UTIL_SCRIPT" "$PRELOAD_SCRIPT"; do + [[ -f "$source_file" ]] || fail "runtime asset is missing: $source_file" +done +[[ -d "$CONF_SOURCE" ]] || fail "server conf is missing: $CONF_SOURCE" + +TEMP_DIR=$(mktemp -d) +TEMP_DIR=$(cd "$TEMP_DIR" && pwd -P) +trap 'rm -rf "$TEMP_DIR"' EXIT + +DIST_ROOT="${TEMP_DIR}/server" +MOCK_JAVA_HOME="${TEMP_DIR}/mock-java-home" +mkdir -p "${DIST_ROOT}"/{bin,conf,ext,lib,logs,plugins} \ + "${MOCK_JAVA_HOME}/bin" +cp "$MODULE_OPTIONS" "$SERVER_SCRIPT" "$INIT_STORE_SCRIPT" "$UTIL_SCRIPT" \ + "$CONSOLE_SCRIPT" "$DUMP_STORE_SCRIPT" "$PRELOAD_SCRIPT" "${DIST_ROOT}/bin/" +cp -R "${CONF_SOURCE}/." "${DIST_ROOT}/conf/" + +# Model a full pre-Phase-2 conf/ directory: it has no module argfile. All +# launchers must get the immutable runtime copy from bin/ instead. +if [[ -e "${DIST_ROOT}/conf/jvm-module.options" ]]; then + fail "legacy conf unexpectedly contains jvm-module.options" +fi + +cat > "${MOCK_JAVA_HOME}/bin/java" <<'MOCK' +#!/bin/bash +for argument in "$@"; do + if [[ "$argument" == "-version" ]]; then + echo 'openjdk version "17.0.0"' >&2 + exit 0 + fi +done +printf '%s\n' "$@" > "${CAPTURE_FILE:?}" +MOCK +chmod +x "${MOCK_JAVA_HOME}/bin/java" "${DIST_ROOT}/bin/hugegraph-server.sh" \ + "${DIST_ROOT}/bin/init-store.sh" "${DIST_ROOT}/bin/gremlin-console.sh" \ + "${DIST_ROOT}/bin/dump-store.sh" + +SERVER_CAPTURE="${TEMP_DIR}/server.args" +CAPTURE_FILE="$SERVER_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ + JAVA_OPTIONS="-Xms512m -Xmx512m" STDOUT_MODE=true \ + "${DIST_ROOT}/bin/hugegraph-server.sh" \ + "${DIST_ROOT}/conf/gremlin-server.yaml" \ + "${DIST_ROOT}/conf/rest-server.properties" false >/dev/null +assert_argument "@${DIST_ROOT}/bin/jvm-module.options" "$SERVER_CAPTURE" + +INIT_STORE_CAPTURE="${TEMP_DIR}/init-store.args" +CAPTURE_FILE="$INIT_STORE_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ + "${DIST_ROOT}/bin/init-store.sh" >/dev/null +assert_argument "@${DIST_ROOT}/bin/jvm-module.options" "$INIT_STORE_CAPTURE" + +for launcher in gremlin-console dump-store; do + for options in "" "-Doperator.marker=preserved"; do + capture="${TEMP_DIR}/${launcher}-${options:+custom}.args" + CAPTURE_FILE="$capture" JAVA_HOME="$MOCK_JAVA_HOME" \ + JAVA_OPTIONS="$options" "${DIST_ROOT}/bin/${launcher}.sh" >/dev/null + assert_argument "@${DIST_ROOT}/bin/jvm-module.options" "$capture" + if [[ "$launcher" == gremlin-console && -n "$options" ]]; then + assert_argument "$options" "$capture" + fi + done +done + +# Required options survive the Server launcher's -j argument as well. +SERVER_J_CAPTURE="${TEMP_DIR}/server-j.args" +CAPTURE_FILE="$SERVER_J_CAPTURE" JAVA_HOME="$MOCK_JAVA_HOME" \ + JAVA_OPTIONS="" STDOUT_MODE=true "${DIST_ROOT}/bin/hugegraph-server.sh" \ + "${DIST_ROOT}/conf/gremlin-server.yaml" \ + "${DIST_ROOT}/conf/rest-server.properties" false \ + "-Xms512m -Xmx512m -Doperator.marker=preserved" >/dev/null +assert_argument "@${DIST_ROOT}/bin/jvm-module.options" "$SERVER_J_CAPTURE" +assert_argument "-Doperator.marker=preserved" "$SERVER_J_CAPTURE" + +mv "${DIST_ROOT}/bin/jvm-module.options" "${TEMP_DIR}/jvm-module.options" +for launcher in gremlin-console dump-store; do + capture="${TEMP_DIR}/${launcher}-missing.args" + output="${TEMP_DIR}/${launcher}-missing.log" + if CAPTURE_FILE="$capture" JAVA_HOME="$MOCK_JAVA_HOME" \ + JAVA_OPTIONS="-Doperator.marker=preserved" \ + "${DIST_ROOT}/bin/${launcher}.sh" >"$output" 2>&1; then + fail "$launcher accepted a missing JVM module options file" + fi + [[ ! -e "$capture" ]] || fail "$launcher invoked Java without required options" + grep -q "Missing or unreadable JVM module options file" "$output" || \ + fail "$launcher did not explain the missing module options file" +done + +echo "PASS: Java 17 upgrade contracts" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-launcher-gc.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-launcher-gc.sh new file mode 100644 index 0000000000..4d277596fc --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-launcher-gc.sh @@ -0,0 +1,133 @@ +#!/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. + +# Exercise each launcher without starting an application, then ask the real JVM +# to report the effective flags. Capturing argv alone misses option precedence. +set -euo pipefail + +SOURCE_ROOT=$(cd "${1:?Usage: $0 SOURCE_ROOT}" && pwd) +REAL_JAVA="${JAVA_HOME:+$JAVA_HOME/bin/}java" +REAL_JAVA=$(command -v "$REAL_JAVA") +FIXTURE=$(mktemp -d) +trap 'rm -rf "$FIXTURE"' EXIT +mkdir -p "$FIXTURE/bin" "$FIXTURE/jdk/bin" "$FIXTURE/conf" \ + "$FIXTURE/lib" "$FIXTURE/ext" "$FIXTURE/plugins" "$FIXTURE/logs" +cat > "$FIXTURE/jdk/bin/java" <<'JAVA' +#!/bin/bash +for arg in "$@"; do + if [[ "$arg" == -version ]]; then + exec "$REAL_JAVA" -version + fi +done +printf '%s\n' "$@" > "$CAPTURE_FILE" +options=() +for arg in "$@"; do + [[ "$arg" == -cp || "$arg" == -jar ]] && break + options+=("$arg") +done +exec "$REAL_JAVA" "${options[@]}" -XX:+PrintFlagsFinal -version +JAVA +# Avoid native allocator downloads; this fixture only checks JVM configuration. +printf '%s\n' '#!/bin/sh' 'echo launcher-test' > "$FIXTURE/jdk/bin/uname" +chmod +x "$FIXTURE/jdk/bin/"* + +failed=0 +for component in server pd store; do + case "$component" in + server) + source_bin="$SOURCE_ROOT/hugegraph-server/hugegraph-dist/src/assembly/static/bin" + launcher=hugegraph-server.sh + ;; + pd) + source_bin="$SOURCE_ROOT/hugegraph-pd/hg-pd-dist/src/assembly/static/bin" + launcher=start-hugegraph-pd.sh + touch "$FIXTURE/lib/hg-pd-service-test.jar" + ;; + store) + source_bin="$SOURCE_ROOT/hugegraph-store/hg-store-dist/src/assembly/static/bin" + launcher=start-hugegraph-store.sh + rm -f "$FIXTURE/lib/"*.jar + touch "$FIXTURE/lib/hg-store-node-test.jar" + ;; + esac + cp "$source_bin/$launcher" "$source_bin/util.sh" "$source_bin/jvm-module.options" "$FIXTURE/bin/" + # Keep the computed-heap/-j path deterministic without probing host memory. + printf '\nfree_memory() { echo 2048; }\n' >> "$FIXTURE/bin/util.sh" + cp "$SOURCE_ROOT/hugegraph-server/hugegraph-dist/src/assembly/static/bin/preload-topling.sh" "$FIXTURE/bin/" + for mode in default g1 G1 env-override cli-override serial zgc; do + gc="" + user_options="" + jvm_options="-Xms64m -Xmx64m" + collector=UseG1GC + ihop=50 + ref_proc=true + rset_pause=5 + override_options="-XX:InitiatingHeapOccupancyPercent=45 -XX:-ParallelRefProcEnabled + -XX:G1RSetUpdatingPauseTimePercent=2" + case "$mode" in + default) collector="" ;; + g1|G1) gc="$mode" ;; + env-override) + gc=g1; jvm_options="$jvm_options $override_options" + ihop=45; ref_proc=false; rset_pause=2 ;; + cli-override) + gc=g1; jvm_options=""; user_options="-Xms64m -Xmx64m $override_options" + ihop=45; ref_proc=false; rset_pause=2 ;; + serial) jvm_options="$jvm_options -XX:+UseSerialGC"; collector=UseSerialGC ;; + zgc) jvm_options="$jvm_options -XX:+UseZGC"; collector=UseZGC ;; + esac + args=(-d false) + [[ -z "$gc" ]] || args+=(-g "$gc") + [[ -z "$user_options" ]] || args+=(-j "$user_options") + if [[ "$component" == server ]]; then + args=(conf/gremlin-server.yaml conf/rest-server.properties false + "$user_options" "$gc" false) + fi + capture="$FIXTURE/$component-$mode.args" + output="$FIXTURE/$component-$mode.flags" + if ! env -u GC_OPTION -u JAVA_TOOL_OPTIONS -u _JAVA_OPTIONS -u JDK_JAVA_OPTIONS -u LD_PRELOAD \ + -u TOPLINGDB_ROCKSDB_PROVIDER -u TOPLING_RUNTIME_CLASSPATH -u TOPLING_ACTIVE_NATIVE \ + JAVA_HOME="$FIXTURE/jdk" REAL_JAVA="$REAL_JAVA" JAVA_OPTIONS="$jvm_options" \ + OPEN_TELEMETRY=false STDOUT_MODE=true CAPTURE_FILE="$capture" \ + PATH="$FIXTURE/jdk/bin:$PATH" bash "$FIXTURE/bin/$launcher" "${args[@]}" > "$output" 2>&1; then + echo "FAIL: $component $mode launcher rejected JVM options" >&2 + failed=$((failed + 1)) + continue + fi + if [[ -n "$collector" ]] && + ! grep -Eq " $collector[[:space:]]*=[[:space:]]*true " "$output" || + ! grep -Eq " InitiatingHeapOccupancyPercent[[:space:]]*=[[:space:]]*$ihop " "$output" || + ! grep -Eq " ParallelRefProcEnabled[[:space:]]*=[[:space:]]*$ref_proc " "$output" || + ! grep -Eq " G1RSetUpdatingPauseTimePercent[[:space:]]*=[[:space:]]*$rset_pause " "$output"; then + echo "FAIL: $component $mode expected ${collector:-JVM default}," \ + "IHOP=$ihop, ref=$ref_proc, RSet=$rset_pause" >&2 + grep -E " (Use.*GC|InitiatingHeapOccupancyPercent|ParallelRefProcEnabled|G1RSetUpdatingPauseTimePercent) " \ + "$output" >&2 + failed=$((failed + 1)) + continue + fi + if [[ -n "$gc" ]]; then + grep -Fxq -- '-XX:+UseG1GC' "$capture" + elif grep -Fxq -- '-XX:+UseG1GC' "$capture"; then + echo "FAIL: $component enabled G1 without an explicit selection" >&2 + exit 1 + fi + echo "PASS: $component $mode (${collector:-JVM default}," \ + "IHOP=$ihop, ref=$ref_proc, RSet=$rset_pause)" + done +done +[[ $failed -eq 0 ]] diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-pd-docker-entrypoint.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-pd-docker-entrypoint.sh index ed39b67492..2d17fccf8b 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-pd-docker-entrypoint.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-pd-docker-entrypoint.sh @@ -147,18 +147,57 @@ for wildcard in '*' 'health,*'; do fi done -# The secret is required, and must never be echoed to the log -if (cd "${TMP_DIR}" && env \ +# The secret is required, and failure must explain its purpose and injection +# path before the launcher is called. Remove the earlier stub output so that +# a silent startup cannot pass this check. +rm -f "${TMP_DIR}/spring.json" +if missing_out=$(cd "${TMP_DIR}" && env -u HG_PD_AUTH_SECRET_KEY \ HG_PD_GRPC_HOST=pd0 HG_PD_RAFT_ADDRESS=pd0:8610 \ HG_PD_RAFT_PEERS_LIST=pd0:8610 HG_PD_INITIAL_STORE_LIST=store0:8500 \ - ./docker-entrypoint.sh >/dev/null 2>&1); then + ./docker-entrypoint.sh 2>&1); then echo " FAIL missing secret: entrypoint started without HG_PD_AUTH_SECRET_KEY" FAIL=$((FAIL + 1)) else - echo " PASS missing secret is refused" - PASS=$((PASS + 1)) + missing_rc=$? + missing_valid=true + [[ "${missing_rc}" -eq 2 && ! -f "${TMP_DIR}/spring.json" ]] || missing_valid=false + for hint in "ERROR: missing required env 'HG_PD_AUTH_SECRET_KEY'" \ + 'PD REST Basic authentication (auth.secret-key)' \ + 'pd.auth.autoGenerate=true' 'env.valueFrom.secretKeyRef' \ + 'pd.auth.existingSecret and pd.auth.key' \ + 'release namespace' 'same value on every PD' 'PD_AUTH_PASSWORD'; do + [[ "${missing_out}" == *"${hint}"* ]] || missing_valid=false + done + if [[ "${missing_valid}" == true ]]; then + echo " PASS missing secret is refused with actionable guidance" + PASS=$((PASS + 1)) + else + echo " FAIL missing secret guidance or exit status" + printf '%s\n' "${missing_out}" + FAIL=$((FAIL + 1)) + fi +fi + +# Missing unrelated required variables retain the concise existing error. +if missing_out=$(cd "${TMP_DIR}" && env -u HG_PD_GRPC_HOST -u GRPC_HOST \ + HG_PD_RAFT_ADDRESS=pd0:8610 HG_PD_RAFT_PEERS_LIST=pd0:8610 \ + HG_PD_INITIAL_STORE_LIST=store0:8500 HG_PD_AUTH_SECRET_KEY=not-logged \ + ./docker-entrypoint.sh 2>&1); then + echo " FAIL missing gRPC host was accepted" + FAIL=$((FAIL + 1)) +else + missing_rc=$? + if [[ "${missing_rc}" -eq 2 && \ + "${missing_out}" == "ERROR: missing required env 'HG_PD_GRPC_HOST'" ]]; then + echo " PASS unrelated missing env retains its existing error" + PASS=$((PASS + 1)) + else + echo " FAIL unrelated missing env error changed" + FAIL=$((FAIL + 1)) + fi fi +# The configured secret must never be echoed to the log. log_out=$(cd "${TMP_DIR}" && env \ HG_PD_GRPC_HOST=pd0 HG_PD_RAFT_ADDRESS=pd0:8610 \ HG_PD_RAFT_PEERS_LIST=pd0:8610 HG_PD_INITIAL_STORE_LIST=store0:8500 \ diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-riscv64-java-runtime.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-riscv64-java-runtime.sh new file mode 100755 index 0000000000..f84bb084ac --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-riscv64-java-runtime.sh @@ -0,0 +1,320 @@ +#!/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. +# +set -euo pipefail + +TRAVIS_DIR=$(cd "$(dirname "$0")" && pwd) +SMOKE_SCRIPT="$TRAVIS_DIR/run-rocksdb-jni-smoke-test.sh" +NATIVE_SMOKE_SCRIPT="$TRAVIS_DIR/run-native-runtime-smoke-test.sh" +WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/hugegraph-riscv64-java-test.XXXXXX") +MOCK_BIN="$WORK_DIR/bin" +MOCK_JAVA_HOME="$WORK_DIR/java-home" +SERVER_DIR="$WORK_DIR/server" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$MOCK_BIN" "$MOCK_JAVA_HOME/bin" "$SERVER_DIR/bin" "$SERVER_DIR/lib" + +cat > "$MOCK_BIN/uname" <<'EOF' +#!/bin/bash +echo "${MOCK_ARCH:-riscv64}" +EOF + +cat > "$MOCK_JAVA_HOME/bin/java" <<'EOF' +#!/bin/bash +set -euo pipefail + +JAVA_VERSION=${MOCK_JAVA_VERSION:-17.0.20} +JAVA_VENDOR=${MOCK_JAVA_VENDOR:-Eclipse Adoptium} + +case "${1:-}" in + -version) + echo "openjdk version \"$JAVA_VERSION\"" >&2 + ;; + -XshowSettings:properties) + echo " java.vm.name = OpenJDK 64-Bit Server VM" >&2 + echo " java.vm.vendor = $JAVA_VENDOR" >&2 + echo " java.vm.version = $JAVA_VERSION+8" >&2 + echo " java.vm.info = mixed mode, sharing" >&2 + echo "openjdk version \"$JAVA_VERSION\"" >&2 + ;; + -cp) + if [[ "${MOCK_ARCH:-riscv64}" == arm64 && -n "${LD_PRELOAD:-}" ]]; then + echo "ARM JNI smoke inherited RISC-V native preload" >&2 + exit 1 + fi + echo "rocksdb-jni-smoke-ok" + ;; + *) + echo "Unexpected Java arguments: $*" >&2 + exit 1 + ;; +esac +EOF + +cat > "$SERVER_DIR/bin/util.sh" <<'EOF' +#!/bin/bash +configure_riscv64_libatomic() { + if [[ "$(uname -m)" == riscv64 ]]; then + LD_PRELOAD=libatomic.so.1 + fi +} +EOF + +cat > "$SERVER_DIR/bin/init-store.sh" <<'EOF' +#!/bin/bash +exit 42 +EOF + +chmod +x "$MOCK_BIN/uname" "$MOCK_JAVA_HOME/bin/java" \ + "$SERVER_DIR/bin/init-store.sh" + +run_smoke() { + env -u LD_PRELOAD \ + PATH="$MOCK_BIN:$PATH" \ + JAVA_HOME="$MOCK_JAVA_HOME" \ + "$@" "$SMOKE_SCRIPT" "$SERVER_DIR" +} + +run_native_smoke() { + env -u LD_PRELOAD \ + PATH="$MOCK_BIN:$PATH" \ + JAVA_HOME="$MOCK_JAVA_HOME" \ + "$@" "$NATIVE_SMOKE_SCRIPT" "$SERVER_DIR" +} + +if ! DEFAULT_OUTPUT=$(run_smoke 2>&1); then + echo "$DEFAULT_OUTPUT" >&2 + echo "RISC-V smoke rejected the Java 17 baseline" >&2 + exit 1 +fi +grep -q '^rocksdb-jni-smoke-ok$' <<< "$DEFAULT_OUTPUT" + +set +e +NATIVE_OUTPUT=$(run_native_smoke 2>&1) +NATIVE_STATUS=$? +set -e +if [[ $NATIVE_STATUS -ne 42 ]]; then + echo "$NATIVE_OUTPUT" >&2 + echo "Native smoke did not reach the controlled post-JNI boundary" >&2 + exit 1 +fi +grep -q '^rocksdb-jni-smoke-ok$' <<< "$NATIVE_OUTPUT" + +# macOS passes the setup-java version through the native wrapper unchanged. +for EXPECTED_VERSION in 17 17.0.20 21.0.1; do + set +e + VERSION_OUTPUT=$(run_native_smoke MOCK_ARCH=arm64 EXPECTED_ARCH=arm64 \ + "EXPECTED_JAVA_MAJOR=$EXPECTED_VERSION" \ + "MOCK_JAVA_VERSION=$EXPECTED_VERSION" 2>&1) + VERSION_STATUS=$? + set -e + if [[ $VERSION_STATUS -ne 42 ]]; then + echo "$VERSION_OUTPUT" >&2 + echo "ARM native smoke rejected expected version $EXPECTED_VERSION" >&2 + exit 1 + fi + grep -q '^rocksdb-jni-smoke-ok$' <<< "$VERSION_OUTPUT" +done +if MAJOR_OUTPUT=$(run_smoke MOCK_ARCH=arm64 EXPECTED_ARCH=arm64 \ + EXPECTED_JAVA_MAJOR=21.0.1 MOCK_JAVA_VERSION=17.0.20 2>&1); then + echo "$MAJOR_OUTPUT" >&2 + echo "ARM smoke accepted a mismatched Java major" >&2 + exit 1 +fi +grep -Fq 'Expected Java 21, got 17.0.20' <<< "$MAJOR_OUTPUT" + +for JAVA_MAJOR_MISMATCH in 11.0.31 21.0.8; do + if MAJOR_OUTPUT=$(run_smoke \ + "MOCK_JAVA_VERSION=$JAVA_MAJOR_MISMATCH" 2>&1); then + echo "$MAJOR_OUTPUT" >&2 + echo "RISC-V smoke accepted Java $JAVA_MAJOR_MISMATCH" >&2 + exit 1 + fi + grep -Fq "Expected Java 17, got $JAVA_MAJOR_MISMATCH" <<< "$MAJOR_OUTPUT" +done + +EXPECTED_ARGS=( + EXPECTED_JAVA_MAJOR=17 + EXPECTED_RISCV64_JAVA_VERSION=17.0.20 + "EXPECTED_RISCV64_JAVA_VENDOR=Eclipse Adoptium" +) +if ! EXPECTED_OUTPUT=$(run_smoke "${EXPECTED_ARGS[@]}" 2>&1); then + echo "$EXPECTED_OUTPUT" >&2 + echo "RISC-V smoke rejected the configured Temurin 17 runtime" >&2 + exit 1 +fi +grep -q '^rocksdb-jni-smoke-ok$' <<< "$EXPECTED_OUTPUT" + +if VERSION_OUTPUT=$(run_smoke "${EXPECTED_ARGS[@]}" \ + MOCK_JAVA_VERSION=17.0.21 2>&1); then + echo "$VERSION_OUTPUT" >&2 + echo "RISC-V smoke accepted an unexpected Java version" >&2 + exit 1 +fi +grep -Fq 'Expected RISC-V Java 17.0.20, got 17.0.21' <<< "$VERSION_OUTPUT" + +if VENDOR_OUTPUT=$(run_smoke "${EXPECTED_ARGS[@]}" \ + "MOCK_JAVA_VENDOR=Unknown Vendor" 2>&1); then + echo "$VENDOR_OUTPUT" >&2 + echo "RISC-V smoke accepted an unexpected Java vendor" >&2 + exit 1 +fi +grep -Fq \ + 'Expected RISC-V Java vendor Eclipse Adoptium, got Unknown Vendor' \ + <<< "$VENDOR_OUTPUT" + +# Exercise the native wrapper through restart using controlled service commands. +# An isolated PATH makes the gtimeout fallback independent of the host platform. +RUNTIME_BIN="$WORK_DIR/runtime-bin" +RUNTIME_TRAVIS="$WORK_DIR/travis" +RUNTIME_LOG="$WORK_DIR/runtime.log" +START_LOG="$WORK_DIR/start.log" +STOP_LOG="$WORK_DIR/stop.log" +PHASE_LOG="$WORK_DIR/phase.log" +RUN_ID_FILE="$WORK_DIR/run-id" +mkdir -p "$RUNTIME_BIN" "$RUNTIME_TRAVIS" "$WORK_DIR/tmp" "$SERVER_DIR/logs" +printf '%s\n' 'mock service log' > "$SERVER_DIR/logs/server.log" +for command in dirname date mktemp rm tee grep awk cut getconf find; do + ln -s "$(command -v "$command")" "$RUNTIME_BIN/$command" +done +cp "$MOCK_BIN/uname" "$RUNTIME_BIN/uname" +cp "$NATIVE_SMOKE_SCRIPT" "$SMOKE_SCRIPT" "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_TRAVIS/" + +cat > "$RUNTIME_BIN/timeout" <<'EOF' +#!/bin/bash +set -euo pipefail +printf '%s' "${0##*/}" >> "$RUNTIME_LOG" +printf ' <%s>' "$@" >> "$RUNTIME_LOG" +printf '\n' >> "$RUNTIME_LOG" +if [[ "${MOCK_START_TIMEOUT:-false}" == true && "$4" == */start-hugegraph.sh ]]; then + exit 124 +fi +shift 3 +exec "$@" +EOF + +cat > "$SERVER_DIR/bin/init-store.sh" <<'EOF' +#!/bin/bash +exit 0 +EOF + +cat > "$SERVER_DIR/bin/start-hugegraph.sh" <<'EOF' +#!/bin/bash +set -euo pipefail +printf 'arch=%s' "$EXPECTED_ARCH" >> "$START_LOG" +printf ' <%s>' "$@" >> "$START_LOG" +printf '\n' >> "$START_LOG" +printf '%s\n' "${MOCK_START_OUTPUT:-}" +EOF + +cat > "$SERVER_DIR/bin/stop-hugegraph.sh" <<'EOF' +#!/bin/bash +printf '%s\n' "$*" >> "$STOP_LOG" +exit "${MOCK_STOP_STATUS:-0}" +EOF + +cat > "$SERVER_DIR/bin/dump-store.sh" <<'EOF' +#!/bin/bash +set -euo pipefail +RUN_ID=$(cat "$RUN_ID_FILE") +printf '%s\n' 'Dump table VERTEX (offset 0 limit 20):' \ + "riscv-smoke-v1-$RUN_ID" "riscv-smoke-v2-$RUN_ID" +EOF + +cat > "$RUNTIME_TRAVIS/run-server-e2e-smoke-test.sh" <<'EOF' +#!/bin/bash +set -euo pipefail +printf '%s\n' "$2" >> "$PHASE_LOG" +printf '%s\n' "$3" > "$RUN_ID_FILE" +printf 'server-e2e-smoke-%s-ok\n' "$2" +EOF + +ln -s "$(command -v cat)" "$RUNTIME_BIN/cat" +chmod +x "$RUNTIME_BIN/timeout" "$SERVER_DIR/bin/"*.sh \ + "$RUNTIME_TRAVIS/"*.sh + +run_restart_smoke() { + rm -f "$RUNTIME_LOG" "$START_LOG" "$STOP_LOG" "$PHASE_LOG" "$RUN_ID_FILE" + env -u LD_PRELOAD -u SERVER_JAVA_OPTIONS \ + PATH="$RUNTIME_BIN" JAVA_HOME="$MOCK_JAVA_HOME" TMPDIR="$WORK_DIR/tmp" \ + RUNTIME_LOG="$RUNTIME_LOG" START_LOG="$START_LOG" STOP_LOG="$STOP_LOG" \ + PHASE_LOG="$PHASE_LOG" RUN_ID_FILE="$RUN_ID_FILE" \ + "$@" "$RUNTIME_TRAVIS/run-native-runtime-smoke-test.sh" "$SERVER_DIR" +} + +if ! RESTART_OUTPUT=$(run_restart_smoke SERVER_STARTUP_TIMEOUT=7 STORE_DUMP_TIMEOUT=9 2>&1); then + echo "$RESTART_OUTPUT" >&2 + echo "RISC-V native smoke failed the controlled restart" >&2 + exit 1 +fi +grep -q '^native-runtime-smoke-ok$' <<< "$RESTART_OUTPUT" +[[ $(wc -l < "$START_LOG") -eq 2 ]] +[[ $(wc -l < "$STOP_LOG") -eq 2 ]] +grep -q '^arch=riscv64 <-t> <7>$' "$START_LOG" +grep -Fq 'timeout <--foreground> <--kill-after=15s> <37s>' "$RUNTIME_LOG" +grep -Fq 'timeout <--foreground> <--kill-after=15s> <9s>' "$RUNTIME_LOG" +[[ $(cat "$PHASE_LOG") == $'create\nverify' ]] + +mv "$RUNTIME_BIN/timeout" "$RUNTIME_BIN/gtimeout" +if ! ARM_OUTPUT=$(run_restart_smoke MOCK_ARCH=arm64 EXPECTED_ARCH=arm64 \ + 'SERVER_JAVA_OPTIONS=-Xms512m -Xmx2g' 2>&1); then + echo "$ARM_OUTPUT" >&2 + echo "ARM native smoke failed the controlled restart" >&2 + exit 1 +fi +grep -q '^native-runtime-smoke-ok$' <<< "$ARM_OUTPUT" +[[ $(grep -c '^arch=arm64 <-t> <300> <-j> <-Xms512m -Xmx2g>$' "$START_LOG") -eq 2 ]] +[[ $(grep -c '^gtimeout ' "$RUNTIME_LOG") -eq 3 ]] +[[ $(cat "$PHASE_LOG") == $'create\nverify' ]] + +if COMPATIBILITY_OUTPUT=$(run_restart_smoke MOCK_ARCH=arm64 EXPECTED_ARCH=arm64 \ + 'MOCK_START_OUTPUT=java.lang.reflect.InaccessibleObjectException: denied' 2>&1); then + echo "$COMPATIBILITY_OUTPUT" >&2 + echo "Native smoke accepted a caught Java compatibility error" >&2 + exit 1 +fi +grep -Fq 'Java runtime compatibility error' <<< "$COMPATIBILITY_OUTPUT" + +set +e +TIMEOUT_OUTPUT=$(run_restart_smoke MOCK_ARCH=arm64 EXPECTED_ARCH=arm64 \ + MOCK_START_TIMEOUT=true 2>&1) +TIMEOUT_STATUS=$? +set -e +if [[ $TIMEOUT_STATUS -ne 124 || -e "$PHASE_LOG" || $(wc -l < "$STOP_LOG") -ne 1 ]]; then + echo "$TIMEOUT_OUTPUT" >&2 + echo "Native startup timeout did not preserve failure and clean up" >&2 + exit 1 +fi + +rm "$RUNTIME_BIN/gtimeout" +set +e +MISSING_TIMEOUT_OUTPUT=$(run_restart_smoke 2>&1) +MISSING_TIMEOUT_STATUS=$? +set -e +[[ $MISSING_TIMEOUT_STATUS -eq 1 ]] +grep -Fq 'Required command is unavailable: timeout or gtimeout' <<< "$MISSING_TIMEOUT_OUTPUT" +[[ $(wc -l < "$STOP_LOG") -eq 1 ]] +for TEMP_DIR in "$WORK_DIR/tmp"/hugegraph-native-runtime-smoke.*; do + [[ ! -e "$TEMP_DIR" ]] +done + +echo "PASS: RISC-V Java runtime contract" +echo "PASS: native restart, ARM heap, timeout and cleanup contracts" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-pd.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-pd.sh index 754e2baae1..9677959aa9 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-pd.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-pd.sh @@ -15,14 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# test-start-hugegraph-pd.sh — Tests for start-hugegraph-pd.sh foreground mode fix +# test-start-hugegraph-pd.sh — Foreground mode and GC option regression tests # -# Baseline (unmodified code): Tests 1 PASS — Tests 2, 3, 4 FAIL -# After chunk 2 fix: All 4 tests PASS -# -# Usage: ./test-start-hugegraph-pd.sh [path-to-pd-dist-root] +# Usage: ./test-start-hugegraph-pd.sh [path-to-pd-dist-root] [--gc-only] # path-to-pd-dist-root: path to extracted PD dist e.g. -# hugegraph-pd/apache-hugegraph-pd-1.7.0/ +# hugegraph-pd/apache-hugegraph-pd-1.8.0/ # defaults to current directory if not provided set -uo pipefail @@ -36,6 +33,8 @@ PD_URL="http://localhost:8620" STARTUP_WAIT=60 # seconds to wait for PD HTTP to respond SETTLE_WAIT=5 # seconds after kill before checking exit WAIT_TIMEOUT=30 # seconds for wait_script_exit timeout +RUNTIME_LOG_CHECK="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/check-java-runtime-logs.sh" +ACTIVE_RUN=false PASS=0 FAIL=0 @@ -66,6 +65,13 @@ section() { echo "── $1 ──" } +start_service() { + mkdir -p "$PD_ROOT/logs" + ACTIVE_RUN=true + "$START_SCRIPT" "$@" > "$PD_ROOT/logs/launcher.out" 2>&1 & + SCRIPT_PID=$! +} + cleanup() { info "Cleaning up..." "$STOP_SCRIPT" >/dev/null 2>&1 || true @@ -73,7 +79,6 @@ cleanup() { kill "$(cat "$PID_FILE")" 2>/dev/null || true fi rm -f "$PID_FILE" - rm -rf "$PD_ROOT/logs/" # kill anything still holding the PD port: fuser on Linux, lsof on macOS, # which ships no fuser. Only the Linux path had to lose its lsof dependency. if [[ "$(uname)" == "Darwin" ]]; then @@ -93,6 +98,15 @@ cleanup() { echo "Warning: Neither lsof nor fuser available for PD port cleanup" fi sleep 3 + if [[ "$ACTIVE_RUN" == true ]]; then + if bash "$RUNTIME_LOG_CHECK" "$PD_ROOT/logs"; then + pass "startup logs contain no Java runtime compatibility errors" + else + fail "startup logs contain Java runtime compatibility errors or could not be checked" + fi + ACTIVE_RUN=false + fi + rm -rf "$PD_ROOT/logs/" } # Wait until PD health endpoint responds or timeout @@ -157,7 +171,7 @@ wait_script_exit() { # ── preflight ───────────────────────────────────────────────────────────────── echo "" -echo "start-hugegraph-pd.sh chunk 2 test suite" +echo "start-hugegraph-pd.sh test suite" echo "root: $PD_ROOT" echo "" @@ -167,10 +181,134 @@ if [[ ! -f "$START_SCRIPT" ]]; then exit 1 fi +# Exercise the shipped launcher in an isolated fixture, without JNI or a service. +# This checks shell selection/argv only; the tests below check real JVM startup. +check_gc_options() { + local fixture option capture status jvm_options + fixture=$(mktemp -d) || { fail "could not create GC test fixture"; return 1; } + fixture=$(cd "$fixture" && pwd -P) + if ! mkdir -p "$fixture/bin" "$fixture/conf" "$fixture/lib" "$fixture/jdk/bin" || + ! cp "$START_SCRIPT" "$BIN/util.sh" "$BIN/preload-topling.sh" \ + "$BIN/jvm-module.options" "$fixture/bin/"; then + fail "could not prepare GC test fixture" + rm -rf "$fixture" + return 1 + fi + # Make default memory selection independent of host memory and OS. + printf '%s\n' 'calc_xmx() { echo 1024; }' >> "$fixture/bin/util.sh" + touch "$fixture/lib/hg-pd-service-test.jar" + cat > "$fixture/jdk/bin/java" <<'JAVA' +#!/bin/bash +if [[ "$1" == "-version" ]]; then + echo 'openjdk version "17.0.1"' >&2 +else + printf '%s\n' "$@" > "$CAPTURE_FILE" + for arg in "$@"; do + if [[ "$arg" == @* ]]; then + sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d' "${arg#@}" >> "$CAPTURE_FILE" + fi + done +fi +JAVA + # Avoid the Store allocator download, which is unrelated to GC selection. + printf '%s\n' '#!/bin/sh' 'echo launcher-test' > "$fixture/jdk/bin/uname" + chmod +x "$fixture/jdk/bin/"* "$fixture/bin/start-hugegraph-pd.sh" + for option in default custom-java-options user-options default-serial default-zgc \ + g1 G1 zgc ZGC invalid missing-module-options; do + capture="$fixture/$option.args" + local -a args=(-d false) + jvm_options="-Xms64m -Xmx64m" + case "$option" in + default) jvm_options="" ;; + custom-java-options) jvm_options="$jvm_options -Dlauncher.test.custom=true" ;; + user-options) jvm_options=""; args+=(-j "-Xmx96m -Dlauncher.test=true") ;; + missing-module-options) rm "$fixture/bin/jvm-module.options" ;; + default-serial) jvm_options="$jvm_options -XX:+UseSerialGC" ;; + default-zgc) jvm_options="$jvm_options -XX:+UseZGC" ;; + *) args+=(-g "$option") ;; + esac + env -u GC_OPTION -u JAVA_TOOL_OPTIONS -u LD_PRELOAD \ + -u TOPLINGDB_ROCKSDB_PROVIDER -u TOPLING_RUNTIME_CLASSPATH -u TOPLING_ACTIVE_NATIVE \ + JAVA_HOME="$fixture/jdk" JAVA_OPTIONS="$jvm_options" \ + OPEN_TELEMETRY=false STDOUT_MODE=true CAPTURE_FILE="$capture" \ + PATH="$fixture/jdk/bin:$PATH" \ + bash "$fixture/bin/start-hugegraph-pd.sh" "${args[@]}" > "$fixture/output" 2>&1 + status=$? + if [[ "$option" == missing-module-options ]]; then + if [[ $status -eq 1 && ! -e "$capture" ]] && + grep -Fq 'JVM module options file is missing or unreadable' "$fixture/output"; then + pass "missing JVM module options are rejected before application launch" + else + fail "missing JVM module options were not rejected correctly" + fi + elif [[ "$option" == invalid ]]; then + if [[ $status -eq 1 && ! -e "$capture" ]] && \ + grep -Fq "Unrecognized gc option: 'invalid'" "$fixture/logs/hugegraph-pd-stdout.log"; then + pass "invalid GC is rejected before application launch" + else + fail "invalid GC was not rejected correctly" + fi + elif [[ $status -ne 0 || ! -s "$capture" ]]; then + fail "GC '$option' did not reach Java (exit $status)" + elif ! grep -Fxq -- "@$fixture/bin/jvm-module.options" "$capture" || + ! grep -Fxq -- '--add-opens=java.base/java.lang=ALL-UNNAMED' "$capture"; then + fail "launcher '$option' dropped mandatory JVM module access" + elif [[ "$option" == custom-java-options ]] && + ! grep -Fxq -- '-Dlauncher.test.custom=true' "$capture"; then + fail "launcher dropped custom JAVA_OPTIONS" + elif [[ "$option" == user-options ]] && + ! grep -Fxq -- '-Dlauncher.test=true' "$capture"; then + fail "launcher dropped custom -j options" + elif [[ "$option" == zgc || "$option" == ZGC ]]; then + if grep -Fxq -- '-XX:+UseZGC' "$capture" && \ + ! grep -Fxq -- '-XX:InitiatingHeapOccupancyPercent=50' "$capture"; then + pass "GC '$option' selects ZGC without G1 tuning" + else + fail "GC '$option' selected incorrect JVM options" + fi + elif [[ "$option" == g1 || "$option" == G1 ]]; then + if grep -Fxq -- '-XX:+UseG1GC' "$capture" && + grep -Fxq -- '-XX:InitiatingHeapOccupancyPercent=50' "$capture" && + ! grep -Fxq -- '-XX:+UseZGC' "$capture"; then + pass "GC '$option' explicitly selects G1" + else + fail "GC '$option' did not explicitly select G1" + fi + elif grep -Fxq -- '-XX:InitiatingHeapOccupancyPercent=50' "$capture" && + ! grep -Fxq -- '-XX:+UseG1GC' "$capture"; then + if [[ "$option" == default-serial ]] && + ! grep -Fxq -- '-XX:+UseSerialGC' "$capture"; then + fail "default GC selection dropped the caller's Serial GC" + elif [[ "$option" == default-zgc ]] && + ! grep -Fxq -- '-XX:+UseZGC' "$capture"; then + fail "default GC selection dropped the caller's ZGC" + elif [[ "$option" == default ]] && grep -Fxq -- '-XX:+UseZGC' "$capture"; then + fail "default GC selection unexpectedly enabled ZGC" + else + pass "GC '$option' preserves JVM/caller collector selection" + fi + else + fail "GC '$option' selected incorrect JVM options" + fi + done + rm -rf "$fixture" +} + +section "GC option selection (isolated launcher fixture)" +check_gc_options +if [[ "${2:-}" == --gc-only ]]; then + [[ $FAIL -eq 0 ]] && exit 0 || exit 1 +fi + +if [[ ! -r "$RUNTIME_LOG_CHECK" ]]; then + echo "ERROR: runtime log checker not found: $RUNTIME_LOG_CHECK" + exit 1 +fi + if [[ "$(uname)" == "Darwin" ]]; then - _prereq_tools="lsof curl java" + _prereq_tools="lsof curl java jcmd" else - _prereq_tools="fuser curl java" + _prereq_tools="fuser curl java jcmd" fi for tool in $_prereq_tools; do if ! command -v "$tool" >/dev/null 2>&1; then @@ -186,8 +324,7 @@ cleanup section "Test 1 — daemon mode regression" info "Starting in daemon mode (no -d flag, default behavior)..." -"$START_SCRIPT" >/dev/null 2>&1 & -SCRIPT_PID=$! +start_service # daemon mode: script backgrounds Java and exits quickly sleep 5 @@ -233,9 +370,8 @@ cleanup section "Test 2 — foreground mode blocks until Java exits" -info "Starting in foreground mode (-d false)..." -"$START_SCRIPT" -d false >/dev/null 2>&1 & -SCRIPT_PID=$! +info "Starting in foreground mode (-d false -g g1)..." +start_service -d false -g g1 info "Waiting up to ${STARTUP_WAIT}s for PD to come up..." if wait_for_pd; then @@ -260,6 +396,17 @@ else info "pid file not written (expected before chunk 2 fix)" fi +if [[ -n "$FG_PID" ]] && ps -p "$FG_PID" >/dev/null 2>&1; then + if GC_FLAGS=$(jcmd "$FG_PID" VM.flags 2>&1) && + grep -Fq -- '-XX:+UseG1GC' <<<"$GC_FLAGS"; then + pass "explicit -g g1 runs the JVM with G1GC" + else + fail "explicit -g g1 did not run the JVM with G1GC: $GC_FLAGS" + fi +else + fail "no running JVM available to verify explicit -g g1" +fi + # cleanup: kill Java or the script if [[ -n "$FG_PID" ]]; then kill "$FG_PID" 2>/dev/null || true @@ -280,9 +427,8 @@ cleanup section "Test 3 — exit code propagates from Java" -info "Starting in foreground mode (-d false)..." -"$START_SCRIPT" -d false >/dev/null 2>&1 & -SCRIPT_PID=$! +info "Starting in foreground mode (-d false -g ZGC)..." +start_service -d false -g ZGC info "Waiting up to ${STARTUP_WAIT}s for PD..." if wait_for_pd; then @@ -336,8 +482,7 @@ cleanup section "Test 4 — SIGTERM forwarded to Java in foreground mode" info "Starting in foreground mode (-d false)..." -"$START_SCRIPT" -d false >/dev/null 2>&1 & -SCRIPT_PID=$! +start_service -d false info "Waiting up to ${STARTUP_WAIT}s for PD..." if wait_for_pd; then diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-signal.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-signal.sh new file mode 100644 index 0000000000..cd3df0814c --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-signal.sh @@ -0,0 +1,107 @@ +#!/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. +# +# Verifies that foreground start-hugegraph.sh exits on SIGINT when its +# background server child ignores SIGINT, as a JVM may do in this launch mode. + +set -uo pipefail + +START_SCRIPT="${1:-}" +if [[ -z "$START_SCRIPT" ]]; then + echo "Usage: $0 " + exit 2 +fi + +if [[ ! -f "$START_SCRIPT" ]]; then + echo "ERROR: start script not found: $START_SCRIPT" + exit 2 +fi + +if ! command -v timeout >/dev/null 2>&1; then + echo "SKIP: required tool 'timeout' not found" + exit 77 +fi + +TEST_ROOT=$(mktemp -d) +PID_FILE="$TEST_ROOT/bin/pid" + +cleanup() { + if [[ -s "$PID_FILE" ]]; then + kill -TERM "$(cat "$PID_FILE")" 2>/dev/null || true + fi + rm -rf "$TEST_ROOT" +} +trap cleanup EXIT + +mkdir -p "$TEST_ROOT/bin" "$TEST_ROOT/conf" "$TEST_ROOT/logs" "$TEST_ROOT/scripts" +cp "$START_SCRIPT" "$TEST_ROOT/bin/start-hugegraph.sh" + +cat > "$TEST_ROOT/bin/util.sh" <<'EOF' +read_property() { + local file="$1" + local property="$2" + grep "^${property}=" "$file" | head -n 1 | cut -d '=' -f 2- +} + +check_port() { + : +} +EOF + +cat > "$TEST_ROOT/bin/hugegraph-server.sh" <<'EOF' +#!/bin/bash +trap 'exit 0' TERM +trap '' INT +while true; do + sleep 1 +done +EOF + +cat > "$TEST_ROOT/conf/rest-server.properties" <<'EOF' +gremlinserver.url=http://127.0.0.1:8182 +restserver.url=http://127.0.0.1:8080 +EOF + +chmod +x "$TEST_ROOT/bin/start-hugegraph.sh" "$TEST_ROOT/bin/hugegraph-server.sh" + +export PID_FILE +export START_SCRIPT="$TEST_ROOT/bin/start-hugegraph.sh" + +timeout --signal=TERM --kill-after=5s 10s bash -c ' + target_pid=$$ + ( + while [[ ! -s "$PID_FILE" ]]; do + sleep 0.05 + done + sleep 0.1 + kill -INT "$target_pid" + ) & + exec "$START_SCRIPT" -d false +' +ACTUAL_EXIT=$? + +if [[ "$ACTUAL_EXIT" -ne 130 ]]; then + echo "FAIL: expected exit 130 after SIGINT, got $ACTUAL_EXIT" + exit 1 +fi + +if [[ -s "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then + echo "FAIL: server child is still running after SIGINT" + exit 1 +fi + +echo "PASS: SIGINT terminates the foreground wrapper and its server child" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-store.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-store.sh index 9ff324ae29..6e1bc0394d 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-store.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph-store.sh @@ -15,12 +15,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# test-start-hugegraph-store.sh — Tests for start-hugegraph-store.sh foreground mode fix +# test-start-hugegraph-store.sh — Foreground mode and GC option regression tests # -# Baseline (unmodified code): Test 1 PASS — Tests 2, 3, 4 FAIL -# After chunk 3 fix: All 4 tests PASS -# -# Usage: ./test-start-hugegraph-store.sh [path-to-store-dist-root] +# Usage: ./test-start-hugegraph-store.sh [path-to-store-dist-root] [--gc-only] set -uo pipefail @@ -34,6 +31,8 @@ PD_URL="http://localhost:8620" STARTUP_WAIT=60 # seconds to wait for Store HTTP to respond SETTLE_WAIT=5 # seconds after kill before checking exit WAIT_TIMEOUT=30 # seconds for wait_script_exit timeout +RUNTIME_LOG_CHECK="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/check-java-runtime-logs.sh" +ACTIVE_RUN=false PASS=0 FAIL=0 @@ -49,6 +48,13 @@ fail() { echo -e "${RED} FAIL${NC} $1"; ERRORS+=("$1"); FAIL=$((FAIL + 1)); } info() { echo -e "${YELLOW} ....${NC} $1"; } section() { echo ""; echo "── $1 ──"; } +start_service() { + mkdir -p "$STORE_ROOT/logs" + ACTIVE_RUN=true + "$START_SCRIPT" "$@" > "$STORE_ROOT/logs/launcher.out" 2>&1 & + SCRIPT_PID=$! +} + cleanup() { info "Cleaning up..." "$STOP_SCRIPT" >/dev/null 2>&1 || true @@ -56,7 +62,6 @@ cleanup() { kill "$(cat "$PID_FILE")" 2>/dev/null || true fi rm -f "$PID_FILE" - rm -rf "$STORE_ROOT/logs/" # kill anything holding Store ports (8520 REST, 8510 raft, 8500 gRPC) if [[ "$(uname)" == "Darwin" ]]; then local port pids pid @@ -76,6 +81,15 @@ cleanup() { echo "Warning: Neither lsof nor fuser available for Store port cleanup" fi sleep 3 + if [[ "$ACTIVE_RUN" == true ]]; then + if bash "$RUNTIME_LOG_CHECK" "$STORE_ROOT/logs"; then + pass "startup logs contain no Java runtime compatibility errors" + else + fail "startup logs contain Java runtime compatibility errors or could not be checked" + fi + ACTIVE_RUN=false + fi + rm -rf "$STORE_ROOT/logs/" } wait_for_store() { @@ -115,7 +129,7 @@ wait_script_exit() { # ── preflight ───────────────────────────────────────────────────────────────── echo "" -echo "start-hugegraph-store.sh chunk 3 test suite" +echo "start-hugegraph-store.sh test suite" echo "root: $STORE_ROOT" echo "" @@ -125,10 +139,134 @@ if [[ ! -f "$START_SCRIPT" ]]; then exit 1 fi +# Exercise the shipped launcher in an isolated fixture, without JNI or a service. +# This checks shell selection/argv only; the tests below check real JVM startup. +check_gc_options() { + local fixture option capture status jvm_options + fixture=$(mktemp -d) || { fail "could not create GC test fixture"; return 1; } + fixture=$(cd "$fixture" && pwd -P) + if ! mkdir -p "$fixture/bin" "$fixture/conf" "$fixture/lib" "$fixture/jdk/bin" || + ! cp "$START_SCRIPT" "$BIN/util.sh" "$BIN/preload-topling.sh" \ + "$BIN/jvm-module.options" "$fixture/bin/"; then + fail "could not prepare GC test fixture" + rm -rf "$fixture" + return 1 + fi + # Make default memory selection independent of host memory and OS. + printf '%s\n' 'calc_xmx() { echo 1024; }' >> "$fixture/bin/util.sh" + touch "$fixture/lib/hg-store-node-test.jar" + cat > "$fixture/jdk/bin/java" <<'JAVA' +#!/bin/bash +if [[ "$1" == "-version" ]]; then + echo 'openjdk version "17.0.1"' >&2 +else + printf '%s\n' "$@" > "$CAPTURE_FILE" + for arg in "$@"; do + if [[ "$arg" == @* ]]; then + sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d' "${arg#@}" >> "$CAPTURE_FILE" + fi + done +fi +JAVA + # Avoid the Store allocator download, which is unrelated to GC selection. + printf '%s\n' '#!/bin/sh' 'echo launcher-test' > "$fixture/jdk/bin/uname" + chmod +x "$fixture/jdk/bin/"* "$fixture/bin/start-hugegraph-store.sh" + for option in default custom-java-options user-options default-serial default-zgc \ + g1 G1 zgc ZGC invalid missing-module-options; do + capture="$fixture/$option.args" + local -a args=(-d false) + jvm_options="-Xms64m -Xmx64m" + case "$option" in + default) jvm_options="" ;; + custom-java-options) jvm_options="$jvm_options -Dlauncher.test.custom=true" ;; + user-options) jvm_options=""; args+=(-j "-Xmx96m -Dlauncher.test=true") ;; + missing-module-options) rm "$fixture/bin/jvm-module.options" ;; + default-serial) jvm_options="$jvm_options -XX:+UseSerialGC" ;; + default-zgc) jvm_options="$jvm_options -XX:+UseZGC" ;; + *) args+=(-g "$option") ;; + esac + env -u GC_OPTION -u JAVA_TOOL_OPTIONS -u LD_PRELOAD \ + -u TOPLINGDB_ROCKSDB_PROVIDER -u TOPLING_RUNTIME_CLASSPATH -u TOPLING_ACTIVE_NATIVE \ + JAVA_HOME="$fixture/jdk" JAVA_OPTIONS="$jvm_options" \ + OPEN_TELEMETRY=false STDOUT_MODE=true CAPTURE_FILE="$capture" \ + PATH="$fixture/jdk/bin:$PATH" \ + bash "$fixture/bin/start-hugegraph-store.sh" "${args[@]}" > "$fixture/output" 2>&1 + status=$? + if [[ "$option" == missing-module-options ]]; then + if [[ $status -eq 1 && ! -e "$capture" ]] && + grep -Fq 'JVM module options file is missing or unreadable' "$fixture/output"; then + pass "missing JVM module options are rejected before application launch" + else + fail "missing JVM module options were not rejected correctly" + fi + elif [[ "$option" == invalid ]]; then + if [[ $status -eq 1 && ! -e "$capture" ]] && \ + grep -Fq "Unrecognized gc option: 'invalid'" "$fixture/logs/hugegraph-store-server.log"; then + pass "invalid GC is rejected before application launch" + else + fail "invalid GC was not rejected correctly" + fi + elif [[ $status -ne 0 || ! -s "$capture" ]]; then + fail "GC '$option' did not reach Java (exit $status)" + elif ! grep -Fxq -- "@$fixture/bin/jvm-module.options" "$capture" || + ! grep -Fxq -- '--add-opens=java.base/java.lang=ALL-UNNAMED' "$capture"; then + fail "launcher '$option' dropped mandatory JVM module access" + elif [[ "$option" == custom-java-options ]] && + ! grep -Fxq -- '-Dlauncher.test.custom=true' "$capture"; then + fail "launcher dropped custom JAVA_OPTIONS" + elif [[ "$option" == user-options ]] && + ! grep -Fxq -- '-Dlauncher.test=true' "$capture"; then + fail "launcher dropped custom -j options" + elif [[ "$option" == zgc || "$option" == ZGC ]]; then + if grep -Fxq -- '-XX:+UseZGC' "$capture" && \ + ! grep -Fxq -- '-XX:InitiatingHeapOccupancyPercent=50' "$capture"; then + pass "GC '$option' selects ZGC without G1 tuning" + else + fail "GC '$option' selected incorrect JVM options" + fi + elif [[ "$option" == g1 || "$option" == G1 ]]; then + if grep -Fxq -- '-XX:+UseG1GC' "$capture" && + grep -Fxq -- '-XX:InitiatingHeapOccupancyPercent=50' "$capture" && + ! grep -Fxq -- '-XX:+UseZGC' "$capture"; then + pass "GC '$option' explicitly selects G1" + else + fail "GC '$option' did not explicitly select G1" + fi + elif grep -Fxq -- '-XX:InitiatingHeapOccupancyPercent=50' "$capture" && + ! grep -Fxq -- '-XX:+UseG1GC' "$capture"; then + if [[ "$option" == default-serial ]] && + ! grep -Fxq -- '-XX:+UseSerialGC' "$capture"; then + fail "default GC selection dropped the caller's Serial GC" + elif [[ "$option" == default-zgc ]] && + ! grep -Fxq -- '-XX:+UseZGC' "$capture"; then + fail "default GC selection dropped the caller's ZGC" + elif [[ "$option" == default ]] && grep -Fxq -- '-XX:+UseZGC' "$capture"; then + fail "default GC selection unexpectedly enabled ZGC" + else + pass "GC '$option' preserves JVM/caller collector selection" + fi + else + fail "GC '$option' selected incorrect JVM options" + fi + done + rm -rf "$fixture" +} + +section "GC option selection (isolated launcher fixture)" +check_gc_options +if [[ "${2:-}" == --gc-only ]]; then + [[ $FAIL -eq 0 ]] && exit 0 || exit 1 +fi + +if [[ ! -r "$RUNTIME_LOG_CHECK" ]]; then + echo "ERROR: runtime log checker not found: $RUNTIME_LOG_CHECK" + exit 1 +fi + if [[ "$(uname)" == "Darwin" ]]; then - _prereq_tools="lsof curl java" + _prereq_tools="lsof curl java jcmd" else - _prereq_tools="fuser curl java" + _prereq_tools="fuser curl java jcmd" fi for tool in $_prereq_tools; do if ! command -v "$tool" >/dev/null 2>&1; then @@ -163,8 +301,7 @@ cleanup section "Test 1 — daemon mode regression" info "Starting in daemon mode (no -d flag, default behavior)..." -"$START_SCRIPT" >/dev/null 2>&1 & -SCRIPT_PID=$! +start_service sleep 5 if ! ps -p "$SCRIPT_PID" >/dev/null 2>&1; then @@ -206,9 +343,8 @@ cleanup section "Test 2 — foreground mode blocks until Java exits" -info "Starting in foreground mode (-d false)..." -"$START_SCRIPT" -d false >/dev/null 2>&1 & -SCRIPT_PID=$! +info "Starting in foreground mode (-d false -g g1)..." +start_service -d false -g g1 info "Waiting up to ${STARTUP_WAIT}s for Store to come up..." if wait_for_store; then @@ -232,6 +368,17 @@ else info "pid file not written (expected before chunk 3 fix)" fi +if [[ -n "$FG_PID" ]] && ps -p "$FG_PID" >/dev/null 2>&1; then + if GC_FLAGS=$(jcmd "$FG_PID" VM.flags 2>&1) && + grep -Fq -- '-XX:+UseG1GC' <<<"$GC_FLAGS"; then + pass "explicit -g g1 runs the JVM with G1GC" + else + fail "explicit -g g1 did not run the JVM with G1GC: $GC_FLAGS" + fi +else + fail "no running JVM available to verify explicit -g g1" +fi + if [[ -n "$FG_PID" ]]; then kill "$FG_PID" 2>/dev/null || true else @@ -251,9 +398,8 @@ cleanup section "Test 3 — exit code propagates from Java" -info "Starting in foreground mode (-d false)..." -"$START_SCRIPT" -d false >/dev/null 2>&1 & -SCRIPT_PID=$! +info "Starting in foreground mode (-d false -g ZGC)..." +start_service -d false -g ZGC info "Waiting up to ${STARTUP_WAIT}s for Store..." if wait_for_store; then @@ -307,8 +453,7 @@ cleanup section "Test 4 — SIGTERM forwarded to Java in foreground mode" info "Starting in foreground mode (-d false)..." -"$START_SCRIPT" -d false >/dev/null 2>&1 & -SCRIPT_PID=$! +start_service -d false info "Waiting up to ${STARTUP_WAIT}s for Store..." if wait_for_store; then diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph.sh index 9f0bcfaa63..1f54b81f90 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-hugegraph.sh @@ -30,6 +30,9 @@ set -uo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/ci-service-utils.sh" + HUGEGRAPH_ROOT="${1:-$(pwd)}" BIN="$HUGEGRAPH_ROOT/bin" START_SCRIPT="$BIN/start-hugegraph.sh" @@ -42,6 +45,8 @@ WAIT_TIMEOUT=30 # seconds for timeout on wait calls PASS=0 FAIL=0 ERRORS=() +STARTUP_LOG="$HUGEGRAPH_ROOT/logs/startup-test.log" +RUNTIME_STARTED=false GREEN='\033[0;32m' RED='\033[0;31m' @@ -99,6 +104,23 @@ cleanup() { fi sleep 3 crontab -l 2>/dev/null | grep -v monitor-hugegraph | crontab - 2>/dev/null || true + if [[ "$RUNTIME_STARTED" == true ]]; then + local logs=("$STARTUP_LOG") + if [[ "${STDOUT_MODE:-false}" != true ]]; then + logs+=("$HUGEGRAPH_ROOT/logs/hugegraph-server.log" + "$HUGEGRAPH_ROOT/logs/hugegraph-server-stdout.log") + fi + if bash "$SCRIPT_DIR/check-java-runtime-logs.sh" "${logs[@]}"; then + pass "startup has no Java runtime compatibility errors" + else + fail "startup log contains a compatibility error or is unavailable" + fi + RUNTIME_STARTED=false + fi + # Each positive start must be checked independently of previous runs. + mkdir -p "$HUGEGRAPH_ROOT/logs" + rm -f "$STARTUP_LOG" "$HUGEGRAPH_ROOT/logs/hugegraph-server.log" \ + "$HUGEGRAPH_ROOT/logs/hugegraph-server-stdout.log" } # Wait until server HTTP endpoint responds (200 or 401) or timeout @@ -192,9 +214,15 @@ fi # Run init-store.sh once if RocksDB has not been initialized yet if [[ ! -d "$HUGEGRAPH_ROOT/rocksdb-data" ]]; then info "RocksDB not initialized — running init-store.sh..." - if "$BIN/init-store.sh" >/dev/null 2>&1; then + mkdir -p "$HUGEGRAPH_ROOT/logs" + INIT_LOG="$HUGEGRAPH_ROOT/logs/startup-init.log" + if "$BIN/init-store.sh" >"$INIT_LOG" 2>&1; then + if ! bash "$SCRIPT_DIR/check-java-runtime-logs.sh" "$INIT_LOG"; then + exit 1 + fi pass "init-store.sh completed successfully" else + cat "$INIT_LOG" echo -e "${RED}ERROR:${NC} init-store.sh failed. Cannot run tests." exit 1 fi @@ -209,7 +237,8 @@ cleanup section "Test 1 — daemon mode regression" info "Starting in daemon mode (waiting up to ${STARTUP_WAIT}s for HTTP)..." -"$START_SCRIPT" -d true -t "$STARTUP_WAIT" >/dev/null 2>&1 +RUNTIME_STARTED=true +"$START_SCRIPT" -d true -t "$STARTUP_WAIT" >"$STARTUP_LOG" 2>&1 EXIT_CODE=$? if [[ $EXIT_CODE -eq 0 ]]; then @@ -256,7 +285,8 @@ cleanup section "Test 2 — foreground mode blocks until Java exits" info "Starting in foreground mode (backgrounded for observation)..." -"$START_SCRIPT" -d false >/dev/null 2>&1 & +RUNTIME_STARTED=true +"$START_SCRIPT" -d false >"$STARTUP_LOG" 2>&1 & SCRIPT_PID=$! info "Waiting up to ${STARTUP_WAIT}s for server to come up..." @@ -311,7 +341,8 @@ cleanup section "Test 3 — foreground PID tracking and exit propagation" info "Starting in foreground mode..." -"$START_SCRIPT" -d false >/dev/null 2>&1 & +RUNTIME_STARTED=true +"$START_SCRIPT" -d false >"$STARTUP_LOG" 2>&1 & SCRIPT_PID=$! info "Waiting up to ${STARTUP_WAIT}s for server..." @@ -369,7 +400,8 @@ info "Clearing crontab..." crontab -l 2>/dev/null | grep -v monitor-hugegraph | crontab - 2>/dev/null || true info "Starting in daemon mode with -m true (waiting up to ${STARTUP_WAIT}s)..." -"$START_SCRIPT" -d true -m true -t "$STARTUP_WAIT" >/dev/null 2>&1 +RUNTIME_STARTED=true +"$START_SCRIPT" -d true -m true -t "$STARTUP_WAIT" >"$STARTUP_LOG" 2>&1 EXIT_CODE=$? if [[ $EXIT_CODE -eq 0 ]]; then @@ -385,7 +417,8 @@ else fi info "Stopping server..." -"$STOP_SCRIPT" -m false >/dev/null 2>&1 || true +# Stop the registered monitor before shutdown so cron cannot restart this fixture. +"$STOP_SCRIPT" >/dev/null 2>&1 || true cleanup @@ -394,7 +427,8 @@ cleanup section "Test 5 — SIGTERM forwarded to Java in foreground mode" info "Starting in foreground mode..." -"$START_SCRIPT" -d false >/dev/null 2>&1 & +RUNTIME_STARTED=true +"$START_SCRIPT" -d false >"$STARTUP_LOG" 2>&1 & SCRIPT_PID=$! info "Waiting up to ${STARTUP_WAIT}s for server..." @@ -411,19 +445,34 @@ if [[ -z "$FG_PID" ]]; then fail "could not get Java PID — skipping signal forwarding check" kill "$SCRIPT_PID" 2>/dev/null || true else + # A monitor restart can overwrite the shared pid file. Verify that this + # assertion observes the wrapper's own child before testing signal forwarding. + FG_PARENT_PID=$(ps -o ppid= -p "$FG_PID" 2>/dev/null | tr -d '[:space:]') || FG_PARENT_PID="" + FG_PID_OWNED=false + if [[ "$FG_PARENT_PID" == "$SCRIPT_PID" ]]; then + FG_PID_OWNED=true + else + fail "pid file PID $FG_PID is not a child of wrapper $SCRIPT_PID (parent $FG_PARENT_PID)" + ps -o pid=,ppid=,stat= -p "$SCRIPT_PID,$FG_PID" 2>/dev/null || true + [[ ! -f "$HUGEGRAPH_ROOT/logs/monitor.log" ]] || tail -n 80 "$HUGEGRAPH_ROOT/logs/monitor.log" + fi + info "Sending SIGTERM to wrapper script (pid $SCRIPT_PID)..." kill -TERM "$SCRIPT_PID" 2>/dev/null || true wait_script_exit "$SCRIPT_PID" ACTUAL_EXIT=$? - # If the trap fired correctly, the wrapper's `wait $PID` already reaped Java. - # If wait_script_exit timed out (killer fired), Java may still be running — also a failure. - if ! ps -p "$FG_PID" >/dev/null 2>&1; then - pass "Java process terminated after SIGTERM sent to wrapper" - else - fail "Java process still running after SIGTERM — signal not forwarded" - kill "$FG_PID" 2>/dev/null || true + # Allow a bounded shutdown window and treat a zombie as already terminated. + # If wait_script_exit timed out, a live Java process remains a failure. + if [[ "$FG_PID_OWNED" == true ]]; then + if wait_for_process_exit "$FG_PID" "$SETTLE_WAIT"; then + pass "Java process terminated after SIGTERM sent to wrapper" + else + fail "Java process still running after SIGTERM — signal not forwarded" + ps -o pid=,ppid=,stat= -p "$SCRIPT_PID,$FG_PID" 2>/dev/null || true + [[ ! -f "$HUGEGRAPH_ROOT/logs/monitor.log" ]] || tail -n 80 "$HUGEGRAPH_ROOT/logs/monitor.log" + fi fi if [[ $ACTUAL_EXIT -ne 0 ]]; then diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-server.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-server.sh new file mode 100755 index 0000000000..fa9f1b2858 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-start-server.sh @@ -0,0 +1,95 @@ +#!/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. +# +# Verifies that start-server.sh can run twice on one configuration, as +# run-api-test.sh does: the second start must leave the config unchanged and +# each line the script appends must be there exactly once, since a key set +# twice is read back as a list (usePD as '[true, true]'). + +set -uo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +START_SERVER="${1:-${SCRIPT_DIR}/start-server.sh}" +STATIC_DIR=$(cd "${SCRIPT_DIR}/../static" && pwd) +TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/start-server-test.XXXXXX") +FAILURES=0 + +trap 'rm -rf "${TMP_DIR}"' EXIT + +fail() { + echo "FAIL: $1" >&2 + FAILURES=$((FAILURES + 1)) +} + +start_server() { + if ! bash "$START_SERVER" "$server_dir" "$backend" > "${TMP_DIR}/start.log" 2>&1; then + cat "${TMP_DIR}/start.log" >&2 + fail "${backend}: start-server.sh exited non-zero" + return 1 + fi +} + +# The lines that set the key must be exactly key=value, or none if value is empty +assert_key() { + local file=$1 key=$2 value=$3 actual + actual=$(awk -v prefix="${key}=" 'index($0, prefix) == 1' "$file" | paste -sd ' ' -) + [[ "$actual" == "${value:+${key}=${value}}" ]] || + fail "${backend}: ${file##*/} has '${actual}' for ${key}, expected '${value:+${key}=${value}}'" +} + +if [[ ! -f "$START_SERVER" ]]; then + echo "ERROR: start script not found: $START_SERVER" + exit 2 +fi + +for backend in hstore hbase rocksdb memory; do + # A copy of the packaged files, with the backend initialization and the + # Server launch stubbed + server_dir="${TMP_DIR}/${backend}" + mkdir -p "${server_dir}/bin" "${server_dir}/conf/graphs" + cp "${STATIC_DIR}/bin/util.sh" "${server_dir}/bin/" + cp "${STATIC_DIR}/conf/rest-server.properties" "${STATIC_DIR}/conf/gremlin-server.yaml" "${server_dir}/conf/" + cp "${STATIC_DIR}/conf/graphs/hugegraph.properties" "${server_dir}/conf/graphs/" + printf '#!/bin/bash\ncat >/dev/null\n' > "${server_dir}/bin/init-store.sh" + printf '#!/bin/bash\n' > "${server_dir}/bin/start-hugegraph.sh" + chmod +x "${server_dir}/bin/init-store.sh" "${server_dir}/bin/start-hugegraph.sh" + + start_server || continue + cp -R "${server_dir}/conf" "${TMP_DIR}/${backend}-first-start" + start_server || continue + (cd "$TMP_DIR" && diff -ru "${backend}-first-start" "${backend}/conf") >&2 || + fail "${backend}: the second start changed the config as shown above" + + use_pd="" + hbase_timeout="" + case "$backend" in + hstore) use_pd=true ;; + hbase) hbase_timeout=200 ;; + esac + rest_conf="${server_dir}/conf/rest-server.properties" + assert_key "$rest_conf" usePD "$use_pd" + assert_key "$rest_conf" restserver.request_timeout "$hbase_timeout" + assert_key "$rest_conf" gremlinserver.timeout "$hbase_timeout" + assert_key "${server_dir}/conf/graphs/hugegraph.properties" schema.sync_deletion true +done + +if [[ "$FAILURES" -gt 0 ]]; then + echo "FAIL: ${FAILURES} check(s) failed after two starts" >&2 + exit 1 +fi + +echo "PASS: start-server.sh run twice sets each config line once for hstore, hbase, rocksdb and memory" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-stop-hugegraph-store.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-stop-hugegraph-store.sh new file mode 100755 index 0000000000..5d05ab6ef2 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-stop-hugegraph-store.sh @@ -0,0 +1,42 @@ +#!/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. +# +# Verify stop failure propagation and PID retention without signalling real processes. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../.." && pwd)" +SCRIPT="$ROOT/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh" +FIXTURE=$(mktemp -d) +trap 'rm -rf "$FIXTURE"' EXIT +mkdir -p "$FIXTURE/bin" +cp "$SCRIPT" "$FIXTURE/bin/stop-hugegraph-store.sh" +cat > "$FIXTURE/bin/util.sh" <<'UTIL' +kill_process_and_wait() { + [[ "$1" == HugeGraphStoreServer && "$2" == 12345 && "$3" == 30 ]] || return 99 + return "$STOP_RESULT" +} +UTIL +printf '12345\n' > "$FIXTURE/bin/pid" +if STOP_RESULT=1 bash "$FIXTURE/bin/stop-hugegraph-store.sh"; then + echo "Stop timeout incorrectly returned success" >&2 + exit 1 +fi +[[ "$(cat "$FIXTURE/bin/pid")" == 12345 ]] +STOP_RESULT=0 bash "$FIXTURE/bin/stop-hugegraph-store.sh" +[[ ! -e "$FIXTURE/bin/pid" ]] +STOP_RESULT=0 bash "$FIXTURE/bin/stop-hugegraph-store.sh" +echo "store-stop-contract-ok" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-tinkerpop-reports.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-tinkerpop-reports.sh new file mode 100644 index 0000000000..e3a8a88fcc --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-tinkerpop-reports.sh @@ -0,0 +1,100 @@ +#!/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. +# + +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RUNNER="${SCRIPT_DIR}/run-tinkerpop-test.sh" +TEST_DIR=$(mktemp -d) +trap 'rm -rf "$TEST_DIR"' EXIT + +mkdir -p "$TEST_DIR/bin" +cat > "$TEST_DIR/bin/mvn" <<'MOCK' +#!/bin/bash +set -eu +REPORT_DIR=hugegraph-server/hugegraph-test/target/surefire-reports +mkdir -p "$REPORT_DIR" +if [[ -n "$TEST_REPORT" ]]; then + for suite in StructureStandardTest ProcessStandardTest HugeGraphFeatureTest HugeGraphProviderLifecycleTest; do + printf '%s\n' "$TEST_REPORT" > "$REPORT_DIR/TEST-org.apache.hugegraph.tinkerpop.$suite.xml" + done +fi +MOCK +chmod +x "$TEST_DIR/bin/mvn" + +check_report() { + local name=$1 + local report=$2 + local expected=$3 + local suite=$4 + local actual=0 + + TEST_REPORT="$report" PATH="$TEST_DIR/bin:$PATH" \ + bash "$RUNNER" hstore "$suite" > "$TEST_DIR/$name.log" 2>&1 || actual=1 + if [[ "$actual" != "$expected" ]]; then + cat "$TEST_DIR/$name.log" + echo "FAIL: $name returned $actual, expected $expected" + exit 1 + fi +} + +# Exercise the actual workflow expressions against representative event branches. +python3 - "$SCRIPT_DIR" <<'PYBRANCH' +from pathlib import Path +import re +import sys +from types import SimpleNamespace + +root = Path(sys.argv[1]).resolve().parents[4] +for filename in ("server-tests.yml", "pd-store-ci.yml"): + source = (root / ".github/workflows" / filename).read_text() + assert "SKIP_TINKERPOP_FOR_CURRENT_PR" not in source, filename + expression = re.search(r"RUN_TINKERPOP_TESTS: >-\s*\$\{\{(.*?)\}\}", source, re.S).group(1) + expression = " ".join(expression.split()).replace("||", "or") + cases = [ + ("123/merge", "upgrade/1.8.0", "master", False), + ("master", "", "", False), + ("123/merge", "task/tp381-3-upgrade-validation", "master", False), + ("123/merge", "task/tinkerpop-3.7-upgrade", "master", False), + ("123/merge", "task/gsoc-phase2-java17", "master", False), + ] + for prefix in ("release-", "test-", "tinkerpop-"): + cases.extend([(prefix + "validation", "", "", True), + ("123/merge", prefix + "validation", "master", True), + ("123/merge", "feature", prefix + "validation", True)]) + for ref, head, base, expected in cases: + github = SimpleNamespace(ref_name=ref, head_ref=head, base_ref=base) + actual = eval(expression, {"__builtins__": {}}, + {"github": github, "startsWith": str.startswith}) + assert actual is expected, (filename, ref, head, base, actual) +print("PASS: TP branch gates exclude ordinary upgrades and old task exceptions") +PYBRANCH + +cd "$TEST_DIR" +for suite in structure process process-standard process-feature tinkerpop; do + check_report executed '' 0 "$suite" + check_report partially-skipped '' 0 "$suite" + check_report all-skipped '' 1 "$suite" + check_report empty '' 1 "$suite" + check_report malformed '' 0 "$suite" + check_report missing '' 1 "$suite" +done + +echo "PASS: TinkerPop report gate requires executed tests and rejects stale reports" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-topling-selection.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-topling-selection.sh new file mode 100755 index 0000000000..de9624cf1f --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-topling-selection.sh @@ -0,0 +1,128 @@ +#!/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. +# +# Selection-only fixtures; real JNI preparation and live services are separate gates. +set -Eeuo pipefail +SOURCE_BIN="$(cd "$(dirname "${BASH_SOURCE[0]}")/../static/bin" && pwd)" +TEST_ROOT=$(mktemp -d) +trap 'rm -rf "$TEST_ROOT"' EXIT +mkdir -p "$TEST_ROOT/fake-bin" +printf '%s\n' '#!/bin/sh' 'case "$1" in -s) echo Linux ;; -m) echo x86_64 ;; esac' > "$TEST_ROOT/fake-bin/uname" +chmod +x "$TEST_ROOT/fake-bin/uname" +for component in server pd store; do + mkdir -p "$TEST_ROOT/$component/bin" "$TEST_ROOT/$component/conf" + cp "$SOURCE_BIN/preload-topling.sh" "$TEST_ROOT/$component/bin/" + # Default startup works without prepared assets and never selects optional JNI. + env -u TOPLINGDB_ROCKSDB_PROVIDER -u LD_PRELOAD -u TOPLING_ACTIVE_NATIVE \ + -u TOPLING_RUNTIME_CLASSPATH bash -euc 'source "$1"; test -z "${TOPLING_RUNTIME_CLASSPATH:-}"' \ + _ "$TEST_ROOT/$component/bin/preload-topling.sh" + mkdir "$TEST_ROOT/$component/topling" + touch "$TEST_ROOT/$component/topling/rocksdbjni.jar" "$TEST_ROOT/$component/topling/librocksdbjni-linux64.so" + (cd "$TEST_ROOT/$component/topling" && sha256sum rocksdbjni.jar librocksdbjni-linux64.so > easy-migrate.sha256) + printf '%s\n' 'http: {auto_start_http: false}' > "$TEST_ROOT/$component/conf/toplingdb.yaml" +done +for order in 'pd store' 'store pd' 'server pd'; do + read -r first second <<< "$order" + PATH="$TEST_ROOT/fake-bin:$PATH" bash -ec ' + export TOPLINGDB_ROCKSDB_PROVIDER=topling + unset TOPLINGDB_EASY_MIGRATE_CONF TOPLING_AUTO_MIGRATE_CONF + export LD_LIBRARY_PATH=/unrelated CLASSPATH=unrelated.jar + source "$1/$2/bin/preload-topling.sh" + bash -ec '"'"'source "$1/$2/bin/preload-topling.sh" + test "$TOPLINGDB_EASY_MIGRATE_CONF" = "$1/$2/conf/toplingdb.yaml" + '"'"' _ "$1" "$3" + # Reproduce the Server launcher exporting its selected JAR to children. + export CLASSPATH="$TOPLING_RUNTIME_CLASSPATH::unrelated.jar::" + TOPLINGDB_ROCKSDB_PROVIDER=rocksdb bash -ec '"'"'source "$1/$2/bin/preload-topling.sh" + test "$CLASSPATH" = :unrelated.jar:: + test -z "${TOPLING_RUNTIME_CLASSPATH:-}" + '"'"' _ "$1" "$3" + source "$1/$3/bin/preload-topling.sh" + test "$CLASSPATH" = :unrelated.jar:: + test "$TOPLING_RUNTIME_CLASSPATH" = "$1/$3/topling/rocksdbjni.jar" + test "$TOPLINGDB_EASY_MIGRATE_CONF" = "$1/$3/conf/toplingdb.yaml" + export TOPLINGDB_EASY_MIGRATE_CONF="$1/$2/conf/toplingdb.yaml" + source "$1/$3/bin/preload-topling.sh" + test "$TOPLINGDB_EASY_MIGRATE_CONF" = "$1/$2/conf/toplingdb.yaml" + export CLASSPATH="$TOPLING_RUNTIME_CLASSPATH:$CLASSPATH" + export TOPLINGDB_ROCKSDB_PROVIDER=rocksdb + source "$1/$2/bin/preload-topling.sh" + test -z "${TOPLING_RUNTIME_CLASSPATH:-}" + test "$TOPLINGDB_EASY_MIGRATE_CONF" = "$1/$2/conf/toplingdb.yaml" + test "$LD_LIBRARY_PATH" = /unrelated + test "$CLASSPATH" = :unrelated.jar:: + ' _ "$TEST_ROOT" "$first" "$second" +done +# LD_PRELOAD accepts colon and space separators. Keep caller entries after our JNI. +for inherited in '/unrelated/liballoc.so:/unrelated/libtrace.so' \ + '/unrelated/liballoc.so /unrelated/libtrace.so'; do + PATH="$TEST_ROOT/fake-bin:$PATH" bash -ec ' + unset TOPLING_ACTIVE_NATIVE TOPLING_RUNTIME_CLASSPATH + export TOPLINGDB_ROCKSDB_PROVIDER=topling LD_PRELOAD="$2" + source "$1/server/bin/preload-topling.sh" + test "$LD_PRELOAD" = "$1/server/topling/librocksdbjni-linux64.so:$2" + # Switching components must replace only the runtime we previously selected. + source "$1/pd/bin/preload-topling.sh" + test "$LD_PRELOAD" = "$1/pd/topling/librocksdbjni-linux64.so:$2" + export TOPLINGDB_ROCKSDB_PROVIDER=rocksdb + source "$1/store/bin/preload-topling.sh" + test "$LD_PRELOAD" = "$2" + ' _ "$TEST_ROOT" "$inherited" +done +# Reject competing JNI before adding the selected runtime; unrelated entries survive. +for provider in unset rocksdb topling; do + for inherited in '/other/librocksdbjni.so:/unrelated/libtrace.so' \ + '/unrelated/libtrace.so /other/librocksdbjni-linux64.so' \ + '/other/libfrocksdbjni.so' "$TEST_ROOT/server/topling/librocksdbjni-linux64.so"; do + if [ "$provider" = topling ] && [ "$inherited" = "$TEST_ROOT/server/topling/librocksdbjni-linux64.so" ]; then + continue + fi + if PATH="$TEST_ROOT/fake-bin:$PATH" bash -ec ' + unset TOPLING_ACTIVE_NATIVE TOPLING_RUNTIME_CLASSPATH + export LD_PRELOAD="$2" + if [ "$3" = unset ]; then + unset TOPLINGDB_ROCKSDB_PROVIDER + else + export TOPLINGDB_ROCKSDB_PROVIDER="$3" + fi + source "$1/server/bin/preload-topling.sh" + ' _ "$TEST_ROOT" "$inherited" "$provider" > "$TEST_ROOT/error" 2>&1; then + echo "FAIL: competing RocksDB JNI was accepted in $provider mode" >&2; exit 1 + fi + grep -q 'competing RocksDB JNI in LD_PRELOAD' "$TEST_ROOT/error" + done +done +# Older or modified preparations must be verified again before launch. +for state in missing stale; do + receipt="$TEST_ROOT/server/topling/easy-migrate.sha256" + if [ "$state" = missing ]; then + mv "$receipt" "$receipt.saved" + else + printf 'changed' >> "$TEST_ROOT/server/topling/rocksdbjni.jar" + fi + if PATH="$TEST_ROOT/fake-bin:$PATH" TOPLINGDB_ROCKSDB_PROVIDER=topling \ + bash -ec 'source "$1"' _ "$TEST_ROOT/server/bin/preload-topling.sh" > "$TEST_ROOT/error" 2>&1; then + echo 'FAIL: unverified preparation was accepted' >&2; exit 1 + fi + grep -q 'unverified EasyMigrate runtime' "$TEST_ROOT/error" + [ "$state" != missing ] || mv "$receipt.saved" "$receipt" +done +if TOPLINGDB_ROCKSDB_PROVIDER=invalid bash -ec 'source "$1"' _ \ + "$TEST_ROOT/server/bin/preload-topling.sh" > "$TEST_ROOT/error" 2>&1; then + echo 'FAIL: invalid provider was accepted' >&2; exit 1 +fi +echo 'PASS: standard default, explicit selection, component and inherited configuration isolation' diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh index 6d7c4ce8d0..046f255d50 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh @@ -52,6 +52,7 @@ assert_output() { } assert_contract() { + local expected_timeout="${1:-300}" ! grep -q '/v1/health' "${CALL_LOG}" || \ fail "/v1/health must not gate readiness" [[ -s "${ARGS_LOG}" ]] || fail "curl was not called" @@ -68,11 +69,15 @@ assert_contract() { if grep -Fv -- '--max-time 3' "${ARGS_LOG}" | grep -q .; then fail "per-peer maximum timeout was not preserved" fi - assert_equal "outer timeout" "300s" "$(cat "${TIMEOUT_LOG}")" + assert_equal "outer timeout" "${expected_timeout}s" "$(cat "${TIMEOUT_LOG}")" } run_case() { local scenario="$1" peers="$2" abort_after="$3" password="${4:-test-password}" + local timeout_args=() + if [[ "$#" -ge 5 ]]; then + timeout_args=("WAIT_STORAGE_TIMEOUT_S=$5") + fi : > "${CALL_LOG}" : > "${ARGS_LOG}" : > "${CONFIG_LOG}" @@ -80,7 +85,8 @@ run_case() { : > "${TIMEOUT_LOG}" : > "${DIST_ROOT}/conf/graphs/hugegraph.properties" - CASE_OUTPUT=$(env \ + CASE_OUTPUT=$(env -u WAIT_STORAGE_TIMEOUT_S \ + ${timeout_args[@]+"${timeout_args[@]}"} \ PATH="${MOCK_BIN}:${PATH}" \ MOCK_SCENARIO="${scenario}" \ MOCK_ABORT_AFTER="${abort_after}" \ @@ -309,4 +315,23 @@ assert_output "No pd.peers configured, skipping storage wait" fail "warned about an unused PD credential with no pd.peers configured" echo " PASS no credential warning without pd.peers" -echo "10 passed, 0 failed" +for storage_timeout in 1 600 86400; do + run_case "pd1-up" "pd0:8620,pd1:8620" 6 "test-password" "${storage_timeout}" + assert_equal "custom timeout ${storage_timeout} rc" "0" "${CASE_RC}" + assert_output "Timeout = ${storage_timeout}s" + assert_contract "${storage_timeout}" + echo " PASS custom timeout ${storage_timeout}" +done + +for storage_timeout in '' 0 -1 1.5 abc 86401 999999999999999999999; do + run_case "pd1-up" "pd0:8620,pd1:8620" 6 "test-password" "${storage_timeout}" + assert_equal "invalid timeout '${storage_timeout}' rc" "2" "${CASE_RC}" + assert_output "WAIT_STORAGE_TIMEOUT_S must be an integer from 1 to 86400 seconds" + [[ ! -s "${TIMEOUT_LOG}" && ! -s "${CALL_LOG}" ]] || \ + fail "invalid timeout started polling PD" + [[ ! -s "${DIST_ROOT}/conf/graphs/hugegraph.properties" ]] || \ + fail "invalid timeout modified the graph config" + echo " PASS invalid timeout '${storage_timeout}'" +done + +echo "20 passed, 0 failed" diff --git a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/cmd/StoreDumper.java b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/cmd/StoreDumper.java index 352ea0b02e..70be13b9a4 100644 --- a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/cmd/StoreDumper.java +++ b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/cmd/StoreDumper.java @@ -21,7 +21,7 @@ import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendStore; import org.apache.hugegraph.dist.RegisterUtil; diff --git a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGraphServer.java b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGraphServer.java index 1165fb1a93..3cbce0ff31 100644 --- a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGraphServer.java +++ b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGraphServer.java @@ -17,12 +17,14 @@ package org.apache.hugegraph.dist; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.config.ServerOptions; import org.apache.hugegraph.constant.ServiceConstant; import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.event.EventHub; import org.apache.hugegraph.meta.MetaManager; import org.apache.hugegraph.meta.PdMetaDriver; @@ -60,6 +62,7 @@ public HugeGraphServer(String gremlinServerConf, String restServerConf) GremlinServer gremlinServer = null; MemoryMonitor memoryMonitor = null; try { + initializeQueryClasses(); ConfigUtil.checkGremlinConfig(gremlinServerConf); HugeConfig restServerConfig = new HugeConfig(restServerConf); String graphsDir = restServerConfig.get(ServerOptions.GRAPHS); @@ -134,6 +137,12 @@ public HugeGraphServer(String gremlinServerConf, String restServerConf) this.memoryMonitor = memoryMonitor; } + static void initializeQueryClasses() { + // Gson reads java.version while ConditionQuery initializes. Complete + // this on the trusted startup thread before accepting Gremlin requests. + new ConditionQuery(HugeType.VERTEX); + } + private static void stopPreparedGremlinServer(GremlinServer server) { if (server == null) { return; diff --git a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGremlinServer.java b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGremlinServer.java index d03f8172d4..90319aef75 100644 --- a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGremlinServer.java +++ b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/HugeGremlinServer.java @@ -17,7 +17,7 @@ package org.apache.hugegraph.dist; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.auth.ContextGremlinServer; import org.apache.hugegraph.event.EventHub; import org.apache.hugegraph.util.ConfigUtil; diff --git a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/RegisterUtil.java b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/RegisterUtil.java index 44c074c6a1..4b862e822c 100644 --- a/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/RegisterUtil.java +++ b/hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/dist/RegisterUtil.java @@ -24,13 +24,12 @@ import org.apache.commons.configuration2.PropertiesConfiguration; import org.apache.commons.configuration2.builder.fluent.Configurations; import org.apache.commons.configuration2.ex.ConfigurationException; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.backend.serializer.SerializerFactory; import org.apache.hugegraph.backend.store.BackendProviderFactory; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.config.OptionSpace; -import org.apache.hugegraph.masterelection.RoleElectionOptions; import org.apache.hugegraph.plugin.HugeGraphPlugin; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; @@ -51,7 +50,6 @@ public class RegisterUtil { static { OptionSpace.register("core", CoreOptions.instance()); OptionSpace.register("dist", DistOptions.instance()); - OptionSpace.register("masterElection", RoleElectionOptions.instance()); } public static void registerBackends() { diff --git a/hugegraph-server/hugegraph-dist/src/test/java/org/apache/hugegraph/dist/ColdGremlinQueryTest.java b/hugegraph-server/hugegraph-dist/src/test/java/org/apache/hugegraph/dist/ColdGremlinQueryTest.java new file mode 100644 index 0000000000..9259a25734 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/test/java/org/apache/hugegraph/dist/ColdGremlinQueryTest.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.hugegraph.dist; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; + +import org.apache.commons.io.FileUtils; +import org.apache.hugegraph.HugeFactory; +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.security.HugeSecurityManager; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.util.Log; +import org.apache.tinkerpop.gremlin.groovy.jsr223.GremlinGroovyScriptEngine; +import org.apache.tinkerpop.gremlin.structure.T; +import org.junit.Assert; +import org.junit.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class ColdGremlinQueryTest { + + @Test + public void testEarlyInitializationOnConfigurationFailure() throws Exception { + Path sourceRoot = Path.of(System.getProperty("basedir", System.getProperty("user.dir"))) + .getParent().getParent(); + Path target = sourceRoot.resolve("hugegraph-server/hugegraph-dist/target"); + Path fixture = Files.createTempDirectory(Files.createDirectories(target), "cold-constructor-"); + Path output = fixture.resolve("constructor.log"); + Process process = null; + try { + process = startJvm(sourceRoot, output, "early", fixture.resolve("missing.yaml").toString()); + Assert.assertTrue("Constructor JVM timed out", process.waitFor(60, TimeUnit.SECONDS)); + Assert.assertEquals(Files.readString(output), 0, process.exitValue()); + Assert.assertTrue(Files.readString(output), Files.readString(output).contains("early-initialization-ok")); + } finally { + stop(process); + FileUtils.deleteDirectory(fixture.toFile()); + } + } + + @Test + public void testFirstGremlinQueryThroughServerStartup() throws Exception { + Path sourceRoot = Path.of(System.getProperty("basedir", System.getProperty("user.dir"))) + .getParent().getParent(); + Path target = sourceRoot.resolve("hugegraph-server/hugegraph-dist/target"); + Path fixture = Files.createTempDirectory(Files.createDirectories(target), "cold-gremlin-"); + Path output = fixture.resolve("server.log"); + Process server = null; + try { + int restPort; + int gremlinPort; + try (ServerSocket rest = new ServerSocket(0, 0, InetAddress.getLoopbackAddress()); + ServerSocket gremlin = new ServerSocket(0, 0, InetAddress.getLoopbackAddress())) { + restPort = rest.getLocalPort(); + gremlinPort = gremlin.getLocalPort(); + } + Path graphs = Files.createDirectory(fixture.resolve("graphs")); + Path graphConfig = graphs.resolve("hugegraph.properties"); + Files.writeString(graphConfig, "gremlin.graph=org.apache.hugegraph.HugeFactory\n" + + "backend=rocksdb\nserializer=binary\nstore=hugegraph\n" + + "rocksdb.data_path=" + fixture.resolve("data") + "\n" + + "rocksdb.wal_path=" + fixture.resolve("data") + "\n"); + Files.writeString(fixture.resolve("java.security"), "networkaddress.cache.ttl=60\n"); + // Seed in another JVM so neither schema creation nor a REST request + // can initialize ConditionQuery in the server JVM under test. + Path seedOutput = fixture.resolve("seed.log"); + Process seed = startJvm(sourceRoot, seedOutput, "seed", graphConfig.toString()); + try { + Assert.assertTrue("Seed JVM timed out", seed.waitFor(60, TimeUnit.SECONDS)); + Assert.assertEquals(Files.readString(seedOutput), 0, seed.exitValue()); + } finally { + stop(seed); + } + + Path staticDir = sourceRoot.resolve("hugegraph-server/hugegraph-dist/src/assembly/static"); + Path gremlinConfig = fixture.resolve("gremlin-server.yaml"); + String yaml = Files.readString(staticDir.resolve("conf/gremlin-server.yaml")) + .replace("scripts/empty-sample.groovy", + staticDir.resolve("scripts/empty-sample.groovy").toString()); + Files.writeString(gremlinConfig, yaml + "\nhost: 127.0.0.1\nport: " + gremlinPort + "\n"); + Path restConfig = fixture.resolve("rest-server.properties"); + Files.writeString(restConfig, "restserver.url=http://127.0.0.1:" + restPort + "\n" + + "gremlinserver.url=127.0.0.1:" + gremlinPort + "\n" + + "graphs=" + graphs + "\n" + + "memory_monitor.threshold=1.0\n"); + server = startJvm(sourceRoot, output, "serve", gremlinConfig.toString(), restConfig.toString()); + HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build(); + String restUrl = "http://127.0.0.1:" + restPort; + waitForServer(client, restUrl, server, output); + + // /versions is a readiness probe, not a graph query. The first graph + // request reaches real Gremlin workers after the production constructor. + ObjectMapper mapper = new ObjectMapper(); + String gremlinUrl = "http://127.0.0.1:" + gremlinPort + "/gremlin"; + Response response = gremlin(client, gremlinUrl, "g.V().hasLabel('cold_start').count()"); + Assert.assertEquals(response.body() + "\n" + Files.readString(output), 200, response.status()); + JsonNode result = mapper.readTree(response.body()); + Assert.assertEquals(response.body(), 200, result.path("status").path("code").asInt()); + Assert.assertEquals(response.body(), 1, result.path("result").path("data").get(0).asInt()); + + response = send(client, HttpRequest.newBuilder(URI.create(restUrl + + "/graphspaces/DEFAULT/graphs/hugegraph/graph/vertices?label=cold_start")) + .timeout(Duration.ofSeconds(10)).GET().build()); + Assert.assertEquals(response.body(), 200, response.status()); + JsonNode vertices = mapper.readTree(response.body()).path("vertices"); + Assert.assertEquals(response.body(), 1, vertices.size()); + Assert.assertEquals(response.body(), "cold_start", vertices.get(0).path("label").asText()); + + assertDenied(client, gremlinUrl, "System.getProperty('java.version')", "java.version"); + assertDenied(client, gremlinUrl, "System.setSecurityManager(null)", "denied permission"); + } finally { + stop(server); + FileUtils.deleteDirectory(fixture.toFile()); + } + } + + public static void main(String[] args) throws Exception { + if ("early".equals(args[0])) { + assertEarlyInitialization(args[1]); + } else if ("seed".equals(args[0])) { + HugeGraphServer.register(); + HugeGraph graph = HugeFactory.open(args[1]); + try { + graph.initBackend(); + graph.schema().vertexLabel("cold_start").useCustomizeStringId().create(); + graph.addVertex(T.id, "cold_start", T.label, "cold_start"); + graph.tx().commit(); + } finally { + graph.close(); + HugeFactory.shutdown(30L); + } + } else { + // Exercise the production constructor, including its + // temporary suspension and restoration of HugeSecurityManager. + HugeGraphServer.register(); + System.setSecurityManager(new HugeSecurityManager()); + HugeGraphServer server = new HugeGraphServer(args[1], args[2]); + Assert.assertTrue(System.getSecurityManager() instanceof HugeSecurityManager); + HugeFactory.removeShutdownHook(); + Runtime.getRuntime().addShutdownHook(new Thread(server::stop)); + // REST starts before Gremlin inside the constructor. Publish readiness + // only after the constructor returns and restores the sandbox. + Files.writeString(Path.of(args[2]).getParent().resolve("ready"), "ready"); + new CountDownLatch(1).await(); + } + } + + private static void assertEarlyInitialization(String missingConfig) throws Exception { + GremlinGroovyScriptEngine engine = new GremlinGroovyScriptEngine(); + SecurityManager previous = System.getSecurityManager(); + String threadName = Thread.currentThread().getName(); + try { + engine.eval("1 + 1"); + Log.logger(ColdGremlinQueryTest.class).warn("Initialize logging for {}", "cold constructor fixture"); + HugeSecurityManager security = new HugeSecurityManager(); + System.setSecurityManager(security); + HugeException error = Assert.assertThrows(HugeException.class, + () -> new HugeGraphServer(missingConfig, missingConfig)); + Assert.assertEquals("Failed to load yaml config file '" + missingConfig + "'", error.getMessage()); + Assert.assertSame(security, System.getSecurityManager()); + + // Stop before graph preparation can incidentally initialize query + // classes. Only the production constructor can warm them here. + Thread.currentThread().setName("gremlin-server-exec-cold-constructor"); + Assert.assertEquals(Boolean.TRUE, + engine.eval("org.apache.hugegraph.dist.ColdGremlinQueryTest.constructQuery()")); + Assert.assertNotNull(new ConditionQuery(HugeType.VERTEX)); + } finally { + Thread.currentThread().setName(threadName); + System.setSecurityManager(previous); + engine.reset(); + } + System.out.println("early-initialization-ok"); + } + + public static boolean constructQuery() { + return new ConditionQuery(HugeType.VERTEX).resultType() == HugeType.VERTEX; + } + + private static Process startJvm(Path sourceRoot, Path output, String... args) throws IOException { + Path staticDir = sourceRoot.resolve("hugegraph-server/hugegraph-dist/src/assembly/static"); + List command = new ArrayList<>(); + command.add(Path.of(System.getProperty("java.home"), "bin", "java").toString()); + command.add("-Xms128m"); + command.add("-Xmx512m"); + command.add("-Djava.security.manager=allow"); + command.add("-Djava.security.properties=" + output.getParent().resolve("java.security")); + command.add("@" + staticDir.resolve("bin/jvm-module.options")); + command.add("-cp"); + command.add(System.getProperty("surefire.test.class.path", System.getProperty("java.class.path"))); + command.add(ColdGremlinQueryTest.class.getName()); + command.addAll(List.of(args)); + return new ProcessBuilder(command).directory(sourceRoot.toFile()) + .redirectErrorStream(true).redirectOutput(output.toFile()).start(); + } + + private static void waitForServer(HttpClient client, String url, Process server, Path output) throws Exception { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(90); + while (System.nanoTime() < deadline) { + Assert.assertTrue(Files.readString(output), server.isAlive()); + try { + HttpResponse response = client.send(HttpRequest.newBuilder(URI.create(url + "/versions")) + .timeout(Duration.ofSeconds(3)).GET().build(), HttpResponse.BodyHandlers.discarding()); + if (response.statusCode() == 200 && Files.exists(output.getParent().resolve("ready"))) { + return; + } + } catch (IOException ignored) { + // The listener is not ready yet. + } + Thread.sleep(100L); + } + Assert.fail("Server startup timed out\n" + Files.readString(output)); + } + + private static Response gremlin(HttpClient client, String url, String script) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + String body = mapper.createObjectNode().put("gremlin", script).put("language", "gremlin-groovy") + .set("aliases", mapper.createObjectNode().put("g", "__g_DEFAULT-hugegraph")).toString(); + return send(client, HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(30)) + .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body)).build()); + } + + private static Response send(HttpClient client, HttpRequest request) throws Exception { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); + byte[] body = response.body(); + if ("gzip".equalsIgnoreCase(response.headers().firstValue("Content-Encoding").orElse(""))) { + try (GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(body))) { + body = gzip.readAllBytes(); + } + } + return new Response(response.statusCode(), new String(body, StandardCharsets.UTF_8)); + } + + private static void assertDenied(HttpClient client, String url, String script, String message) throws Exception { + Response response = gremlin(client, url, script); + Assert.assertNotEquals(response.body(), 200, response.status()); + Assert.assertTrue(response.body(), response.body().contains(message)); + } + + private static void stop(Process process) throws InterruptedException { + if (process != null && process.isAlive()) { + process.destroy(); + if (!process.waitFor(45, TimeUnit.SECONDS)) { + process.destroyForcibly().waitFor(10, TimeUnit.SECONDS); + } + } + } + + private record Response(int status, String body) { + } +} diff --git a/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/Example1.java b/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/Example1.java index a6cda3932f..b65b860162 100644 --- a/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/Example1.java +++ b/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/Example1.java @@ -23,14 +23,14 @@ import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.tx.GraphTransaction; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.traversal.optimize.Text; import org.apache.hugegraph.type.HugeType; @@ -479,10 +479,10 @@ public static void testLeftIndexProcess(final HugeGraph graph) { .has("city", "Hangzhou").toList(); assert vertices.isEmpty(); // set breakpoint here to see secondary_indexes and range_indexes table - vertices = graph.traversal().V().has("age", 28).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", 28).toList(); assert vertices.isEmpty(); // set breakpoint here to see secondary_indexes and range_indexes table - vertices = graph.traversal().V().has("city", "Hangzhou").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "Hangzhou").toList(); assert vertices.isEmpty(); } } diff --git a/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/Example4.java b/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/Example4.java index 0416dd757f..3d552e19dc 100644 --- a/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/Example4.java +++ b/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/Example4.java @@ -22,10 +22,10 @@ import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.meta.MetaManager; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.util.Log; diff --git a/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/ExampleUtil.java b/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/ExampleUtil.java index c5f0a4206f..3bde4e4d65 100644 --- a/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/ExampleUtil.java +++ b/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/ExampleUtil.java @@ -21,7 +21,7 @@ import java.util.Iterator; import java.util.concurrent.TimeoutException; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.dist.RegisterUtil; diff --git a/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/PerfExampleBase.java b/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/PerfExampleBase.java index 278b6336bd..88dd3c6a8c 100644 --- a/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/PerfExampleBase.java +++ b/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/PerfExampleBase.java @@ -29,8 +29,8 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.backend.cache.Cache; import org.apache.hugegraph.backend.cache.CacheManager; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.perf.PerfUtil; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.structure.HugeVertex; diff --git a/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/TaskExample.java b/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/TaskExample.java index 2afa719b99..fef7c3676d 100644 --- a/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/TaskExample.java +++ b/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/TaskExample.java @@ -22,8 +22,8 @@ import org.apache.commons.collections.IteratorUtils; import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.task.HugeTask; import org.apache.hugegraph.task.TaskCallable; import org.apache.hugegraph.task.TaskScheduler; diff --git a/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseSerializer.java b/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseSerializer.java index 0cfc928ab8..37b98dc037 100644 --- a/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseSerializer.java +++ b/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseSerializer.java @@ -19,7 +19,7 @@ import java.util.Arrays; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.serializer.BinarySerializer; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.type.HugeType; diff --git a/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseSessions.java b/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseSessions.java index 3255d092d6..d27ce99591 100644 --- a/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseSessions.java +++ b/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseSessions.java @@ -67,8 +67,8 @@ import org.apache.hadoop.hbase.filter.PrefixFilter; import org.apache.hadoop.hbase.util.VersionInfo; import org.apache.hadoop.security.UserGroupInformation; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntry.BackendIterator; import org.apache.hugegraph.backend.store.BackendSession.AbstractBackendSession; import org.apache.hugegraph.backend.store.BackendSessionPool; diff --git a/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseStore.java b/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseStore.java index 56f7210cc8..be5995881b 100644 --- a/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseStore.java +++ b/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseStore.java @@ -30,9 +30,9 @@ import org.apache.hadoop.hbase.NamespaceExistException; import org.apache.hadoop.hbase.TableExistsException; import org.apache.hadoop.hbase.TableNotFoundException; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.AbstractBackendStore; import org.apache.hugegraph.backend.store.BackendAction; import org.apache.hugegraph.backend.store.BackendEntry; @@ -177,7 +177,7 @@ public synchronized void open(HugeConfig config) { } @Override - public void close() { + public synchronized void close() { this.checkOpened(); this.sessions.close(); diff --git a/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseTable.java b/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseTable.java index 53a8d14f65..3faeb5d3e3 100644 --- a/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseTable.java +++ b/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseTable.java @@ -37,23 +37,23 @@ import org.apache.hadoop.hbase.client.Admin; import org.apache.hadoop.hbase.client.RegionInfo; import org.apache.hadoop.hbase.client.Result; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.page.PageState; -import org.apache.hugegraph.backend.query.Aggregate; -import org.apache.hugegraph.backend.query.Aggregate.AggregateFunc; -import org.apache.hugegraph.backend.query.Condition.Relation; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdPrefixQuery; -import org.apache.hugegraph.backend.query.IdRangeQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Aggregate; +import org.apache.hugegraph.query.Aggregate.AggregateFunc; +import org.apache.hugegraph.query.Condition.Relation; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdPrefixQuery; +import org.apache.hugegraph.query.IdRangeQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; import org.apache.hugegraph.backend.serializer.BinaryEntryIterator; import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntryIterator; import org.apache.hugegraph.backend.store.BackendTable; -import org.apache.hugegraph.backend.store.Shard; +import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.exception.NotSupportException; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.Bytes; diff --git a/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseTables.java b/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseTables.java index c3cda4031e..7535e29d01 100644 --- a/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseTables.java +++ b/hugegraph-server/hugegraph-hbase/src/main/java/org/apache/hugegraph/backend/store/hbase/HbaseTables.java @@ -25,12 +25,12 @@ import org.apache.hadoop.hbase.CellUtil; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.util.Bytes; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; import org.apache.hugegraph.backend.serializer.BinaryEntryIterator; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntryIterator; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.HugeKeys; diff --git a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessions.java b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessions.java index 85d664c587..2048e7d98e 100755 --- a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessions.java +++ b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessions.java @@ -22,7 +22,8 @@ import java.util.Set; import org.apache.commons.lang3.tuple.Pair; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; import org.apache.hugegraph.backend.store.BackendSession.AbstractBackendSession; @@ -183,6 +184,12 @@ public abstract BackendColumnIterator scan(String table, public abstract BackendColumnIterator getWithBatch(String table, List keys); + public BackendColumnIterator getWithBatchExact(String table, List keys) { + return BackendColumnIterator.wrap(keys.stream().map(key -> + BackendColumn.of(key.getKey(), this.get(table, key.getOwner(), key.getKey()))) + .filter(column -> column.value != null && column.value.length != 0).iterator()); + } + public abstract void merge(String table, byte[] ownerKey, byte[] key, byte[] value); diff --git a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessionsImpl.java b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessionsImpl.java index d172651393..dca14a0e27 100755 --- a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessionsImpl.java +++ b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessionsImpl.java @@ -32,13 +32,14 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.tuple.Pair; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; import org.apache.hugegraph.backend.store.BackendEntryIterator; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.meta.PdMetaDriver.PDAuthConfig; import org.apache.hugegraph.pd.client.PDClient; import org.apache.hugegraph.pd.client.PDConfig; @@ -123,6 +124,15 @@ private void initStoreNode(HugeConfig config) { @Override public void open() throws Exception { + this.registerGraph(); + this.session.open(); + } + + /** + * Registers the graph with PD once per JVM. clear() deletes the graph in PD and forgets the + * registration, so the next open() or createTable() registers it again. + */ + private void registerGraph() throws PDException { if (!infoInitializedGraph.contains(this.graphName)) { synchronized (infoInitializedGraph) { if (!infoInitializedGraph.contains(this.graphName)) { @@ -138,12 +148,11 @@ public void open() throws Exception { } } } - this.session.open(); } @Override protected boolean opened() { - return this.session != null; + return this.session.opened(); } @Override @@ -153,6 +162,15 @@ public Set openedTables() { @Override public synchronized void createTable(String... tables) { + // After clear() the store's sessions can still be open, as clearBackend() closes the + // stores without closing sessions the running graph holds, so initBackend()'s open() + // returns early and reaches here without registering the graph again + try { + this.registerGraph(); + } catch (PDException e) { + throw new BackendException("Failed to register graph '%s' with PD", e, + this.graphName); + } for (String table : tables) { this.session.createTable(table); this.tables.put(table, tableCode++); @@ -179,11 +197,14 @@ public void truncateTable(String table) { @Override public void clear() { - this.session.deleteGraph(); try { + this.session.deleteGraph(); hgStoreClient.getPdClient().delGraph(this.graphName); } catch (PDException ignored) { + } finally { + // The graph is gone from PD, or partly deleted: register it again before reuse + infoInitializedGraph.remove(this.graphName); } } @@ -409,6 +430,11 @@ public void close() { } } + static BackendColumnIterator getWithBatchExact(HgStoreSession graph, String table, List keys) { + return BackendColumnIterator.wrap(graph.batchGetOwner(table, keys).stream() + .map(entry -> BackendColumn.of(entry.key(), entry.value())).iterator()); + } + /** * HstoreSession implement for hstore */ @@ -787,6 +813,12 @@ public BackendColumnIterator getWithBatch(String table, return new ColumnIterator<>(table, kvIterator); } + @Override + public BackendColumnIterator getWithBatchExact(String table, List keys) { + assert !this.hasChanges(); + return HstoreSessionsImpl.getWithBatchExact(this.graph, table, keys); + } + @Override public void merge(String table, byte[] ownerKey, byte[] key, byte[] value) { @@ -801,9 +833,9 @@ public void setMode(GraphMode mode) { @Override public void truncate() throws Exception { + // The schema lives in PD meta and survives a truncate, so the schema + // id counters in PD must survive too: a reset hands out ids again this.graph.truncate(); - HstoreSessionsImpl.getDefaultPdClient() - .resetIdByKey(this.getGraphName()); } @Override diff --git a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreStore.java b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreStore.java index 6439096674..6faf4d4e8a 100644 --- a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreStore.java +++ b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreStore.java @@ -17,12 +17,14 @@ package org.apache.hugegraph.backend.store.hstore; +import org.apache.hugegraph.backend.BinaryId; + import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -37,20 +39,21 @@ import com.google.common.collect.Lists; import org.apache.commons.collections.CollectionUtils; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.query.ConditionQueryFlatten; -import org.apache.hugegraph.backend.query.IdPrefixQuery; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.IdPrefixQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; -import org.apache.hugegraph.backend.serializer.BytesBuffer; -import org.apache.hugegraph.backend.serializer.MergeIterator; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.OlapKey; import org.apache.hugegraph.backend.store.AbstractBackendStore; import org.apache.hugegraph.backend.store.BackendAction; import org.apache.hugegraph.backend.store.BackendEntry; +import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; import org.apache.hugegraph.backend.store.BackendFeatures; import org.apache.hugegraph.backend.store.BackendMutation; import org.apache.hugegraph.backend.store.BackendStoreProvider; @@ -59,12 +62,13 @@ import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.iterator.CIter; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.type.HugeTableType; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Action; import org.apache.hugegraph.type.define.GraphMode; import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; import org.slf4j.Logger; @@ -233,7 +237,7 @@ public synchronized void open(HugeConfig config) { } @Override - public void close() { + public synchronized void close() { this.checkOpened(); this.sessions.close(); @@ -402,8 +406,9 @@ public IdPrefixQuery next() { List queryList = Lists.newArrayList(); if (hugeGraph != null) { for (ConditionQuery conditionQuery : - ConditionQueryFlatten.flatten(cq)) { - Id label = conditionQuery.condition(HugeKeys.LABEL); + ConditionQueryFlatten.flatten(cq)) { + Id label = conditionQuery.singleConditionValueOrNull( + HugeKeys.LABEL); /* Parent type + sortKeys: g.V("V.id").outE("parentLabel") .has("sortKey","value") converted to all subtypes + sortKeys */ if ((this.subEls == null || @@ -451,7 +456,7 @@ public IdPrefixQuery next() { BytesBuffer buffer = BytesBuffer.allocate(BytesBuffer.BUF_EDGE_ID); buffer.writeId(ownerId); - return new IdPrefixQuery(cq, new BinaryBackendEntry.BinaryId( + return new IdPrefixQuery(cq, new BinaryId( buffer.bytes(), ownerId)); } @@ -459,7 +464,7 @@ private boolean matchEdgeSortKeys(ConditionQuery query, boolean matchAll, HugeGraph graph) { assert query.resultType().isEdge(); - Id label = query.condition(HugeKeys.LABEL); + Id label = query.singleConditionValueOrNull(HugeKeys.LABEL); if (label == null) { return false; } @@ -566,65 +571,14 @@ public Iterator> query(List typeList, private Iterator getBackendEntryIterator( Iterator entries, Query query) { - HstoreTable table; Set olapPks = query.olapPks(); if (this.isGraphStore && !olapPks.isEmpty()) { - List> iterators = new ArrayList<>(); - for (Id pk : olapPks) { - // Construct OLAP table query query condition - Query q = this.constructOlapQueryCondition(pk, query); - table = this.table(HugeType.OLAP); - iterators.add(table.queryOlap(this.session(HugeType.OLAP), q)); - } - entries = new MergeIterator<>(entries, iterators, - BackendEntry::mergeable); + HstoreTables.OlapTable table = (HstoreTables.OlapTable) this.table(HugeType.OLAP); + entries = table.mergeEntries(this.session(HugeType.OLAP), entries, olapPks, query.paging()); } return entries; } - /** - * Reconstruct the query OLAP table query - * Due to the olap merged into one table, when writing olap data, the key has a pk added at the end. - * So when making inquiries here, it is necessary to reconstruct the pk prefix. - * Write reference BinarySerializer.writeOlapVertex - * - * @param pk - * @param query - * @return - */ - private Query constructOlapQueryCondition(Id pk, Query query) { - if (query instanceof IdQuery && !CollectionUtils.isEmpty((query).ids())) { - IdQuery q = (IdQuery) query.copy(); - Iterator iterator = q.ids().iterator(); - LinkedHashSet linkedHashSet = new LinkedHashSet<>(); - while (iterator.hasNext()) { - Id id = iterator.next(); - if (id instanceof BinaryBackendEntry.BinaryId) { - id = ((BinaryBackendEntry.BinaryId) id).origin(); - } - - // create binary id - BytesBuffer buffer = - BytesBuffer.allocate(1 + pk.length() + 1 + id.length()); - buffer.writeId(pk); - id = new BinaryBackendEntry.BinaryId( - buffer.writeId(id).bytes(), id); - linkedHashSet.add(id); - } - q.resetIds(); - q.query(linkedHashSet); - return q; - } else { - // create binary id - BytesBuffer buffer = BytesBuffer.allocate(1 + pk.length()); - pk = new BinaryBackendEntry.BinaryId( - buffer.writeId(pk).bytes(), pk); - - IdPrefixQuery idPrefixQuery = new IdPrefixQuery(HugeType.OLAP, pk); - return idPrefixQuery; - } - } - @Override public Number queryNumber(Query query) { this.checkOpened(); @@ -748,6 +702,8 @@ public long getCounter(HugeType type) { public static class HstoreGraphStore extends HstoreStore { + private static final int OLAP_DELETE_BATCH_SIZE = 1024; + public HstoreGraphStore(BackendStoreProvider provider, String namespace, String store) { super(provider, namespace, store); @@ -817,16 +773,84 @@ public void checkAndRegisterOlapTable(Id pkId) { @Override public void clearOlapTable(Id pkId) { + this.deleteOlapData(pkId); } @Override public void removeOlapTable(Id pkId) { + // Every OLAP property shares this table and its registered manager. + this.deleteOlapData(pkId); + } + + private void deleteOlapData(Id pkId) { + E.checkArgumentNotNull(pkId, "OLAP property ID"); + Session session = this.session(HugeType.OLAP); + String tableName = this.table(HugeType.OLAP).table(); + E.checkState(!session.hasChanges(), "Can't clear OLAP data with pending graph mutations"); + E.checkState(super.sessions.existsTable(tableName), "Not exist table '%s'", tableName); + byte[] propertyPrefix = BytesBuffer.allocate(pkId.length() + 9).writeId(pkId).bytes(); + int pending = 0; + try { + // A property prefix alone also matches legacy vertex-only keys from other properties. + try (BackendColumnIterator columns = session.scan(tableName)) { + while (columns.hasNext()) { + BackendColumn column = columns.next(); + if (!OlapKey.matchesProperty(column.value, pkId)) { + boolean targetCompound = column.name != null && + column.name.length > propertyPrefix.length && + Bytes.prefixWith(column.name, propertyPrefix); + if (targetCompound) { + E.checkState(false, "OLAP value at key '%s' must match property '%s'", + olapKeyDiagnostic(column.name), pkId); + } + continue; + } + BytesBuffer key = BytesBuffer.wrap(column.name); + Id first = key.readId(); + Id vertexId = first; + if (key.remaining() != 0) { + if (!pkId.equals(first)) { + E.checkState(false, "OLAP key '%s' must match property '%s'", + olapKeyDiagnostic(column.name), pkId); + } + vertexId = key.readId(); + if (key.remaining() != 0) { + E.checkState(false, + "Unexpected trailing bytes in OLAP key '%s' for property '%s'", + olapKeyDiagnostic(column.name), pkId); + } + } + session.delete(tableName, vertexId.asBytes(), column.name); + if (++pending == OLAP_DELETE_BATCH_SIZE) { + session.commit(); + pending = 0; + } + } + } + if (pending != 0) { + session.commit(); + } + } catch (RuntimeException | Error failure) { + try { + session.rollback(); + } catch (RuntimeException | Error rollbackFailure) { + if (rollbackFailure != failure) { + failure.addSuppressed(rollbackFailure); + } + } + throw failure; + } + } + + private static String olapKeyDiagnostic(byte[] key) { + int maxBytes = 64; + return key.length <= maxBytes ? Bytes.toHex(key) : + Bytes.toHex(Arrays.copyOf(key, maxBytes)) + "..."; } @Override public boolean existOlapTable(Id pkId) { - String tableName = this.olapTableName(pkId); - return super.sessions.existsTable(tableName); + return super.sessions.existsTable(this.table(HugeType.OLAP).table()); } } } diff --git a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreTable.java b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreTable.java index a1baa14089..5774eeb7c6 100755 --- a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreTable.java +++ b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreTable.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.backend.store.hstore; +import org.apache.hugegraph.backend.BinaryId; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -29,25 +31,25 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.page.PageState; -import org.apache.hugegraph.backend.query.Aggregate; -import org.apache.hugegraph.backend.query.Aggregate.AggregateFunc; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.Condition.Relation; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdPrefixQuery; -import org.apache.hugegraph.backend.query.IdRangeQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Aggregate; +import org.apache.hugegraph.query.Aggregate.AggregateFunc; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.Condition.Relation; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdPrefixQuery; +import org.apache.hugegraph.query.IdRangeQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; import org.apache.hugegraph.backend.serializer.BinaryEntryIterator; import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; import org.apache.hugegraph.backend.store.BackendEntryIterator; import org.apache.hugegraph.backend.store.BackendTable; -import org.apache.hugegraph.backend.store.Shard; +import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.backend.store.hstore.HstoreSessions.Countable; import org.apache.hugegraph.backend.store.hstore.HstoreSessions.Session; import org.apache.hugegraph.exception.NotSupportException; @@ -210,8 +212,8 @@ public byte[] getInsertOwner(BackendEntry entry) { * @return */ protected byte[] getOwnerId(Id id) { - if (id instanceof BinaryBackendEntry.BinaryId) { - id = ((BinaryBackendEntry.BinaryId) id).origin(); + if (id instanceof BinaryId) { + id = ((BinaryId) id).origin(); } if (id != null && id.edge()) { id = ((EdgeId) id).ownerVertexId(); diff --git a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreTables.java b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreTables.java index fe8c324f19..54b721d325 100644 --- a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreTables.java +++ b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreTables.java @@ -18,12 +18,28 @@ package org.apache.hugegraph.backend.store.hstore; import java.util.List; - -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.Condition.Relation; -import org.apache.hugegraph.backend.query.ConditionQuery; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Collections; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.nio.ByteBuffer; + +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.backend.BinaryId; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.OlapKey; +import org.apache.hugegraph.iterator.FlatMapperIterator; +import org.apache.hugegraph.iterator.WrappedIterator; +import org.apache.hugegraph.store.HgOwnerKey; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.Condition.Relation; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.serializer.BinarySerializer; +import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; import org.apache.hugegraph.backend.store.hstore.HstoreSessions.Session; @@ -124,7 +140,7 @@ public void delete(Session session, BackendEntry entry) { * Regular index delete will call eliminate() */ byte[] ownerKey = super.ownerDelegate.apply(entry); - for (BackendEntry.BackendColumn column : entry.columns()) { + for (BackendColumn column : entry.columns()) { // Don't assert entry.belongToMe(column), length-prefix is 1* session.deletePrefix(this.table(), ownerKey, column.name); } @@ -197,17 +213,207 @@ protected BackendColumnIterator queryByCond(Session session, public static class OlapTable extends HstoreTable { public static final String TABLE = HugeTableType.OLAP_TABLE.string(); + private static final int MAX_REQUEST_KEYS = 1024; + private static final int MAX_SELECTED_VERTICES = 128; public OlapTable(String database) { // Originally multiple ap_{pk_id} merged into one ap table super(database, TABLE); } + @Override + public void insert(Session session, BackendEntry entry) { + E.checkArgumentNotNull(entry.subId(), "Merged OLAP write requires a property ID"); + byte[] owner = this.getInsertOwner(entry); + for (BackendColumn column : entry.columns()) { + E.checkArgument(OlapKey.matchesProperty(column.value, entry.subId()), + "OLAP value must match property ID '%s'", entry.subId()); + session.put(this.table(), owner, OlapKey.format(entry.subId(), entry.originId()), column.value); + } + } + + @Override + public void insert(Session session, BackendEntry entry, boolean isEdge) { + this.insert(session, entry); + } + + @Override + public void delete(Session session, BackendEntry entry) { + E.checkArgumentNotNull(entry.subId(), "Merged OLAP delete requires a property ID"); + byte[] owner = this.getInsertOwner(entry); + session.delete(this.table(), owner, OlapKey.format(entry.subId(), entry.originId())); + // Delete an old vertex-only record only if it belongs to the requested property. + byte[] legacyKey = BytesBuffer.allocate(entry.originId().length() + 9) + .writeId(entry.originId()).bytes(); + if (OlapKey.matchesProperty(session.get(this.table(), owner, legacyKey), entry.subId())) { + session.delete(this.table(), owner, legacyKey); + } + } + + @Override + public boolean queryExist(Session session, BackendEntry entry) { + E.checkArgumentNotNull(entry.subId(), "Merged OLAP existence query requires a property ID"); + Id key = new BinaryId(OlapKey.format(entry.subId(), entry.originId()), entry.originId()); + try (BackendColumnIterator result = this.getById(session, key)) { + return result.hasNext(); + } + } + + @Override + protected BackendColumnIterator queryBy(Session session, Query query) { + if (!query.ids().isEmpty() && query.conditions().isEmpty()) { + return BackendColumnIterator.wrap(new FlatMapperIterator<>( + query.ids().iterator(), id -> this.getById(session, id))); + } + return super.queryBy(session, query); + } + + @Override + protected BackendColumnIterator getById(Session session, Id id) { + // This query key is explicitly constructed by HstoreStore for the merged OLAP table. + BytesBuffer key = BytesBuffer.wrap(id.asBytes()); + Id propertyId = key.readId(); + Id vertexId = key.readId(); + byte[] owner = this.getOwnerId(id instanceof BinaryId ? ((BinaryId) id).origin() : vertexId); + byte[] value = session.get(this.table(), owner, id.asBytes()); + if (value == null || value.length == 0) { + byte[] legacyKey = BytesBuffer.allocate(vertexId.length() + 9).writeId(vertexId).bytes(); + value = session.get(this.table(), owner, legacyKey); + } + if (!OlapKey.matchesProperty(value, propertyId)) { + return BackendColumnIterator.empty(); + } + // Normalize only the returned key, so the existing OLAP entry parser retains vertex origin. + return BackendColumnIterator.iterator(BackendColumn.of(id.asBytes(), value)); + } + @Override protected BackendColumnIterator queryById(Session session, Id id) { return this.getById(session, id); } + Iterator mergeEntries(Session session, Iterator entries, + Set properties, boolean paging) { + if (properties == null || properties.isEmpty()) { + return entries; + } + return new WrappedIterator() { + private Iterator batch = Collections.emptyIterator(); + private final int batchSize = paging ? 1 : (int) Math.max(1L, Math.min( + MAX_SELECTED_VERTICES, MAX_REQUEST_KEYS / (properties.size() + 1L))); + private boolean closed; + + @Override + public void close() throws Exception { + if (!this.closed) { + this.closed = true; + this.current = none(); + this.batch = Collections.emptyIterator(); + super.close(); + } + } + + @Override + protected Iterator originIterator() { + return entries; + } + + @Override + protected boolean fetch() { + if (this.closed) { + return false; + } + try { + return this.fetchBatch(); + } catch (RuntimeException | Error failure) { + try { + this.close(); + } catch (Exception closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } + } + + private boolean fetchBatch() { + if (!this.batch.hasNext()) { + List selected = new ArrayList<>(this.batchSize); + while (selected.size() < this.batchSize && entries.hasNext()) { + selected.add(entries.next()); + } + if (selected.isEmpty()) { + WrappedIterator.close(this); + return false; + } + this.mergeSelected(selected); + this.batch = selected.iterator(); + } + this.current = this.batch.next(); + return true; + } + + private void mergeSelected(List selected) { + Map legacy = new HashMap<>(); + Iterator remaining = properties.iterator(); + boolean firstChunk = true; + do { + int capacity = Math.min(MAX_REQUEST_KEYS, + selected.size() * (Math.min(properties.size(), MAX_REQUEST_KEYS) + 1)); + List keys = new ArrayList<>(capacity); + if (firstChunk) { + for (BackendEntry entry : selected) { + Id vertexId = entry.originId(); + byte[] key = BytesBuffer.allocate(vertexId.length() + 9).writeId(vertexId).bytes(); + keys.add(HgOwnerKey.of(OlapTable.this.getOwnerId(vertexId), key)); + } + } + int propertyLimit = (MAX_REQUEST_KEYS - keys.size()) / selected.size(); + List requestedProperties = new ArrayList<>(Math.min(propertyLimit, properties.size())); + while (requestedProperties.size() < propertyLimit && remaining.hasNext()) { + Id property = remaining.next(); + requestedProperties.add(property); + for (BackendEntry entry : selected) { + Id vertexId = entry.originId(); + keys.add(HgOwnerKey.of(OlapTable.this.getOwnerId(vertexId), + OlapKey.format(property, vertexId))); + } + } + Map values = new HashMap<>(); + try (BackendColumnIterator columns = session.getWithBatchExact(OlapTable.this.table(), keys)) { + while (columns.hasNext()) { + BackendColumn column = columns.next(); + values.put(ByteBuffer.wrap(column.name), column); + } + } + if (firstChunk) { + for (BackendEntry entry : selected) { + Id vertexId = entry.originId(); + byte[] key = BytesBuffer.allocate(vertexId.length() + 9).writeId(vertexId).bytes(); + legacy.put(vertexId, values.get(ByteBuffer.wrap(key))); + } + firstChunk = false; + } + for (BackendEntry entry : selected) { + Id vertexId = entry.originId(); + for (Id property : requestedProperties) { + byte[] key = OlapKey.format(property, vertexId); + BackendColumn column = values.get(ByteBuffer.wrap(key)); + if (column == null || column.value == null || column.value.length == 0) { + column = legacy.get(vertexId); + } + if (column == null || !OlapKey.matchesProperty(column.value, property)) { + continue; + } + BinaryBackendEntry olap = new BinaryBackendEntry(entry.type(), key, false, true); + olap.columns(BackendColumn.of(key, column.value)); + E.checkState(entry.mergeable(olap), "OLAP row must belong to its selected vertex"); + } + } + } while (remaining.hasNext()); + } + }; + } + @Override public boolean isOlap() { return true; diff --git a/hugegraph-server/hugegraph-hstore/src/test/java/org/apache/hugegraph/backend/store/hstore/HstoreSessionsImplTest.java b/hugegraph-server/hugegraph-hstore/src/test/java/org/apache/hugegraph/backend/store/hstore/HstoreSessionsImplTest.java index b962ae0b0d..959cf8fe18 100644 --- a/hugegraph-server/hugegraph-hstore/src/test/java/org/apache/hugegraph/backend/store/hstore/HstoreSessionsImplTest.java +++ b/hugegraph-server/hugegraph-hstore/src/test/java/org/apache/hugegraph/backend/store/hstore/HstoreSessionsImplTest.java @@ -20,17 +20,30 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.pd.client.PDClient; +import org.apache.hugegraph.pd.common.PDException; +import org.apache.hugegraph.pd.grpc.Metapb; +import org.apache.hugegraph.store.HgStoreClient; import org.apache.hugegraph.store.HgKvEntry; +import org.apache.hugegraph.store.HgStoreSession; +import org.apache.hugegraph.store.HgOwnerKey; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.serializer.OlapKey; import org.apache.hugegraph.store.HgKvIterator; import org.junit.Assert; import org.junit.Test; +import org.mockito.Mockito; public class HstoreSessionsImplTest { @@ -45,6 +58,97 @@ public void testProductionClassDoesNotReferenceTestAssert() throws IOException { } } + @Test + public void testInitAfterClearRegistersGraphAgain() throws Exception { + String namespace = "hstore-clear-test-" + System.nanoTime(); + String graphName = namespace + "/m"; + PDClient pdClient = Mockito.mock(PDClient.class); + HgStoreClient storeClient = Mockito.mock(HgStoreClient.class); + Mockito.when(storeClient.openSession(Mockito.anyString())) + .thenAnswer(invocation -> Mockito.mock(HgStoreSession.class)); + Mockito.when(storeClient.getPdClient()).thenReturn(pdClient); + + Object[] saved = setStoreClients(pdClient, storeClient, Boolean.TRUE); + try { + HugeConfig config = new HugeConfig(Map.of()); + HstoreStore store = new HstoreStore.HstoreSchemaStore(null, namespace, "m"); + + // initBackend(), then the graph opens the store to serve requests + store.open(config); + store.init(); + store.open(config); + Mockito.verify(pdClient, Mockito.times(1)).setGraph(graph(graphName)); + + for (int i = 1; i <= 50; i++) { + // clearBackend(): the graph still holds the store, so its sessions stay open + store.open(config); + store.clear(true); + store.close(); + // initBackend(): open() returns early on the open sessions, init() must + // register the graph PD deleted + store.open(config); + store.init(); + store.close(); + + Mockito.verify(pdClient, Mockito.times(i)).delGraph(graphName); + Mockito.verify(pdClient, Mockito.times(i + 1)).setGraph(graph(graphName)); + } + } finally { + setStoreClients(saved[0], saved[1], saved[2]); + } + } + + @Test + public void testInitAfterFailedDeleteRegistersGraphAgain() throws Exception { + String namespace = "hstore-clear-test-" + System.nanoTime(); + String graphName = namespace + "/m"; + PDClient pdClient = Mockito.mock(PDClient.class); + Mockito.doThrow(new PDException(-1, "PD is not the leader")) + .when(pdClient).delGraph(graphName); + HgStoreClient storeClient = Mockito.mock(HgStoreClient.class); + Mockito.when(storeClient.openSession(Mockito.anyString())) + .thenAnswer(invocation -> Mockito.mock(HgStoreSession.class)); + Mockito.when(storeClient.getPdClient()).thenReturn(pdClient); + + Object[] saved = setStoreClients(pdClient, storeClient, Boolean.TRUE); + try { + HugeConfig config = new HugeConfig(Map.of()); + HstoreStore store = new HstoreStore.HstoreSchemaStore(null, namespace, "m"); + store.open(config); + store.init(); + + // The store side may be gone even though PD refused the delete + store.clear(true); + store.init(); + Mockito.verify(pdClient, Mockito.times(2)).setGraph(graph(graphName)); + } finally { + setStoreClients(saved[0], saved[1], saved[2]); + } + } + + private static Metapb.Graph graph(String graphName) { + return Mockito.argThat(graph -> graph != null && + graph.getGraphName().equals(graphName)); + } + + /** + * Swaps the PD and Store clients HstoreSessionsImpl keeps in static fields, so no real + * client is created, and returns the previous values + */ + private static Object[] setStoreClients(Object pdClient, Object storeClient, + Object initialized) throws Exception { + String[] names = {"defaultPdClient", "hgStoreClient", "initializedNode"}; + Object[] values = {pdClient, storeClient, initialized}; + Object[] previous = new Object[names.length]; + for (int i = 0; i < names.length; i++) { + Field field = HstoreSessionsImpl.class.getDeclaredField(names[i]); + field.setAccessible(true); + previous[i] = field.get(null); + field.set(null, values[i]); + } + return previous; + } + @Test public void testColumnIteratorPositionTracksNextUnreadKey() throws Exception { @@ -71,6 +175,30 @@ public void testColumnIteratorClearsPositionWhenFullyExhausted() Assert.assertNull(iterator.position()); } + @Test + public void testExactOlapBatchUsesNativePointKeysAndOwnerRouting() { + byte[] vertexOwner = IdGenerator.of(9).asBytes(); + byte[] namespaced = OlapKey.format(IdGenerator.of(9), IdGenerator.of(9)); + List keys = List.of(HgOwnerKey.of(vertexOwner, namespaced)); + int[] calls = {0}; + HgStoreSession graph = (HgStoreSession) Proxy.newProxyInstance(HgStoreSession.class.getClassLoader(), + new Class[]{HgStoreSession.class}, (proxy, method, args) -> { + Assert.assertEquals("batchGetOwner", method.getName()); + Assert.assertEquals("g+olap", args[0]); + Assert.assertSame(keys, args[1]); + Assert.assertArrayEquals(vertexOwner, keys.get(0).getOwner()); + Assert.assertArrayEquals(namespaced, keys.get(0).getKey()); + calls[0]++; + return List.of(new TestEntry(namespaced)); + }); + try (BackendColumnIterator result = HstoreSessionsImpl.getWithBatchExact(graph, "g+olap", keys)) { + Assert.assertTrue(result.hasNext()); + Assert.assertArrayEquals(namespaced, result.next().name); + Assert.assertFalse(result.hasNext()); + } + Assert.assertEquals(1, calls[0]); + } + private static BackendColumnIterator newColumnIterator( HgKvIterator iterator) throws Exception { Class clazz = Class.forName(HstoreSessionsImpl.class.getName() + diff --git a/hugegraph-server/hugegraph-hstore/src/test/java/org/apache/hugegraph/backend/store/hstore/HstoreTableTest.java b/hugegraph-server/hugegraph-hstore/src/test/java/org/apache/hugegraph/backend/store/hstore/HstoreTableTest.java index 79791daae1..0ae351548e 100644 --- a/hugegraph-server/hugegraph-hstore/src/test/java/org/apache/hugegraph/backend/store/hstore/HstoreTableTest.java +++ b/hugegraph-server/hugegraph-hstore/src/test/java/org/apache/hugegraph/backend/store/hstore/HstoreTableTest.java @@ -18,23 +18,45 @@ package org.apache.hugegraph.backend.store.hstore; import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.LinkedHashSet; +import java.nio.ByteBuffer; +import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import org.apache.commons.lang3.tuple.Pair; -import org.apache.hugegraph.backend.id.Id.IdType; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id.IdType; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.backend.BinaryId; +import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; +import org.apache.hugegraph.backend.serializer.BinarySerializer; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.OlapKey; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.ConfigOption; +import org.apache.hugegraph.iterator.CIter; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.HugeVertex; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.WriteType; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.page.PageInfo; import org.apache.hugegraph.backend.page.PageState; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdPrefixQuery; -import org.apache.hugegraph.backend.query.IdRangeQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdPrefixQuery; +import org.apache.hugegraph.query.IdRangeQuery; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.query.IdQuery; import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; import org.apache.hugegraph.backend.store.BackendEntryIterator; import org.apache.hugegraph.store.HgOwnerKey; @@ -216,6 +238,343 @@ public void testPrefixListQueryPushesCopyAndKeepsOrigin() { Assert.assertNull(pushed.originQuery()); } + @Test + public void testMergedOlapInsertPreservesStandaloneEntryAndIsolatesProperties() { + HstoreTables.OlapTable table = new HstoreTables.OlapTable("g"); + OlapSession session = new OlapSession(); + BinaryBackendEntry score = olapEntry(9, 1, 99); + BinaryBackendEntry rank = olapEntry(10, 1, 123); + byte[] standaloneKey = score.columns().iterator().next().name; + table.insert(session, score, false); // Actual HstoreStore.mutate entry point. + table.insert(session, rank); + Assert.assertEquals(2, session.values.size()); + Assert.assertArrayEquals(standaloneKey, score.columns().iterator().next().name); + Assert.assertNull(session.values.get(ByteBuffer.wrap(standaloneKey))); + Assert.assertTrue(OlapKey.matchesProperty(session.values.get(ByteBuffer.wrap( + OlapKey.format(IdGenerator.of(9), IdGenerator.of(1)))), IdGenerator.of(9))); + for (byte[] owner : session.owners) { + Assert.assertArrayEquals(IdGenerator.of(1).asBytes(), owner); + } + } + + @Test + public void testOlapPointFallbackAndDeleteValidateLegacyPropertyIdentity() { + HstoreTables.OlapTable table = new HstoreTables.OlapTable("g"); + OlapSession session = new OlapSession(); + BinaryBackendEntry legacy = olapEntry(9, 1, 42); + BackendColumn old = legacy.columns().iterator().next(); + session.values.put(ByteBuffer.wrap(old.name), old.value); + Id scoreKey = new BinaryId(OlapKey.format(IdGenerator.of(9), IdGenerator.of(1)), IdGenerator.of(1)); + Id rankKey = new BinaryId(OlapKey.format(IdGenerator.of(10), IdGenerator.of(1)), IdGenerator.of(1)); + try (BackendColumnIterator result = table.getById(session, scoreKey)) { + Assert.assertTrue(result.hasNext()); + Assert.assertArrayEquals(scoreKey.asBytes(), result.next().name); + } + try (BackendColumnIterator result = table.getById(session, rankKey)) { + Assert.assertFalse(result.hasNext()); + } + IdQuery query = new IdQuery(HugeType.VERTEX); + query.query(scoreKey); + query.query(rankKey); + Iterator queried = table.queryOlap(session, query); + Assert.assertTrue(queried.hasNext()); + Assert.assertEquals(IdGenerator.of(1), queried.next().originId()); + Assert.assertFalse(queried.hasNext()); + table.delete(session, olapEntry(10, 1, 123)); + Assert.assertNotNull(session.values.get(ByteBuffer.wrap(old.name))); + table.delete(session, legacy); + Assert.assertNull(session.values.get(ByteBuffer.wrap(old.name))); + } + + @Test + public void testOlapConditionalUpdatesUsePropertyScopedExistenceForOldAndNewRows() { + HstoreTables.OlapTable table = new HstoreTables.OlapTable("g"); + OlapSession session = new OlapSession(); + BinaryBackendEntry oldScore = olapEntry(9, 1, 42); + BackendColumn old = oldScore.columns().iterator().next(); + session.values.put(ByteBuffer.wrap(old.name), old.value); + BinaryBackendEntry score = olapEntry(9, 1, 99); + BinaryBackendEntry rank = olapEntry(10, 1, 123); + byte[] scoreKey = OlapKey.format(IdGenerator.of(9), IdGenerator.of(1)); + byte[] rankKey = OlapKey.format(IdGenerator.of(10), IdGenerator.of(1)); + Assert.assertTrue(table.queryExist(session, score)); + Assert.assertFalse(table.queryExist(session, rank)); + table.updateIfPresent(session, rank); + Assert.assertNull(session.values.get(ByteBuffer.wrap(rankKey))); + table.updateIfAbsent(session, rank); + Assert.assertNotNull(session.values.get(ByteBuffer.wrap(rankKey))); + table.updateIfPresent(session, score); + Assert.assertArrayEquals(score.columns().iterator().next().value, + session.values.get(ByteBuffer.wrap(scoreKey))); + table.updateIfAbsent(session, olapEntry(9, 1, 101)); + Assert.assertArrayEquals(score.columns().iterator().next().value, + session.values.get(ByteBuffer.wrap(scoreKey))); + Assert.assertArrayEquals(old.value, session.values.get(ByteBuffer.wrap(old.name))); + table.delete(session, score); + Assert.assertFalse(table.queryExist(session, score)); + Assert.assertTrue(table.queryExist(session, rank)); + table.updateIfPresent(session, score); + Assert.assertNull(session.values.get(ByteBuffer.wrap(scoreKey))); + Assert.assertEquals(1, session.values.size()); + } + + @Test + public void testOlapMergeBatchesOnlySelectedVerticesAndPrefersNamespacedRows() throws Exception { + HstoreTables.OlapTable table = new HstoreTables.OlapTable("g"); + OlapSession session = new OlapSession(); + BinaryBackendEntry legacy = olapEntry(9, 1, 42); + BackendColumn old = legacy.columns().iterator().next(); + session.values.put(ByteBuffer.wrap(old.name), old.value); + table.insert(session, olapEntry(9, 1, 99)); + table.insert(session, olapEntry(10, 1, 123)); + BackendColumn legacySecond = olapEntry(9, 2, 43).columns().iterator().next(); + session.values.put(ByteBuffer.wrap(legacySecond.name), legacySecond.value); + List base = new ArrayList<>(); + for (int i = 1; i <= 130; i++) { + base.add(vertexEntry(i)); + } + EntrySource source = new EntrySource(base); + Iterator merged = table.mergeEntries(session, source, + Set.of(IdGenerator.of(9), IdGenerator.of(10)), false); + Assert.assertEquals(3, merged.next().columnsSize()); + Assert.assertEquals(1, session.batchCalls); + Assert.assertEquals(128 * 3, session.requested.get(0).size()); + for (HgOwnerKey key : session.requested.get(0)) { + Assert.assertFalse(Arrays.equals(key.getOwner(), IdGenerator.of(129).asBytes())); + } + int count = 1; + while (merged.hasNext()) { + Assert.assertEquals(IdGenerator.of(++count), merged.next().originId()); + } + Assert.assertEquals(130, count); + Assert.assertEquals(2, base.get(1).columnsSize()); // Legacy score must not also be merged as rank. + Assert.assertEquals(1, base.get(2).columnsSize()); + Assert.assertEquals(2, session.batchCalls); + Assert.assertEquals(2, session.batchCloses); + Assert.assertEquals(1, source.closes); + BackendColumn score = base.get(0).columns().stream() + .filter(column -> Arrays.equals(column.name, OlapKey.format(IdGenerator.of(9), IdGenerator.of(1)))) + .findFirst().get(); + Assert.assertArrayEquals(olapEntry(9, 1, 99).columns().iterator().next().value, score.value); + // The old row remains unchanged and has not been rewritten during reads. + Assert.assertArrayEquals(old.value, session.values.get(ByteBuffer.wrap(old.name))); + } + + @Test + public void testOlapMergeCapsTotalKeysForManyPropertiesAndRetainsLastChunkRows() throws Exception { + HstoreTables.OlapTable table = new HstoreTables.OlapTable("g"); + OlapSession session = new OlapSession(); + Set properties = manyOlapProperties(); + BackendColumn old = olapEntry(9999, 1, 42).columns().iterator().next(); + session.values.put(ByteBuffer.wrap(old.name), old.value); + table.insert(session, olapEntry(10000, 1, 99)); + EntrySource source = new EntrySource(List.of(vertexEntry(1), vertexEntry(2))); + Iterator merged = table.mergeEntries(session, source, properties, false); + BackendEntry first = merged.next(); + Assert.assertEquals(1, source.consumed); // Many properties reduce the selected vertex batch. + Assert.assertEquals(3, first.columnsSize()); + Assert.assertEquals(10, session.batchCalls); + Assert.assertEquals(10, session.batchCloses); + int oldRequests = 0; + for (List requested : session.requested) { + Assert.assertTrue(requested.size() <= 1024); + for (HgOwnerKey key : requested) { + Assert.assertArrayEquals(IdGenerator.of(1).asBytes(), key.getOwner()); + if (Arrays.equals(old.name, key.getKey())) { + oldRequests++; + } + } + } + Assert.assertEquals(1, oldRequests); + BinaryBackendEntry decoded = (BinaryBackendEntry) first; + Assert.assertArrayEquals(old.value, decoded.column( + OlapKey.format(IdGenerator.of(9999), IdGenerator.of(1))).value); + Assert.assertArrayEquals(olapEntry(10000, 1, 99).columns().iterator().next().value, + decoded.column(OlapKey.format(IdGenerator.of(10000), IdGenerator.of(1))).value); + ((AutoCloseable) merged).close(); + Assert.assertEquals(1, source.closes); + Assert.assertEquals(1, source.consumed); + } + + @Test + public void testOlapPropertyChunksCloseOnLaterBatchFailureWithoutReadingAhead() { + HstoreTables.OlapTable table = new HstoreTables.OlapTable("g"); + OlapSession session = new OlapSession(); + session.failBatchAt = 3; + EntrySource source = new EntrySource(List.of(vertexEntry(1), vertexEntry(2))); + Iterator merged = table.mergeEntries(session, source, manyOlapProperties(), true); + try { + merged.hasNext(); + Assert.fail("A later property chunk failure must propagate"); + } catch (IllegalStateException expected) { + Assert.assertEquals("batch failed", expected.getMessage()); + } + Assert.assertEquals(1, source.consumed); + Assert.assertEquals(1, source.closes); + Assert.assertEquals(3, session.batchCalls); + Assert.assertEquals(3, session.batchCloses); + for (List requested : session.requested) { + Assert.assertTrue(requested.size() <= 1024); + } + } + + private static Set manyOlapProperties() { + Set properties = new LinkedHashSet<>(); + for (int i = 1; i <= 10000; i++) { + properties.add(IdGenerator.of(i)); + } + return properties; + } + + @Test + public void testOlapMergePreservesPagingMetadataAndClosesOnEarlyCloseOrFailure() throws Exception { + HstoreTables.OlapTable table = new HstoreTables.OlapTable("g"); + OlapSession session = new OlapSession(); + EntrySource source = new EntrySource(List.of(vertexEntry(1), vertexEntry(2))); + Iterator merged = table.mergeEntries(session, source, Set.of(IdGenerator.of(9)), true); + Assert.assertEquals(IdGenerator.of(1), merged.next().originId()); + Assert.assertEquals(1, source.consumed); + Assert.assertEquals(1, ((CIter) merged).metadata("position")); + ((AutoCloseable) merged).close(); + Assert.assertEquals(1, source.closes); + Assert.assertEquals(1, session.batchCloses); + Assert.assertFalse(merged.hasNext()); + EntrySource failed = new EntrySource(List.of(vertexEntry(1))); + session.failBatch = true; + Iterator failure = table.mergeEntries(session, failed, Set.of(IdGenerator.of(9)), false); + try { + failure.hasNext(); + Assert.fail("Native batch failure must propagate"); + } catch (IllegalStateException expected) { + Assert.assertEquals("batch failed", expected.getMessage()); + } + Assert.assertEquals(1, failed.closes); + Assert.assertEquals(2, session.batchCloses); + EntrySource empty = new EntrySource(List.of(vertexEntry(1))); + Assert.assertSame(empty, table.mergeEntries(session, empty, Set.of(), false)); + Assert.assertSame(empty, table.mergeEntries(session, empty, null, false)); + Assert.assertEquals(2, session.batchCalls); + } + + private static BinaryBackendEntry vertexEntry(int vertex) { + BinaryBackendEntry entry = new BinaryBackendEntry(HugeType.VERTEX, + BytesBuffer.allocate(16).writeId(IdGenerator.of(vertex)).bytes()); + entry.column(entry.id().asBytes(), new byte[]{0}); + return entry; + } + + private static BinaryBackendEntry olapEntry(int property, int vertex, int value) { + HugeConfig config = new HugeConfig(Map.of()); + HugeGraph graph = (HugeGraph) Proxy.newProxyInstance(HugeGraph.class.getClassLoader(), + new Class[]{HugeGraph.class}, (proxy, method, args) -> { + if (method.getName().equals("option")) { + return ((ConfigOption) args[0]).defaultValue(); + } + if (method.getName().equals("configuration")) { + return config; + } + return null; + }); + PropertyKey key = new PropertyKey(graph, IdGenerator.of(property), "property" + property); + key.dataType(DataType.INT); + key.writeType(WriteType.OLAP_COMMON); + HugeVertex olap = new HugeVertex(graph, IdGenerator.of(vertex), VertexLabel.OLAP_VL); + olap.addProperty(key, value); + return (BinaryBackendEntry) new BinarySerializer(config).writeOlapVertex(olap); + } + + private static final class EntrySource implements CIter { + private final Iterator source; + private int consumed; + private int closes; + + private EntrySource(List entries) { + this.source = entries.iterator(); + } + + @Override + public boolean hasNext() { + return this.source.hasNext(); + } + + @Override + public BackendEntry next() { + this.consumed++; + return this.source.next(); + } + + @Override + public void close() { + this.closes++; + } + + @Override + public Object metadata(String meta, Object... args) { + return this.consumed; + } + } + + private static final class OlapSession extends ScanRecordingSession { + private final Map values = new HashMap<>(); + private final List owners = new ArrayList<>(); + private final List> requested = new ArrayList<>(); + private int batchCalls; + private int batchCloses; + private boolean failBatch; + private int failBatchAt; + + @Override + public void put(String table, byte[] owner, byte[] key, byte[] value) { + this.owners.add(owner); + this.values.put(ByteBuffer.wrap(key), value); + } + + @Override + public byte[] get(String table, byte[] owner, byte[] key) { + this.owners.add(owner); + return this.values.get(ByteBuffer.wrap(key)); + } + + @Override + public void delete(String table, byte[] owner, byte[] key) { + this.values.remove(ByteBuffer.wrap(key)); + } + + @Override + public BackendColumnIterator getWithBatchExact(String table, List keys) { + this.batchCalls++; + this.requested.add(keys); + Iterator result = keys.stream().filter(key -> + this.values.containsKey(ByteBuffer.wrap(key.getKey()))) + .map(key -> BackendColumn.of(key.getKey(), this.values.get(ByteBuffer.wrap(key.getKey())))) + .iterator(); + return new BackendColumnIterator() { + @Override + public boolean hasNext() { + if (failBatch || (failBatchAt > 0 && batchCalls == failBatchAt)) { + throw new IllegalStateException("batch failed"); + } + return result.hasNext(); + } + + @Override + public BackendColumn next() { + return result.next(); + } + + @Override + public byte[] position() { + return null; + } + + @Override + public void close() { + batchCloses++; + } + }; + } + } + private HstoreTable newTestTable() { HstoreTable table = new HstoreTable("hugegraph", "g+oe"); table.ownerByQueryDelegate = (type, id) -> new byte[]{0}; @@ -245,7 +604,7 @@ private static byte[] keyBytes(int key) { return bytes; } - private static final class ScanRecordingSession extends HstoreSessions.Session { + private static class ScanRecordingSession extends HstoreSessions.Session { private boolean scanCalled = false; private byte[] lastQueryBytes = null; diff --git a/hugegraph-server/hugegraph-rocksdb/pom.xml b/hugegraph-server/hugegraph-rocksdb/pom.xml index 845cf40f9c..276f8163cd 100644 --- a/hugegraph-server/hugegraph-rocksdb/pom.xml +++ b/hugegraph-server/hugegraph-rocksdb/pom.xml @@ -37,7 +37,7 @@ org.rocksdb rocksdbjni - 8.10.2 + ${rocksdb.version} diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/OpenedRocksDB.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/OpenedRocksDB.java index 2aa6d2292c..f1cd73b17f 100644 --- a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/OpenedRocksDB.java +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/OpenedRocksDB.java @@ -27,7 +27,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.io.FileUtils; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.store.rocksdb.RocksDBIteratorPool.ReusedRocksIterator; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; @@ -44,9 +44,18 @@ public class OpenedRocksDB implements AutoCloseable { private final RocksDB rocksdb; private final Map cfHandles; private final SstFileManager sstFileManager; + private RecoveryLock recoveryLock; + private boolean nativeClosed; + private Throwable nativeCloseFailure; public OpenedRocksDB(RocksDB rocksdb, Map cfHandles, SstFileManager sstFileManager) { + this(rocksdb, cfHandles, sstFileManager, null); + } + + OpenedRocksDB(RocksDB rocksdb, Map cfHandles, + SstFileManager sstFileManager, RecoveryLock recoveryLock) { + this.recoveryLock = recoveryLock; this.rocksdb = rocksdb; this.cfHandles = cfHandles; this.sstFileManager = sstFileManager; @@ -81,16 +90,49 @@ public boolean isOwningHandle() { } @Override - public void close() { - if (!this.isOwningHandle()) { + public synchronized void close() { + this.closeNative(); + RocksDBSnapshotRestore.unlock(this.recoveryLock); + this.recoveryLock = null; + } + + synchronized boolean ownsRecoveryLock() { + return this.recoveryLock != null && this.recoveryLock.isOpen(); + } + + // Transfer the existing lock to the replacement DB without an unlocked gap. + synchronized RecoveryLock closeForRestore() { + this.closeNative(); + E.checkState(this.recoveryLock == null || this.recoveryLock.isOpen(), + "Cannot transfer a recovery lock after close failed"); + RecoveryLock transferred = this.recoveryLock; + this.recoveryLock = null; + return transferred; + } + + private void closeNative() { + if (this.nativeCloseFailure instanceof RuntimeException) { + throw (RuntimeException) this.nativeCloseFailure; + } + if (this.nativeCloseFailure instanceof Error) { + throw (Error) this.nativeCloseFailure; + } + if (this.nativeClosed) { return; } - for (CFHandle cf : this.cfHandles.values()) { - cf.close(); + try { + for (CFHandle cf : this.cfHandles.values()) { + cf.close(); + } + this.cfHandles.clear(); + // JNI clears isOwningHandle before disposal completes. Only a normal + // return proves closure; a failed close must never release or transfer the lease. + this.rocksdb.close(); + this.nativeClosed = true; + } catch (RuntimeException | Error e) { + this.nativeCloseFailure = e; + throw e; } - this.cfHandles.clear(); - - this.rocksdb.close(); } public long totalSize() { diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RecoveryLock.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RecoveryLock.java new file mode 100644 index 0000000000..716ce5cd81 --- /dev/null +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RecoveryLock.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.hugegraph.backend.store.rocksdb; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.channels.OverlappingFileLockException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.HashSet; +import java.util.Set; + +/** Owns the descriptor and its physical-file reservation through actual close completion. */ +final class RecoveryLock implements AutoCloseable { + + private static final Set OWNERS = new HashSet<>(); + + private final FileChannel channel; + private final Object identity; + private final Path path; + private boolean closed; + private Throwable closeFailure; + + private RecoveryLock(FileChannel channel, Object identity, Path path) { + this.channel = channel; + this.identity = identity; + this.path = path; + } + + static RecoveryLock acquire(Path path, String data) throws IOException { + RecoveryLock owner; + synchronized (OWNERS) { + // CREATE_NEW never opens an already owned inode. Opening then closing a + // second descriptor can drop this process's existing POSIX record lock. + try { + Files.createFile(path); + } catch (FileAlreadyExistsException ignored) { + // Read its identity without opening another descriptor. + } + BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class); + if (!attributes.isRegularFile()) { + throw new IOException("Recovery lock must be a regular file: " + path); + } + Object identity = attributes.fileKey(); + // Prefer physical keys; the JDK permits providers with no fileKey. + // isSameFile also recognizes aliases and propagates an identity lookup failure. + for (RecoveryLock active : OWNERS) { + boolean same = identity != null && active.identity != null ? identity.equals(active.identity) : + Files.isSameFile(path, active.path); + if (same) { + throw new OverlappingFileLockException(); + } + } + owner = new RecoveryLock(FileChannel.open(path, StandardOpenOption.WRITE), identity, path); + OWNERS.add(owner); + } + try { + if (owner.channel.tryLock() == null) { + throw new IOException("Database is already open or recovering: " + data); + } + return owner; + } catch (IOException | RuntimeException | Error e) { + try { + owner.close(); + } catch (IOException | RuntimeException | Error closeFailure) { + e.addSuppressed(closeFailure); + } + throw e; + } + } + + synchronized boolean isOpen() { + return !this.closed && this.closeFailure == null; + } + + @Override + public synchronized void close() throws IOException { + if (this.closed) { + return; + } + if (this.closeFailure instanceof IOException) { + throw (IOException) this.closeFailure; + } + if (this.closeFailure instanceof RuntimeException) { + throw (RuntimeException) this.closeFailure; + } + if (this.closeFailure instanceof Error) { + throw (Error) this.closeFailure; + } + try { + // AbstractInterruptibleChannel flips isOpen before implCloseChannel returns. + // Reserve this file until close finishes, without blocking unrelated owners. + this.channel.close(); + } catch (IOException | RuntimeException | Error e) { + this.closeFailure = e; + throw e; + } + this.closed = true; + synchronized (OWNERS) { + OWNERS.remove(this); + } + } +} diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBIngester.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBIngester.java index 2d379b89ea..22913895c6 100644 --- a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBIngester.java +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBIngester.java @@ -27,7 +27,7 @@ import java.util.ArrayList; import java.util.List; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.util.Log; import org.rocksdb.ColumnFamilyHandle; import org.rocksdb.IngestExternalFileOptions; diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBIteratorPool.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBIteratorPool.java index 66acdd5ec9..2923446357 100644 --- a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBIteratorPool.java +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBIteratorPool.java @@ -20,7 +20,7 @@ import java.util.Queue; import java.util.concurrent.ArrayBlockingQueue; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.util.Log; import org.apache.hugegraph.util.StringEncoding; diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBOptions.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBOptions.java index 17148e0944..0f0d18f1a3 100644 --- a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBOptions.java +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBOptions.java @@ -61,6 +61,14 @@ public static synchronized RocksDBOptions instance() { "rocksdb-data/data" ); + public static final ConfigOption PROVIDER = + new ConfigOption<>( + "rocksdb.provider", + "The RocksDB-compatible runtime provider.", + allowValues("rocksdb", "topling"), + "rocksdb" + ); + public static final ConfigListOption DATA_DISKS = new ConfigListOption<>( "rocksdb.data_disks", diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSessions.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSessions.java index 7d0576e369..30bb285bc6 100644 --- a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSessions.java +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSessions.java @@ -17,6 +17,9 @@ package org.apache.hugegraph.backend.store.rocksdb; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Set; @@ -55,10 +58,48 @@ public RocksDBSessions(HugeConfig config, String database, String store) { public abstract String hardLinkSnapshot(String snapshotPath) throws RocksDBException; + // Only paths returned by this invocation of hardLinkSnapshot may be passed here. + void cleanupSnapshot(String snapshotPath) { + throw new UnsupportedOperationException("cleanupSnapshot"); + } + public abstract void reloadRocksDB() throws RocksDBException; public abstract void forceCloseRocksDB(); + public boolean databaseOpened() { + return this.opened(); + } + + public void clearTables(Collection tables) { + Session session = (Session) this.useSession(); + try { + List>> ranges = new ArrayList<>(); + for (String table : tables) { + Pair range = session.keyRange(table); + if (range != null) { + ranges.add(Pair.of(table, range)); + } + } + + for (Pair> entry : ranges) { + String table = entry.getLeft(); + byte[] first = entry.getRight().getLeft(); + byte[] last = entry.getRight().getRight(); + if (!Arrays.equals(first, last)) { + session.deleteRange(table, first, last); + } + session.delete(table, last); + } + session.commit(); + } catch (Throwable e) { + session.rollback(); + throw e; + } finally { + this.close(); + } + } + @Override public abstract Session session(); diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSnapshotRestore.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSnapshotRestore.java new file mode 100644 index 0000000000..e39079e6ee --- /dev/null +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSnapshotRestore.java @@ -0,0 +1,707 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.backend.store.rocksdb; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.FileChannel; +import java.nio.channels.OverlappingFileLockException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Properties; +import java.util.TreeSet; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.io.FileUtils; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.util.Log; +import org.slf4j.Logger; + +/** + * Fail-closed snapshot installation. The marker lives outside the data directory + * and the original checkpoint remains the recovery source until after reopening. + * A subsequent open retries the recorded checkpoint before native recovery runs. + */ +final class RocksDBSnapshotRestore { + + private static final Logger LOG = Log.logger(RocksDBSnapshotRestore.class); + + private final File data; + private final File wal; + private final File snapshot; + private final Path marker; + private final FileOperations files; + private final Path configuredWal; + private String generation; + private String operation; + private final List walLinks = new ArrayList<>(); + private final List walLinkTargets = new ArrayList<>(); + + RocksDBSnapshotRestore(String data, String wal, String snapshot, + FileOperations files) throws IOException { + this.data = dataDirectory(data); + this.wal = wal == null || wal.isEmpty() ? this.data : + new File(wal).getCanonicalFile(); + this.snapshot = new File(snapshot).getCanonicalFile(); + this.marker = marker(this.data.toString()); + this.files = files; + this.configuredWal = wal == null || wal.isEmpty() ? this.data.toPath() : + new File(wal).toPath().toAbsolutePath().normalize(); + Path rawWal = wal == null || wal.isEmpty() ? this.data.toPath() : + new File(wal).toPath().toAbsolutePath(); + this.validateWalLinks(rawWal); + Path component = this.configuredWal.getRoot(); + for (Path name : this.configuredWal) { + component = component.resolve(name); + if (Files.isSymbolicLink(component)) { + this.walLinks.add(component); + this.walLinkTargets.add(Files.readSymbolicLink(component)); + } + } + this.validatePaths(); + validateCheckpoint(this.snapshot); + } + + private static File dataDirectory(String data) throws IOException { + Path configured = new File(data).toPath().toAbsolutePath(); + // A direct DB alias becomes dangling while install replaces its target, + // changing canonical marker/lock identity. Stable parent aliases are fine. + while (configured.getFileName() != null && ".".equals(configured.getFileName().toString())) { + configured = configured.getParent(); + } + if (Files.isSymbolicLink(configured)) { + throw new IOException("Database directory cannot itself be a symbolic link; " + + "use a stable parent directory alias instead: " + configured); + } + return new File(data).getCanonicalFile(); + } + + private void validateWalLinks(Path rawWal) throws IOException { + boolean parentTraversal = false; + for (Path part : rawWal) { + parentTraversal |= "..".equals(part.toString()); + } + Path component = rawWal.getRoot(); + for (Path part : rawWal) { + component = component.resolve(part); + if (Files.isSymbolicLink(component)) { + if (parentTraversal && this.linkInsideData(component)) { + throw new IOException("WAL cannot combine a link inside data with '..': " + rawWal); + } + this.validateWalLinkTarget(component, 0); + } + } + } + + // Inspect target paths without normalizing away filesystem link/.. semantics. + // Only reject hidden links that data replacement would delete; external + // parent aliases and directly recorded simple WAL links remain supported. + private void validateWalLinkTarget(Path link, int depth) throws IOException { + if (depth >= 40) { + throw new IOException("WAL symbolic link target chain is too deep: " + link); + } + Path target = Files.readSymbolicLink(link); + if (!target.isAbsolute()) { + target = link.getParent().resolve(target); + } + Path component = target.getRoot(); + for (Path part : target) { + component = component.resolve(part); + if (Files.isSymbolicLink(component)) { + if (this.linkInsideData(component)) { + throw new IOException("WAL target chain contains an unrecorded link inside data: " + component); + } + this.validateWalLinkTarget(component, depth + 1); + } + } + } + + private boolean linkInsideData(Path link) throws IOException { + Path parent = link.getParent().toFile().getCanonicalFile().toPath(); + if (parent.startsWith(this.data.toPath())) { + return true; + } + // Parent bind aliases have different canonical strings but the same inode. + if (Files.exists(this.data.toPath())) { + for (Path ancestor = parent; ancestor != null; ancestor = ancestor.getParent()) { + if (Files.isSameFile(ancestor, this.data.toPath())) { + return true; + } + } + } + return false; + } + + private static void validateCheckpoint(File snapshot) throws IOException { + Path current = new File(snapshot, "CURRENT").toPath(); + if (!Files.isRegularFile(current, LinkOption.NOFOLLOW_LINKS) || Files.size(current) > 128L) { + throw new IOException("Checkpoint CURRENT is missing or invalid: " + snapshot); + } + String manifest = new String(Files.readAllBytes(current), StandardCharsets.UTF_8).trim(); + if (!manifest.matches("MANIFEST-[0-9]+") || + !Files.isRegularFile(new File(snapshot, manifest).toPath(), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Checkpoint MANIFEST is missing or invalid: " + snapshot); + } + } + + private static String generation(File snapshot) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + Path root = snapshot.toPath(); + try (Stream paths = Files.walk(root)) { + for (Path path : paths.sorted(Comparator.comparing(Path::toString)).collect(Collectors.toList())) { + BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS); + if (!attrs.isDirectory() && !attrs.isRegularFile()) { + throw new IOException("Invalid checkpoint entry: " + path); + } + digest.update((root.relativize(path) + "\n" + attrs.fileKey() + "\n").getBytes( + StandardCharsets.UTF_8)); + if (attrs.isDirectory()) { + continue; + } + digest.update((attrs.size() + "\n").getBytes(StandardCharsets.UTF_8)); + // SSTs are immutable. Their filesystem identity avoids rereading + // all table data on each retry; filesystems without fileKey use content. + if (path.toString().endsWith(".sst") && attrs.fileKey() != null) { + continue; + } + try (InputStream input = Files.newInputStream(path)) { + byte[] buffer = new byte[8192]; + int count; + while ((count = input.read(buffer)) != -1) { + digest.update(buffer, 0, count); + } + } + } + } + StringBuilder value = new StringBuilder(); + for (byte part : digest.digest()) { + value.append(String.format("%02x", part & 0xff)); + } + return value.toString(); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + private static String metadataDigest(Properties state) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + for (String key : new TreeSet<>(state.stringPropertyNames())) { + if ("metadata-sha256".equals(key)) { + continue; + } + digestMetadataString(digest, key); + digestMetadataString(digest, state.getProperty(key)); + } + StringBuilder value = new StringBuilder(); + for (byte part : digest.digest()) { + value.append(String.format("%02x", part & 0xff)); + } + return value.toString(); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + private static void digestMetadataString(MessageDigest digest, String value) { + // Length-prefixed UTF-16 code units preserve every Properties string, + // including escaped delimiters and unpaired surrogates, without ambiguity. + int length = value.length(); + for (int shift = 24; shift >= 0; shift -= 8) { + digest.update((byte) (length >>> shift)); + } + for (int i = 0; i < length; i++) { + char part = value.charAt(i); + digest.update((byte) (part >>> 8)); + digest.update((byte) part); + } + } + + private static void validateMetadata(Properties state, Path marker) throws IOException { + String checksum = state.getProperty("metadata-sha256"); + if (checksum == null || !checksum.matches("[0-9a-f]{64}") || + !checksum.equals(metadataDigest(state))) { + throw new IOException("Recovery metadata checksum missing or changed; preserve marker: " + marker); + } + } + + private void validateGeneration(Properties state) throws IOException { + this.generation = state.getProperty("generation"); + this.operation = state.getProperty("operation"); + if (this.generation == null || this.operation == null || + !this.operation.matches("[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}") || + !this.data.toString().equals(state.getProperty("data")) || + !this.generation.equals(generation(this.snapshot))) { + throw new IOException("Checkpoint generation changed or recovery identity missing: " + this.marker); + } + } + + private Path staging() { + // A sibling survives every supported nesting of the data and WAL roots. + return Paths.get(this.data + ".resume-staging-" + this.operation); + } + + private static boolean overlaps(File first, File second) throws IOException { + return contains(first, second) || contains(second, first); + } + + private static boolean contains(File root, File directory) throws IOException { + Path parent = root.toPath(); + Path child = directory.toPath(); + if (child.startsWith(parent)) { + return true; + } + // Parent bind aliases keep distinct canonical strings. Match existing + // physical prefixes, then compare their remaining directory suffixes. + for (Path left = parent; left != null; left = left.getParent()) { + if (Files.notExists(left)) { + continue; + } + for (Path right = child; right != null; right = right.getParent()) { + if (!Files.notExists(right) && Files.isSameFile(left, right)) { + Path prefix = left.relativize(parent); + Path suffix = right.relativize(child); + if (prefix.toString().isEmpty() || suffix.startsWith(prefix)) { + return true; + } + } + } + } + return false; + } + + private void validatePaths() throws IOException { + if (overlaps(this.snapshot, this.data) || contains(this.snapshot, this.wal)) { + throw new IOException("Checkpoint must not overlap data or WAL: " + this.snapshot); + } + } + + private static Path marker(String data) throws IOException { + return new File(dataDirectory(data) + ".resume-pending").toPath(); + } + + // Caller must hold the native owner lease or the same database recovery lock. + static boolean hasNoPendingRestore(String data) throws IOException { + return Files.notExists(marker(data), LinkOption.NOFOLLOW_LINKS); + } + + static RecoveryLock lock(String data) { + try { + Path directory = dataDirectory(data).toPath(); + Path parent = directory.getParent(); + if (parent == null) { + throw new IOException("Database directory cannot be a filesystem root: " + directory); + } + // Sibling guards must share the DB's backing parent. Binding just the + // DB directory hides those guards at aliases and cannot be replaced + // during restore (EBUSY). Mount the parent data root instead. + if (Files.isDirectory(directory) && + (System.getProperty("os.name").startsWith("Linux") ? isLinuxMountPoint(directory) : + !Files.getFileStore(directory).equals(Files.getFileStore(parent)))) { + throw new IOException("Database directory cannot itself be a mount point; " + + "mount its parent data root instead: " + directory); + } + Path path = new File(directory + ".resume-lock").toPath(); + Files.createDirectories(path.getParent()); + return RecoveryLock.acquire(path, data); + } catch (IOException | OverlappingFileLockException e) { + throw new BackendException("Cannot lock database for open/recovery: '%s'", e, data); + } + } + + private static boolean isLinuxMountPoint(Path directory) throws IOException { + // FileStore equality cannot detect a bind mount within the same filesystem. + // Read this process's namespace; kernel mountinfo escapes whitespace and backslashes. + for (String mount : Files.readAllLines(Paths.get("/proc/self/mountinfo"), + StandardCharsets.UTF_8)) { + String[] fields = mount.split(" ", 7); + if (fields.length < 7) { + throw new IOException("Invalid Linux mountinfo record"); + } + String path = fields[4].replace("\\040", " ").replace("\\011", "\t") + .replace("\\012", "\n").replace("\\134", "\\"); + if (directory.equals(Paths.get(path))) { + return true; + } + } + return false; + } + + static void unlock(RecoveryLock channel) { + if (channel != null) { + try { + channel.close(); + } catch (IOException e) { + throw new BackendException("Failed to release database recovery lock", e); + } + } + } + + static void unlock(RecoveryLock channel, Throwable failure) { + try { + unlock(channel); + } catch (RuntimeException | Error closeFailure) { + if (closeFailure != failure) { + failure.addSuppressed(closeFailure); + } + } + } + + static void start(String data, String wal, String snapshot) throws IOException { + Path marker = marker(data); + if (Files.exists(marker, LinkOption.NOFOLLOW_LINKS)) { + Properties pending = new Properties(); + try (InputStream input = Files.newInputStream(marker)) { + pending.load(input); + } + validateMetadata(pending, marker); + Path configuredWal = wal == null || wal.isEmpty() ? new File(data).getCanonicalFile().toPath() : + new File(wal).toPath().toAbsolutePath().normalize(); + if (!new File(snapshot).getCanonicalPath().equals(pending.getProperty("snapshot")) || + !configuredWal.toString().equals(pending.getProperty("configured-wal"))) { + throw new IOException("Another or incomplete checkpoint restore is pending: " + marker); + } + // prepareOpen will reconstruct links and revalidate the recorded source. + return; + } + new RocksDBSnapshotRestore(data, wal, snapshot, new FileOperations()).begin(); + } + + void begin() throws IOException { + this.validatePaths(); + Properties state = new Properties(); + this.generation = generation(this.snapshot); + this.operation = UUID.randomUUID().toString(); + state.setProperty("generation", this.generation); + state.setProperty("operation", this.operation); + state.setProperty("data", this.data.toString()); + state.setProperty("snapshot", this.snapshot.toString()); + state.setProperty("wal", this.wal.toString()); + state.setProperty("configured-wal", this.configuredWal.toString()); + state.setProperty("wal-links", Integer.toString(this.walLinks.size())); + for (int i = 0; i < this.walLinks.size(); i++) { + state.setProperty("wal-link-" + i, this.walLinks.get(i).toString()); + state.setProperty("wal-link-target-" + i, this.walLinkTargets.get(i).toString()); + } + state.setProperty("metadata-sha256", metadataDigest(state)); + // Publish only a complete, forced record without replacing an interrupted restore. + try { + this.files.publishMarker(this.marker, state); + } catch (FileAlreadyExistsException e) { + Properties pending = new Properties(); + try (InputStream input = Files.newInputStream(this.marker)) { + pending.load(input); + } + validateMetadata(pending, this.marker); + this.validateGeneration(pending); + state.setProperty("operation", this.operation); + state.setProperty("metadata-sha256", metadataDigest(state)); + if (!state.equals(pending)) { + throw new IOException("Another checkpoint restore is pending: " + this.marker, e); + } + } + try (FileChannel channel = FileChannel.open(this.marker, StandardOpenOption.WRITE)) { + channel.force(true); + } + this.files.forceDirectory(this.marker.getParent()); + } + + static RocksDBSnapshotRestore prepareOpen(String data, String wal) { + try { + Path marker = marker(data); + if (Files.notExists(marker, LinkOption.NOFOLLOW_LINKS)) { + return null; + } + if (!Files.isRegularFile(marker, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid recovery marker " + marker); + } + Properties state = new Properties(); + try (InputStream input = Files.newInputStream(marker)) { + state.load(input); + } + validateMetadata(state, marker); + String snapshot = state.getProperty("snapshot"); + String savedWal = state.getProperty("wal"); + if (snapshot == null || savedWal == null) { + throw new IOException("Incomplete recovery marker " + marker); + } + Path configuredWal = wal == null || wal.isEmpty() ? new File(data).getCanonicalFile().toPath() : + new File(wal).toPath().toAbsolutePath().normalize(); + if (!configuredWal.toString().equals(state.getProperty("configured-wal"))) { + throw new IOException("WAL configuration changed: " + marker); + } + String operation = state.getProperty("operation"); + String generation = state.getProperty("generation"); + File source = new File(snapshot).getCanonicalFile(); + File target = new File(data).getCanonicalFile(); + if (operation == null || !operation.matches("[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}") || + generation == null || !generation.matches("[0-9a-f]{64}") || + !target.toString().equals(state.getProperty("data")) || + !source.toString().equals(snapshot) || overlaps(source, target) || + !new File(savedWal).isAbsolute() || + !Paths.get(savedWal).normalize().toString().equals(savedWal) || + contains(source, new File(savedWal)) || + !generation(source).equals(generation)) { + throw new IOException("Checkpoint generation changed or recovery identity invalid: " + marker); + } + int links; + try { + links = Integer.parseInt(state.getProperty("wal-links")); + } catch (NumberFormatException e) { + throw new IOException("Invalid WAL links in recovery marker " + marker, e); + } + if (links < 0 || links > configuredWal.getNameCount()) { + throw new IOException("Invalid WAL link count: " + marker); + } + List recordedLinks = new ArrayList<>(); + List recordedTargets = new ArrayList<>(); + for (int i = 0; i < links; i++) { + String link = state.getProperty("wal-link-" + i); + String linkTarget = state.getProperty("wal-link-target-" + i); + if (link == null || linkTarget == null || !Paths.get(link).isAbsolute() || + !Paths.get(link).normalize().toString().equals(link) || + !configuredWal.startsWith(Paths.get(link))) { + throw new IOException("Invalid WAL link in recovery marker " + marker); + } + recordedLinks.add(Paths.get(link)); + recordedTargets.add(Paths.get(linkTarget)); + } + // The complete record is checksum-verified before reconstructing any alias. + // Real canonical WAL identity is checked again after links are restored. + for (int i = 0; i < links; i++) { + restoreLink(recordedLinks.get(i), recordedTargets.get(i)); + } + RocksDBSnapshotRestore restore = + new RocksDBSnapshotRestore(data, wal, snapshot, new FileOperations()); + if (!restore.wal.toString().equals(savedWal)) { + throw new IOException("WAL configuration changed during recovery: " + marker); + } + restore.validateGeneration(state); + restore.install(); + return restore; + } catch (IOException e) { + throw new BackendException("Pending snapshot recovery failed for '%s'; " + + "preserve the .resume-pending marker and checkpoint", e, data); + } + } + + void install() throws IOException { + this.validatePaths(); + if (!this.generation.equals(generation(this.snapshot))) { + throw new IOException("Checkpoint generation changed: " + this.snapshot); + } + // A preceding publication fsync may have failed. Persist its entry before retry deletes live data. + this.files.forceDirectory(this.marker.getParent()); + // Copy, never move: partial data copies and WAL failures must be retryable. + this.files.deleteDirectory(this.data); + this.files.copyDirectory(this.snapshot, this.data); + verifyTree(this.snapshot.toPath(), this.data.toPath()); + for (int i = 0; i < this.walLinks.size(); i++) { + restoreLink(this.walLinks.get(i), this.walLinkTargets.get(i)); + } + this.installWal(); + } + + private static void verifyTree(Path source, Path copy) throws IOException { + try (Stream paths = Files.walk(source)) { + for (Path path : (Iterable) paths::iterator) { + Path target = copy.resolve(source.relativize(path)); + if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + if (!Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Missing checkpoint directory: " + target); + } + } else if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) && + Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS)) { + verify(path.toFile(), target.toFile()); + } else { + throw new IOException("Invalid checkpoint file: " + path); + } + } + } + } + + private static void restoreLink(Path link, Path target) throws IOException { + if (!Files.exists(link, LinkOption.NOFOLLOW_LINKS)) { + Files.createDirectories(link.getParent()); + Files.createSymbolicLink(link, target); + } else if (!Files.isSymbolicLink(link) || !Files.readSymbolicLink(link).equals(target)) { + throw new IOException("WAL link changed during recovery: " + link); + } + } + + void complete() { + try { + this.validatePaths(); + // Only this operation's staging is reclaimed, after successful native reopen. + FileUtils.deleteDirectory(this.staging().toFile()); + Files.delete(this.marker); + // Do not consume the only retry source until marker removal is durable. + this.files.forceDirectory(this.marker.getParent()); + } catch (IOException e) { + throw new BackendException("Failed to finish snapshot recovery at '%s'", e, this.marker); + } + // Restore historically consumes its source. Only clean it after native + // reopen succeeded AND the marker no longer directs retries to it. + try { + this.validatePaths(); + FileUtils.deleteDirectory(this.snapshot); + } catch (IOException e) { + LOG.warn("Snapshot restored but source cleanup failed: {}", this.snapshot, e); + } + } + + private void installWal() throws IOException { + FileUtils.forceMkdir(this.wal); + if (this.data.equals(this.wal) || Files.isSameFile(this.data.toPath(), this.wal.toPath())) { + return; + } + List source = logs(this.data); + Path staging = this.staging(); + if (Files.exists(staging, LinkOption.NOFOLLOW_LINKS) && + !Files.isDirectory(staging, LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("Invalid operation staging directory: " + staging); + } + Files.createDirectories(staging); + // The persisted operation UUID names this directory exclusively. Retry + // republishes the verified checkpoint; retain all retired WAL until reopen. + for (File log : source) { + File staged = staging.resolve(log.getName()).toFile(); + this.files.copyFile(log, staged); + verify(log, staged); + } + for (File old : logs(this.wal)) { + Path aside = staging.resolve(old.getName() + ".aside"); + if (Files.exists(aside, LinkOption.NOFOLLOW_LINKS)) { + // An earlier attempt already preserved the original WAL. + Files.delete(old.toPath()); + } else { + this.files.move(old.toPath(), aside); + } + } + for (File log : source) { + this.files.move(staging.resolve(log.getName()), new File(this.wal, log.getName()).toPath()); + } + List installed = logs(this.wal); + if (installed.size() != source.size()) { + throw new IOException("Unexpected WAL files during checkpoint restore: " + this.wal); + } + for (File log : source) { + verify(log, new File(this.wal, log.getName())); + } + for (File log : source) { + Files.delete(log.toPath()); + } + } + + private static void verify(File source, File copy) throws IOException { + if (!FileUtils.contentEquals(source, copy)) { + throw new IOException("Checkpoint WAL content mismatch: " + copy); + } + } + + private static List logs(File directory) throws IOException { + File[] children = directory.listFiles(); + if (children == null) { + throw new IOException("Cannot list WAL directory " + directory); + } + List logs = new ArrayList<>(); + for (File child : children) { + if (child.getName().matches("[0-9]+\\.log")) { + if (!Files.isRegularFile(child.toPath(), LinkOption.NOFOLLOW_LINKS)) { + throw new IOException("WAL must be a regular file: " + child); + } + logs.add(child); + } + } + return logs; + } + + // Package-private seam for deterministic IO faults, without mocking native DBs. + static class FileOperations { + + void publishMarker(Path marker, Properties state) throws IOException { + Path staged = Files.createTempFile(marker.getParent(), + marker.getFileName() + ".staging-", ".tmp"); + try { + try (OutputStream output = this.markerOutput(staged)) { + state.store(output, "Pending RocksDB checkpoint restore; do not remove"); + } + try (FileChannel channel = FileChannel.open(staged, StandardOpenOption.WRITE)) { + channel.force(true); + } + // A same-directory hard link publishes atomically and refuses an existing name. + // If linking is unsupported, fail before installing or touching the live data. + Files.createLink(marker, staged); + } catch (IOException | RuntimeException | Error failure) { + try { + Files.deleteIfExists(staged); + } catch (IOException cleanup) { + failure.addSuppressed(cleanup); + } + throw failure; + } + Files.delete(staged); + } + + void forceDirectory(Path directory) throws IOException { + if (!System.getProperty("os.name").startsWith("Linux")) { + return; + } + try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) { + channel.force(true); + } + } + + OutputStream markerOutput(Path staged) throws IOException { + return Files.newOutputStream(staged, StandardOpenOption.WRITE); + } + + void copyDirectory(File source, File target) throws IOException { + FileUtils.copyDirectory(source, target); + } + + void deleteDirectory(File directory) throws IOException { + FileUtils.deleteDirectory(directory); + } + + void copyFile(File source, File target) throws IOException { + FileUtils.copyFile(source, target); + } + + void move(Path source, Path target) throws IOException { + Files.move(source, target); + } + } +} diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBStdSessions.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBStdSessions.java index c1cc1c5075..4815d15d89 100644 --- a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBStdSessions.java +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBStdSessions.java @@ -18,6 +18,8 @@ package org.apache.hugegraph.backend.store.rocksdb; import java.io.File; +import java.io.IOException; +import java.nio.channels.OverlappingFileLockException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -29,12 +31,13 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.tuple.Pair; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.serializer.BinarySerializer; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; import org.apache.hugegraph.backend.store.BackendEntryIterator; import org.apache.hugegraph.backend.store.rocksdb.RocksDBIteratorPool.ReusedRocksIterator; @@ -43,6 +46,7 @@ import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; +import org.apache.hugegraph.util.RocksDBRuntime; import org.apache.hugegraph.util.StringEncoding; import org.rocksdb.BlockBasedTableConfig; import org.rocksdb.BloomFilter; @@ -64,6 +68,7 @@ import org.rocksdb.RocksDBException; import org.rocksdb.RocksIterator; import org.rocksdb.SstFileManager; +import org.rocksdb.Status; import org.rocksdb.TableFormatConfig; import org.rocksdb.WriteBatch; import org.rocksdb.WriteOptions; @@ -79,27 +84,33 @@ public class RocksDBStdSessions extends RocksDBSessions { private final String dataPath; private final String walPath; - private volatile OpenedRocksDB rocksdb; + // Copies share the current native owner as well as its lifetime reference count. + private final AtomicReference rocksdb; private final AtomicInteger refCount; public RocksDBStdSessions(HugeConfig config, String database, String store, - String dataPath, String walPath) throws RocksDBException { + String dataPath, String walPath) throws + RocksDBException { super(config, database, store); this.config = config; this.dataPath = dataPath; this.walPath = walPath; - this.rocksdb = RocksDBStdSessions.openRocksDB(config, dataPath, walPath); + + this.rocksdb = new AtomicReference<>(RocksDBStdSessions.openRocksDB(config, dataPath, walPath)); this.refCount = new AtomicInteger(1); } public RocksDBStdSessions(HugeConfig config, String database, String store, String dataPath, String walPath, - List cfNames) throws RocksDBException { + List cfNames) throws + RocksDBException { super(config, database, store); this.config = config; this.dataPath = dataPath; this.walPath = walPath; - this.rocksdb = RocksDBStdSessions.openRocksDB(config, cfNames, dataPath, walPath); + + this.rocksdb = + new AtomicReference<>(RocksDBStdSessions.openRocksDB(config, cfNames, dataPath, walPath)); this.refCount = new AtomicInteger(1); this.ingestExternalFile(); @@ -113,7 +124,10 @@ private RocksDBStdSessions(HugeConfig config, String database, String store, this.walPath = origin.walPath; this.rocksdb = origin.rocksdb; this.refCount = origin.refCount; - this.refCount.incrementAndGet(); + synchronized (this.rocksdb) { + origin.checkValid(); + this.refCount.incrementAndGet(); + } } @Override @@ -123,12 +137,12 @@ public void open() throws Exception { @Override protected boolean opened() { - return this.rocksdb != null && this.rocksdb.isOwningHandle(); + return this.rocksdb.get().isOwningHandle(); } @Override public Set openedTables() { - return this.rocksdb.cfs(); + return this.rocksdb.get().cfs(); } @Override @@ -138,7 +152,7 @@ public synchronized void createTable(String... tables) List cfds = new ArrayList<>(); for (String table : tables) { - if (this.rocksdb.existCf(table)) { + if (this.rocksdb.get().existCf(table)) { continue; } ColumnFamilyDescriptor cfd = new ColumnFamilyDescriptor( @@ -156,7 +170,7 @@ public synchronized void createTable(String... tables) for (ColumnFamilyHandle cfh : cfhs) { String table = decode(cfh.getName()); - this.rocksdb.addCf(table, new OpenedRocksDB.CFHandle(this.rocksdb(), cfh)); + this.rocksdb.get().addCf(table, new OpenedRocksDB.CFHandle(this.rocksdb(), cfh)); } this.ingestExternalFile(); @@ -173,7 +187,7 @@ public synchronized void dropTable(String... tables) throws RocksDBException { */ List cfhs = new ArrayList<>(); for (String table : tables) { - OpenedRocksDB.CFHandle cfh = this.rocksdb.cf(table); + OpenedRocksDB.CFHandle cfh = this.rocksdb.get().cf(table); if (cfh == null) { continue; } @@ -187,39 +201,41 @@ public synchronized void dropTable(String... tables) throws RocksDBException { this.rocksdb().dropColumnFamilies(cfhs); for (String table : tables) { - OpenedRocksDB.CFHandle cfh = this.rocksdb.cf(table); + OpenedRocksDB.CFHandle cfh = this.rocksdb.get().cf(table); if (cfh == null) { continue; } cfh.destroy(); - this.rocksdb.removeCf(table); + this.rocksdb.get().removeCf(table); } } @Override public boolean existsTable(String table) { - return this.rocksdb.existCf(table); + return this.rocksdb.get().existCf(table); } @Override - public void reloadRocksDB() throws RocksDBException { - if (this.rocksdb.isOwningHandle()) { - this.rocksdb.close(); + public synchronized void reloadRocksDB() throws RocksDBException { + synchronized (this.rocksdb) { + RecoveryLock recoveryLock = this.rocksdb.get().closeForRestore(); + this.rocksdb.set(RocksDBStdSessions.openRocksDB(this.config, ImmutableList.of(), + this.dataPath, this.walPath, recoveryLock)); } - this.rocksdb = RocksDBStdSessions.openRocksDB(this.config, ImmutableList.of(), - this.dataPath, this.walPath); } @Override - public void forceCloseRocksDB() { - this.rocksdb().close(); + public synchronized void forceCloseRocksDB() { + synchronized (this.rocksdb) { + this.rocksdb.get().close(); + } } @Override public List property(String property) { try { if (property.equals(RocksDBMetrics.KEY_DISK_USAGE)) { - long size = this.rocksdb.totalSize(); + long size = this.rocksdb.get().totalSize(); return ImmutableList.of(String.valueOf(size)); } List values = new ArrayList<>(); @@ -252,34 +268,36 @@ public RocksDBSessions copy(HugeConfig config, String database, String store) { @Override public void createSnapshot(String snapshotPath) { - this.rocksdb.createCheckpoint(snapshotPath); + this.rocksdb.get().createCheckpoint(snapshotPath); } @Override - public void resumeSnapshot(String snapshotPath) { - File originDataDir = new File(this.dataPath); - File snapshotDir = new File(snapshotPath); - try { - /* - * Close current instance first - * NOTE: must close rocksdb instance before deleting file directory, - * if close after copying the snapshot directory to origin position, - * it may produce dirty data. - */ - this.forceCloseRocksDB(); - // Delete origin data directory - if (originDataDir.exists()) { - LOG.info("Delete origin data directory {}", originDataDir); - FileUtils.deleteDirectory(originDataDir); + public synchronized void resumeSnapshot(String snapshotPath) { + synchronized (this.rocksdb) { + File snapshotDir = new File(snapshotPath); + RecoveryLock recoveryLock = null; + try { + // Close the native handle before replacing files, but retain the + // sibling lock throughout marker creation, installation and reopen. + recoveryLock = this.rocksdb.get().closeForRestore(); + if (recoveryLock == null) { + recoveryLock = lockForOpen(this.dataPath); + } + RocksDBSnapshotRestore.start(this.dataPath, this.walPath, snapshotPath); + // openRocksDB owns cleanup once handed the lease; do not release it + // again if its newly opened native handle fails to close. + RecoveryLock reopenLock = recoveryLock; + recoveryLock = null; + this.rocksdb.set(RocksDBStdSessions.openRocksDB(this.config, ImmutableList.of(), + this.dataPath, this.walPath, reopenLock)); + } catch (Exception e) { + RocksDBSnapshotRestore.unlock(recoveryLock, e); + throw new BackendException("Failed to resume snapshot '%s' to '%s'", + e, snapshotDir, this.dataPath); + } catch (Error e) { + RocksDBSnapshotRestore.unlock(recoveryLock, e); + throw e; } - // Move snapshot directory to origin data directory - FileUtils.moveDirectory(snapshotDir, originDataDir); - LOG.info("Move snapshot directory {} to {}", snapshotDir, originDataDir); - // Reload rocksdb instance - this.reloadRocksDB(); - } catch (Exception e) { - throw new BackendException("Failed to resume snapshot '%s' to' %s'", - e, snapshotDir, this.dataPath); } } @@ -298,14 +316,62 @@ public String buildSnapshotPath(String snapshotPrefix) { } @Override - public String hardLinkSnapshot(String snapshotPath) throws RocksDBException { - String snapshotLinkPath = this.dataPath + "_temp"; - try (OpenedRocksDB rocksdb = openRocksDB(this.config, ImmutableList.of(), - snapshotPath, null)) { - rocksdb.createCheckpoint(snapshotLinkPath); - } - LOG.info("The snapshot {} has been hard linked to {}", snapshotPath, snapshotLinkPath); - return snapshotLinkPath; + public synchronized String hardLinkSnapshot(String snapshotPath) throws RocksDBException { + synchronized (this.rocksdb) { + try (RecoveryLock acquired = this.rocksdb.get().ownsRecoveryLock() ? + null : lockForOpen(this.dataPath)) { + if (!RocksDBSnapshotRestore.hasNoPendingRestore(this.dataPath)) { + throw new BackendException("Recovery may be pending for '%s'; reopen the database " + + "to retry its original checkpoint before creating another copy", + this.dataPath); + } + String snapshotLinkPath = this.dataPath + "_temp-" + java.util.UUID.randomUUID(); + try (OpenedRocksDB rocksdb = this.openSnapshot(snapshotPath)) { + rocksdb.createCheckpoint(snapshotLinkPath); + return snapshotLinkPath; + } catch (RocksDBException | RuntimeException | Error failure) { + try { + this.cleanupSnapshotUnderLock(snapshotLinkPath); + } catch (IOException | RuntimeException | Error cleanupFailure) { + if (cleanupFailure != failure) { + failure.addSuppressed(cleanupFailure); + } + } + throw failure; + } + } catch (IOException e) { + throw new BackendException("Failed to create temporary checkpoint for '%s'", e, this.dataPath); + } + } + } + + OpenedRocksDB openSnapshot(String snapshotPath) throws RocksDBException { + return openRocksDB(this.config, ImmutableList.of(), snapshotPath, null, null, true); + } + + @Override + synchronized void cleanupSnapshot(String snapshotPath) { + synchronized (this.rocksdb) { + try (RecoveryLock acquired = this.rocksdb.get().ownsRecoveryLock() ? + null : lockForOpen(this.dataPath)) { + this.cleanupSnapshotUnderLock(snapshotPath); + } catch (IOException | RocksDBException e) { + throw new BackendException("Failed to clean temporary checkpoint '%s'", e, snapshotPath); + } + } + } + + // The caller retains either the shared native owner's lease or an acquired lock. + private void cleanupSnapshotUnderLock(String snapshotPath) throws IOException { + // An existing or unreadable marker may own recovery material. Do not + // parse it here or call prepareOpen, which can install data. + if (!RocksDBSnapshotRestore.hasNoPendingRestore(this.dataPath)) { + LOG.warn("Preserving temporary checkpoint {} because recovery may be pending for {}", + snapshotPath, this.dataPath); + return; + } + FileUtils.deleteDirectory(new File(snapshotPath + "_temp")); + FileUtils.deleteDirectory(new File(snapshotPath)); } @Override @@ -315,32 +381,34 @@ public final Session session() { @Override protected final Session newSession() { - E.checkState(this.rocksdb.isOwningHandle(), "RocksDB has not been initialized"); + E.checkState(this.rocksdb.get().isOwningHandle(), "RocksDB has not been initialized"); return new StdSession(this.config()); } @Override protected synchronized void doClose() { - this.checkValid(); - - if (this.refCount.decrementAndGet() > 0) { - return; + synchronized (this.rocksdb) { + // Native restore failure does not remove the pool's owner reference. + // OpenedRocksDB.close is idempotent and retains any failed-close lease. + if (this.refCount.decrementAndGet() > 0) { + return; + } + assert this.refCount.get() == 0; + this.rocksdb.get().close(); } - assert this.refCount.get() == 0; - this.rocksdb.close(); } private void checkValid() { - E.checkState(this.rocksdb.isOwningHandle(), "It seems RocksDB has been closed"); + E.checkState(this.rocksdb.get().isOwningHandle(), "It seems RocksDB has been closed"); } private RocksDB rocksdb() { this.checkValid(); - return this.rocksdb.rocksdb(); + return this.rocksdb.get().rocksdb(); } private OpenedRocksDB.CFHandle cf(String cfName) { - OpenedRocksDB.CFHandle cfh = this.rocksdb.cf(cfName); + OpenedRocksDB.CFHandle cfh = this.rocksdb.get().cf(cfName); if (cfh == null) { throw new BackendException("Table '%s' is not opened", cfName); } @@ -355,7 +423,7 @@ private void ingestExternalFile() throws RocksDBException { } RocksDBIngester ingester = new RocksDBIngester(this.rocksdb()); // Ingest all *.sst files in each directory named cf name - for (String cf : this.rocksdb.cfs()) { + for (String cf : this.rocksdb.get().cfs()) { Path path = Paths.get(directory, cf); if (path.toFile().isDirectory()) { try (OpenedRocksDB.CFHandle cfh = this.cf(cf)) { @@ -365,61 +433,163 @@ private void ingestExternalFile() throws RocksDBException { } } + private static void finishRestore(RocksDBSnapshotRestore restore) { + if (restore != null) { + restore.complete(); + } + } + + private static void closeFailedOpen(OpenedRocksDB opened, RecoveryLock recoveryLock, Throwable failure) { + if (opened == null) { + RocksDBSnapshotRestore.unlock(recoveryLock, failure); + return; + } + try { + opened.close(); + } catch (RuntimeException | Error closeFailure) { + if (closeFailure != failure) { + failure.addSuppressed(closeFailure); + } + } + } + private static OpenedRocksDB openRocksDB(HugeConfig config, String dataPath, - String walPath) throws RocksDBException { - // Init options - Options options = new Options(); - RocksDBStdSessions.initOptions(config, options, options, options, options); - options.setWalDir(walPath); - SstFileManager sstFileManager = new SstFileManager(Env.getDefault()); - options.setSstFileManager(sstFileManager); - /* - * Open RocksDB at the first time - * Don't merge old CFs, we expect a clear DB when using this one - */ - RocksDB rocksdb = RocksDB.open(options, dataPath); - Map cfs = new ConcurrentHashMap<>(); - return new OpenedRocksDB(rocksdb, cfs, sstFileManager); + String walPath) throws + RocksDBException { + RocksDBRuntime.verify(config.get(RocksDBOptions.PROVIDER)); + RecoveryLock recoveryLock = lockForOpen(dataPath); + OpenedRocksDB opened = null; + try { + RocksDBSnapshotRestore restore = RocksDBSnapshotRestore.prepareOpen(dataPath, walPath); + // Init options + Options options = new Options(); + RocksDBStdSessions.initOptions(config, options, options, options, options); + options.setWalDir(walPath); + SstFileManager sstFileManager = new SstFileManager(Env.getDefault()); + options.setSstFileManager(sstFileManager); + + Map cfs = new ConcurrentHashMap<>(); + RocksDB rocksdb = RocksDB.open(options, dataPath); + opened = new OpenedRocksDB(rocksdb, cfs, sstFileManager, recoveryLock); + finishRestore(restore); + return opened; + } catch (RuntimeException | RocksDBException | Error e) { + closeFailedOpen(opened, recoveryLock, e); + throw e; + } } private static OpenedRocksDB openRocksDB(HugeConfig config, List cfNames, String dataPath, - String walPath) throws RocksDBException { - // Old CFs should always be opened - Set mergedCFs = RocksDBStdSessions.mergeOldCFs(dataPath, - cfNames); - List cfs = ImmutableList.copyOf(mergedCFs); - - // Init CFs options - List cfds = new ArrayList<>(cfs.size()); - for (String cf : cfs) { - ColumnFamilyDescriptor cfd = new ColumnFamilyDescriptor(encode(cf)); - ColumnFamilyOptions options = cfd.getOptions(); - RocksDBStdSessions.initOptions(config, null, null, options, options); - cfds.add(cfd); + String walPath) throws + RocksDBException { + return openRocksDB(config, cfNames, dataPath, walPath, null); + } + + private static OpenedRocksDB openRocksDB(HugeConfig config, List cfNames, + String dataPath, String walPath, + RecoveryLock heldLock) throws RocksDBException { + return openRocksDB(config, cfNames, dataPath, walPath, heldLock, false); + } + + private static OpenedRocksDB openRocksDB(HugeConfig config, List cfNames, + String dataPath, String walPath, + RecoveryLock heldLock, boolean readOnly) throws RocksDBException { + RocksDBRuntime.verify(config.get(RocksDBOptions.PROVIDER)); + // Checkpoints are immutable sources, not live databases to recover. + RecoveryLock recoveryLock = null; + OpenedRocksDB opened = null; + if (!readOnly) { + recoveryLock = heldLock != null ? heldLock : lockForOpen(dataPath); } + try { + RocksDBSnapshotRestore restore = readOnly ? null : RocksDBSnapshotRestore.prepareOpen(dataPath, walPath); + // Old CFs should always be opened + Set mergedCFs = RocksDBStdSessions.mergeOldCFs(dataPath, + cfNames); + List cfs = ImmutableList.copyOf(mergedCFs); + + // Init CFs options + List cfds = new ArrayList<>(cfs.size()); + for (String cf : cfs) { + ColumnFamilyDescriptor cfd = new ColumnFamilyDescriptor(encode(cf)); + ColumnFamilyOptions options = cfd.getOptions(); + RocksDBStdSessions.initOptions(config, null, null, options, options); + cfds.add(cfd); + } - // Init DB options - DBOptions options = new DBOptions(); - RocksDBStdSessions.initOptions(config, options, options, null, null); - if (walPath != null) { - options.setWalDir(walPath); + // Init DB options + DBOptions options = new DBOptions(); + RocksDBStdSessions.initOptions(config, options, options, null, null); + if (walPath != null) { + options.setWalDir(walPath); + } + SstFileManager sstFileManager = new SstFileManager(Env.getDefault()); + options.setSstFileManager(sstFileManager); + // Open RocksDB with CFs + List cfhs = new ArrayList<>(); + Map cfHandles = new ConcurrentHashMap<>(); + + RocksDB rocksdb = readOnly ? RocksDB.openReadOnly(options, dataPath, cfds, cfhs) : + RocksDB.open(options, dataPath, cfds, cfhs); + opened = new OpenedRocksDB(rocksdb, cfHandles, sstFileManager, recoveryLock); + + E.checkState(cfhs.size() == cfs.size(), + "Expect same size of cf-handles and cf-names"); + // Collect CF Handles + for (int i = 0; i < cfs.size(); i++) { + cfHandles.put(cfs.get(i), new OpenedRocksDB.CFHandle(rocksdb, cfhs.get(i))); + } + finishRestore(restore); + return opened; + } catch (RuntimeException | RocksDBException | Error e) { + closeFailedOpen(opened, recoveryLock, e); + throw e; } - SstFileManager sstFileManager = new SstFileManager(Env.getDefault()); - options.setSstFileManager(sstFileManager); + } - // Open RocksDB with CFs - List cfhs = new ArrayList<>(); - RocksDB rocksdb = RocksDB.open(options, dataPath, cfds, cfhs); + private static RecoveryLock lockForOpen(String dataPath) throws RocksDBException { + try { + return RocksDBSnapshotRestore.lock(dataPath); + } catch (BackendException e) { + // Opening a session must retain the checked RocksDB failure contract, + // including the contention classification used by Store's shared-CF path. + Throwable cause = e.getCause(); + String message; + boolean contention = false; + if (cause instanceof OverlappingFileLockException) { + contention = true; + message = "IO error: lock hold by current process: " + + dataPath + ".resume-lock: No locks available " + + "(database open/recovery lock)"; + } else if (cause instanceof IOException && cause.getMessage() != null && + cause.getMessage().startsWith("Database is already open or recovering:")) { + contention = true; + message = "IO error: database open/recovery lock held: " + + dataPath + ": No locks available"; + } else { + String reason = cause == null ? "" : ": " + cause; + message = e.getMessage() + reason; + } + throw new RecoveryLockException(message, contention, e); + } + } + + static final class RecoveryLockException extends RocksDBException { + + private static final long serialVersionUID = 1L; + + private final boolean contention; - E.checkState(cfhs.size() == cfs.size(), - "Expect same size of cf-handles and cf-names"); - // Collect CF Handles - Map cfHandles = new ConcurrentHashMap<>(); - for (int i = 0; i < cfs.size(); i++) { - cfHandles.put(cfs.get(i), new OpenedRocksDB.CFHandle(rocksdb, cfhs.get(i))); + RecoveryLockException(String message, boolean contention, Throwable cause) { + super(message, new Status(Status.Code.IOError, Status.SubCode.None, message)); + this.contention = contention; + this.initCause(cause); + } + + boolean isContention() { + return this.contention; } - return new OpenedRocksDB(rocksdb, cfHandles, sstFileManager); } private static Set mergeOldCFs(String path, @@ -729,9 +899,17 @@ public void open() { } @Override - public void close() { + public synchronized void close() { assert this.closeable(); - this.opened = false; + try { + this.batch.close(); + } finally { + try { + this.writeOptions.close(); + } finally { + this.opened = false; + } + } } @Override @@ -740,8 +918,11 @@ public boolean closed() { } @Override - public void reset() { - this.batch = new WriteBatch(); + public synchronized void reset() { + // A pool iterator may retain this session after its final detach. + if (this.opened) { + this.batch.clear(); + } } /** @@ -782,14 +963,18 @@ public Pair keyRange(String table) { RocksIterator iter = rocksdb().newIterator(cf.get())) { iter.seekToFirst(); if (!iter.isValid()) { + iter.status(); return null; } startKey = iter.key(); iter.seekToLast(); if (!iter.isValid()) { - return Pair.of(startKey, null); + iter.status(); + throw new BackendException("Missing last key in table '%s'", table); } endKey = iter.key(); + } catch (RocksDBException e) { + throw new BackendException("Failed to read key range of table '%s'", e, table); } return Pair.of(startKey, endKey); } diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBStore.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBStore.java index 3b6b54eadb..d28030b36c 100644 --- a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBStore.java +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBStore.java @@ -44,10 +44,10 @@ import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.serializer.MergeIterator; import org.apache.hugegraph.backend.store.AbstractBackendStore; import org.apache.hugegraph.backend.store.BackendAction; @@ -60,6 +60,7 @@ import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.exception.ConnectionException; +import org.apache.hugegraph.exception.NotSupportException; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.Consumers; import org.apache.hugegraph.util.E; @@ -103,6 +104,7 @@ public abstract class RocksDBStore extends AbstractBackendStore tableDBMap = this.tableDBMapping(); + List mainTables = new ArrayList<>(this.tableNames()); + for (String table : tableDBMap.keySet()) { + if (table.equals(HugeType.OLAP.string())) { + mainTables.removeAll(this.olapTables()); + } else { + mainTables.remove(table); + } + } + this.sessions.clearTables(mainTables); + for (Map.Entry entry : + tableDBMap.entrySet()) { + Collection tables; + if (entry.getKey().equals(HugeType.OLAP.string())) { + tables = this.olapTables(); + } else { + tables = Collections.singletonList(entry.getKey()); + } + entry.getValue().clearTables(tables); + } + } + @Override public void beginTx() { Lock readLock = this.storeLock.readLock(); @@ -718,8 +763,10 @@ public Map createSnapshot(String snapshotPrefix) { @Override public void resumeSnapshot(String snapshotPrefix, boolean deleteSnapshot) { - Lock readLock = this.storeLock.readLock(); - readLock.lock(); + Lock writeLock = this.storeLock.writeLock(); + Map temporarySnapshots = new HashMap<>(); + Throwable failure = null; + writeLock.lock(); try { if (!this.opened()) { return; @@ -731,6 +778,7 @@ public void resumeSnapshot(String snapshotPrefix, boolean deleteSnapshot) { LOG.debug("The origin data path: {}", entry.getKey()); if (!deleteSnapshot) { snapshotPath = sessions.hardLinkSnapshot(snapshotPath); + temporarySnapshots.put(snapshotPath, sessions); } LOG.debug("The snapshot data path: {}", snapshotPath); snapshotPaths.put(snapshotPath, sessions); @@ -752,9 +800,30 @@ public void resumeSnapshot(String snapshotPrefix, boolean deleteSnapshot) { } LOG.info("The store '{}' resume snapshot successfully", this); } catch (RocksDBException | IOException e) { - throw new BackendException("Failed to resume snapshot", e); + BackendException wrapped = new BackendException("Failed to resume snapshot", e); + failure = wrapped; + throw wrapped; + } catch (RuntimeException | Error e) { + failure = e; + throw e; } finally { - readLock.unlock(); + try { + for (Map.Entry entry : temporarySnapshots.entrySet()) { + try { + entry.getValue().cleanupSnapshot(entry.getKey()); + } catch (RuntimeException | Error cleanupFailure) { + if (failure != null) { + if (cleanupFailure != failure) { + failure.addSuppressed(cleanupFailure); + } + } else { + LOG.warn("Failed to clean temporary checkpoint {}", entry.getKey(), cleanupFailure); + } + } + } + } finally { + writeLock.unlock(); + } } } @@ -1104,10 +1173,16 @@ public synchronized void init() { Lock writeLock = this.storeLock().writeLock(); writeLock.lock(); try { - RocksDBSessions.Session session = super.session(HugeType.META); - String driverVersion = this.provider().driverVersion(); - this.meta.writeVersion(session, driverVersion); - LOG.info("Write down the backend version: {}", driverVersion); + RocksDBSessions sessions = super.db(HugeType.META); + sessions.useSession(); + try { + RocksDBSessions.Session session = super.session(HugeType.META); + String driverVersion = this.provider().driverVersion(); + this.meta.writeVersion(session, driverVersion); + LOG.info("Write down the backend version: {}", driverVersion); + } finally { + sessions.close(); + } } finally { writeLock.unlock(); } diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBTable.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBTable.java index 23e88def9b..b2082abe56 100644 --- a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBTable.java +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBTable.java @@ -26,23 +26,23 @@ import java.util.Set; import org.apache.commons.lang3.tuple.Pair; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.page.PageState; -import org.apache.hugegraph.backend.query.Aggregate; -import org.apache.hugegraph.backend.query.Aggregate.AggregateFunc; -import org.apache.hugegraph.backend.query.Condition.Relation; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdPrefixQuery; -import org.apache.hugegraph.backend.query.IdRangeQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.Aggregate; +import org.apache.hugegraph.query.Aggregate.AggregateFunc; +import org.apache.hugegraph.query.Condition.Relation; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdPrefixQuery; +import org.apache.hugegraph.query.IdRangeQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; import org.apache.hugegraph.backend.serializer.BinaryEntryIterator; import org.apache.hugegraph.backend.store.BackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; import org.apache.hugegraph.backend.store.BackendEntryIterator; import org.apache.hugegraph.backend.store.BackendTable; -import org.apache.hugegraph.backend.store.Shard; +import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions.Session; import org.apache.hugegraph.exception.NotSupportException; import org.apache.hugegraph.iterator.FlatMapperIterator; diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBTables.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBTables.java index becb685c58..74a52fef39 100644 --- a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBTables.java +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBTables.java @@ -22,10 +22,11 @@ import java.util.Collection; import java.util.List; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.Condition.Relation; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.Condition.Relation; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.serializer.BinarySerializer; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; @@ -234,7 +235,7 @@ public void delete(RocksDBSessions.Session session, BackendEntry entry) { * Only delete index by label will come here * Regular index delete will call eliminate() */ - for (BackendEntry.BackendColumn column : entry.columns()) { + for (BackendColumn column : entry.columns()) { // Don't assert entry.belongToMe(column), length-prefix is 1* session.deletePrefix(this.table(), column.name); } diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdbsst/RocksDBSstSessions.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdbsst/RocksDBSstSessions.java index b85ae5312b..052ab55387 100644 --- a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdbsst/RocksDBSstSessions.java +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdbsst/RocksDBSstSessions.java @@ -31,7 +31,7 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.tuple.Pair; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; import org.apache.hugegraph.backend.store.rocksdb.RocksDBIngester; import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions; diff --git a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdbsst/RocksDBSstStore.java b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdbsst/RocksDBSstStore.java index d7b75610f1..0e33cb3e50 100644 --- a/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdbsst/RocksDBSstStore.java +++ b/hugegraph-server/hugegraph-rocksdb/src/main/java/org/apache/hugegraph/backend/store/rocksdbsst/RocksDBSstStore.java @@ -19,7 +19,7 @@ import java.util.List; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.backend.store.BackendStoreProvider; import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions; import org.apache.hugegraph.backend.store.rocksdb.RocksDBStore; diff --git a/hugegraph-server/hugegraph-test/conf/jvm-test-module.options b/hugegraph-server/hugegraph-test/conf/jvm-test-module.options new file mode 100644 index 0000000000..ab1bbb0162 --- /dev/null +++ b/hugegraph-server/hugegraph-test/conf/jvm-test-module.options @@ -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. + +# Access required by TinkerPop and its Kryo dependencies in Java 17 tests. +# Runtime argument files contain only the subset required by production dependencies. +--add-exports=java.base/sun.security.x509=ALL-UNNAMED +--add-exports=java.base/sun.security.action=ALL-UNNAMED +--add-opens=java.base/java.io=ALL-UNNAMED +--add-opens=java.base/java.nio=ALL-UNNAMED +--add-opens=java.base/sun.nio.cs=ALL-UNNAMED +--add-opens=java.base/java.lang=ALL-UNNAMED +--add-opens=java.base/java.lang.invoke=ALL-UNNAMED +--add-opens=java.base/java.lang.reflect=ALL-UNNAMED +--add-opens=java.base/java.util=ALL-UNNAMED +--add-opens=java.base/java.util.concurrent=ALL-UNNAMED +--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED +--add-opens=java.base/java.net=ALL-UNNAMED diff --git a/hugegraph-server/hugegraph-test/pom.xml b/hugegraph-server/hugegraph-test/pom.xml index 259d5a9b9a..5e0e51fe8f 100644 --- a/hugegraph-server/hugegraph-test/pom.xml +++ b/hugegraph-server/hugegraph-test/pom.xml @@ -67,11 +67,11 @@ ${tinkerpop.version} - org.apache.tinkerpop - gremlin-groovy-test - 3.2.11 + com.google.inject + guice + 4.2.3 + provided - org.glassfish.jersey.test-framework.providers jersey-test-framework-provider-grizzly2 @@ -114,11 +114,18 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 + + + @{argLine} + "@${project.basedir}/../hugegraph-dist/src/assembly/static/bin/jvm-module.options" + "@${project.basedir}/conf/jvm-test-module.options" + + core-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -131,6 +138,7 @@ unit-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -143,6 +151,7 @@ api-test + true @@ -161,6 +170,7 @@ tinkerpop-structure-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -173,14 +183,22 @@ tinkerpop-process-test + true - -Dbuild.dir=${project.build.directory} + + @{argLine} + "@${project.basedir}/../hugegraph-dist/src/assembly/static/bin/jvm-module.options" + "@${project.basedir}/conf/jvm-test-module.options" + "-Dbuild.dir=${project.build.directory}" + ${basedir}/src/main/java/ ${basedir}/target/classes/ **/ProcessStandardTest.java + **/HugeGraphFeatureTest.java + **/HugeGraphProviderLifecycleTest.java @@ -210,12 +228,6 @@ org.jacoco jacoco-maven-plugin - 0.8.8 - - - org/apache/hugegraph/traversal/algorithm/*.class - - pre-test @@ -260,6 +272,8 @@ microbenchmarks + + false diff --git a/hugegraph-server/hugegraph-test/src/compatibility/java/CypherClientCompatibilityTest.java b/hugegraph-server/hugegraph-test/src/compatibility/java/CypherClientCompatibilityTest.java new file mode 100644 index 0000000000..da51b8d639 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/compatibility/java/CypherClientCompatibilityTest.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. + */ + +import java.util.Collections; + +import org.apache.hugegraph.api.cypher.CypherErrorMapper; +import org.apache.hugegraph.api.cypher.CypherModel; +import org.apache.hugegraph.rest.RestResult; +import org.apache.hugegraph.structure.gremlin.Response; +import org.junit.Assert; +import org.junit.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Run with an existing hugegraph-client jar (see docs/cypher-api.md). + * Kept outside the Server reactor to avoid a Server -> Client dependency cycle. + */ +public class CypherClientCompatibilityTest { + + private static final ObjectMapper JSON = new ObjectMapper(); + + @Test + public void testLegacySuccessAndFailure() { + for (int code : new int[]{200, 400}) { + String body = "{\"requestId\":\"request\",\"status\":{\"code\":" + code + + ",\"message\":\"original\",\"attributes\":{}}," + + "\"result\":{\"data\":" + (code == 200 ? "[]" : "null") + + ",\"meta\":{}}}"; + Response response = read(body); + Assert.assertEquals(code, response.status().code()); + Assert.assertEquals("original", response.status().message()); + Assert.assertTrue(response.status().attributes().isEmpty()); + } + } + + @Test + public void testCurrentSuccess() throws Exception { + Response response = read(JSON.writeValueAsString(CypherModel.dataOf( + "request", Collections.singletonList(Collections.singletonMap("value", 1))))); + Assert.assertEquals(200, response.status().code()); + Assert.assertEquals("request", response.requestId()); + Assert.assertTrue(response.status().attributes().isEmpty()); + Assert.assertEquals(1, response.result().size()); + } + + @Test + public void testCurrentFailure() throws Exception { + CypherModel.CypherError error = CypherErrorMapper.map( + new IllegalArgumentException("Invalid input 'R'")); + Response response = read(JSON.writeValueAsString( + CypherModel.failOf("request", "original failure", error))); + // CypherManager checks this status before handing the result to Hubble. + Assert.assertEquals(400, response.status().code()); + Assert.assertEquals("original failure", response.status().message()); + Assert.assertEquals("HugeGraph.Cypher.SyntaxError", JSON.valueToTree( + response.status().attributes()).at("/errors/0/code").asText()); + } + + @Test + public void testTopLevelErrorsNegativeControl() throws Exception { + ObjectNode body = (ObjectNode) JSON.readTree(JSON.writeValueAsString( + CypherModel.dataOf("request", Collections.emptyList()))); + body.putArray("errors"); + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> read(body.toString())); + Throwable cause = failure; + while (cause.getCause() != null) { + cause = cause.getCause(); + } + Assert.assertEquals("UnrecognizedPropertyException", cause.getClass().getSimpleName()); + Assert.assertTrue(cause.getMessage().contains("errors")); + } + + private static Response read(String body) { + return new RestResult(200, body, null).readObject(Response.class); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/DistributedTaskAdmissionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/DistributedTaskAdmissionTest.java new file mode 100644 index 0000000000..2449107736 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/DistributedTaskAdmissionTest.java @@ -0,0 +1,791 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.dist.RegisterUtil; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.meta.MetaManager; +import org.apache.hugegraph.meta.lock.LockResult; +import org.apache.hugegraph.meta.managers.TaskMetaManager; +import org.apache.hugegraph.job.EphemeralJob; +import org.apache.hugegraph.masterelection.GlobalMasterInfo; +import org.apache.hugegraph.task.DistributedTaskScheduler; +import org.apache.hugegraph.task.HugeTask; +import org.apache.hugegraph.task.TaskAndResultSchedulerTest.EmptyCallable; +import org.apache.hugegraph.task.TaskCallable; +import org.apache.hugegraph.task.TaskManager; +import org.apache.hugegraph.task.TaskScheduler; +import org.apache.hugegraph.task.TaskStatus; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.unit.FakeObjects; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class DistributedTaskAdmissionTest { + + private TaskMetaManager originalLocks; + + @Before + public void prepareLocks() { + this.originalLocks = Whitebox.getInternalState(MetaManager.instance(), "taskMetaManager"); + TaskMetaManager locks = Mockito.mock(TaskMetaManager.class); + Mockito.when(locks.tryLockTask(Mockito.anyString(), Mockito.anyString(), Mockito.anyString())) + .thenAnswer(invocation -> { + LockResult acquired = new LockResult(); + acquired.lockSuccess(true); + return acquired; + }); + Whitebox.setInternalState(MetaManager.instance(), "taskMetaManager", locks); + } + + @After + public void restoreLocks() { + Whitebox.setInternalState(MetaManager.instance(), "taskMetaManager", this.originalLocks); + } + + @Test + public void testCloseDuringPersistentRegistrationPreservesNewTask() throws Exception { + try (Fixture fixture = new Fixture("register_new")) { + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch finishRead = new CountDownLatch(1); + fixture.scheduler.beforeTaskRead = () -> { + reading.countDown(); + awaitUninterruptibly(finishRead); + }; + HugeTask task = task(false, new AtomicBoolean(), 9999917L); + ExecutorService requests = Executors.newSingleThreadExecutor(); + try { + fixture.scheduler.schedule(task); + Assert.assertTrue(reading.await(10L, TimeUnit.SECONDS)); + Future close = requests.submit(fixture.scheduler::close); + AtomicBoolean closed = Whitebox.getInternalState(fixture.scheduler, "closed"); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10L); + while (!closed.get() && System.nanoTime() < deadline) { + Thread.yield(); + } + Assert.assertTrue(closed.get()); + Assert.assertTrue(fixture.scheduler.pendingTasks() > 0); + Assert.assertFalse(close.isDone()); + finishRead.countDown(); + Assert.assertTrue(close.get(10L, TimeUnit.SECONDS)); + fixture.scheduler.beforeTaskRead = () -> { }; + Assert.assertEquals(TaskStatus.NEW, fixture.scheduler.task(task.id(), false).status()); + Assert.assertEquals(TaskStatus.NEW, task.status()); + fixture.assertWorkerClean(); + } finally { + finishRead.countDown(); + stop(requests); + } + } + } + + @Test + public void testAcceptedRemoteCancellationPreventsEarlyDrain() throws Exception { + try (Fixture fixture = new Fixture("cancel_gate")) { + CountDownLatch busy = new CountDownLatch(1); + CountDownLatch finishWorker = new CountDownLatch(1); + Future occupied = fixture.worker.submit(() -> { + busy.countDown(); + awaitUninterruptibly(finishWorker); + }); + ExecutorService requests = Executors.newFixedThreadPool(2); + GatedTask task = new GatedTask(9999918L, new EmptyCallable()); + try { + Assert.assertTrue(busy.await(10L, TimeUnit.SECONDS)); + fixture.scheduler.schedule(task); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + task.armed.set(true); + Future cancel = requests.submit(() -> fixture.scheduler.cancel(task)); + Assert.assertTrue(task.reached.await(10L, TimeUnit.SECONDS)); + Future close = requests.submit(fixture.scheduler::close); + Assert.assertTrue(fixture.scheduler.pendingTasks() > 0); + Assert.assertFalse(close.isDone()); + task.release.countDown(); + cancel.get(10L, TimeUnit.SECONDS); + Assert.assertTrue(close.get(10L, TimeUnit.SECONDS)); + Assert.assertEquals(TaskStatus.CANCELLING, fixture.scheduler.task(task.id(), false).status()); + } finally { + task.release.countDown(); + finishWorker.countDown(); + occupied.get(10L, TimeUnit.SECONDS); + stop(requests); + } + } + } + + @Test + public void testQueuedPersistentClosePreservesNewTask() throws Exception { + try (Fixture fixture = new Fixture("queued_new")) { + fixture.worker.armed.set(true); + HugeTask task = task(false, new AtomicBoolean(), 9999910L); + fixture.scheduler.schedule(task); + Assert.assertTrue(fixture.worker.reached.await(10L, TimeUnit.SECONDS)); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + Assert.assertTrue(fixture.scheduler.close()); + Assert.assertFalse(task.isCancelled()); + Assert.assertEquals(TaskStatus.NEW, task.status()); + Assert.assertEquals(TaskStatus.NEW, fixture.scheduler.task(task.id(), false).status()); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + for (boolean ephemeral : new boolean[]{true, false}) { + Assert.assertThrows(IllegalStateException.class, + () -> fixture.scheduler.schedule(task(ephemeral, new AtomicBoolean(), 9999911L))); + } + } + } + + @Test + public void testBusyWorkerLeavesPersistentBacklogInDatabase() throws Exception { + try (Fixture fixture = new Fixture("busy_new")) { + CountDownLatch busy = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + Future occupied = fixture.worker.submit(() -> { + busy.countDown(); + Assert.assertTrue(release.await(10L, TimeUnit.SECONDS)); + return null; + }); + try { + Assert.assertTrue(busy.await(10L, TimeUnit.SECONDS)); + HugeTask task = task(false, new AtomicBoolean(), 9999912L); + fixture.scheduler.schedule(task); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + Assert.assertTrue(fixture.worker.getQueue().isEmpty()); + Assert.assertEquals(TaskStatus.NEW, fixture.scheduler.task(task.id(), false).status()); + Assert.assertTrue(fixture.scheduler.close()); + } finally { + release.countDown(); + occupied.get(10L, TimeUnit.SECONDS); + } + } + } + + @Test + public void testCancellationFailurePreservesCallerTransaction() throws Exception { + try (Fixture fixture = new Fixture("failed_caller_tx")) { + fixture.worker.armed.set(true); + AtomicBoolean reject = new AtomicBoolean(true); + IllegalStateException failure = new IllegalStateException("Cancellation rejected"); + HugeTask task = new HugeTask<>(IdGenerator.of(-9999951L), null, new EphemeralJob() { + @Override + public String type() { + return "test"; + } + + @Override + public Object execute() { + return null; + } + }) { + @Override + public boolean cancel(boolean interrupt) { + if (reject.get()) { + throw failure; + } + return super.cancel(interrupt); + } + }; + task.type("test"); + task.name("failed-cancellation-caller-tx"); + fixture.scheduler.schedule(task); + Assert.assertTrue(fixture.worker.reached.await(10L, TimeUnit.SECONDS)); + try { + this.openCallerTransaction(fixture.graph); + Assert.assertThrows(HugeException.class, () -> fixture.scheduler.cancel(task), + caught -> Assert.assertSame(failure, caught.getCause())); + Assert.assertFalse(task.isCancelled()); + this.assertCallerTransactionCommits(fixture); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + } finally { + reject.set(false); + fixture.worker.release.countDown(); + } + fixture.assertWorkerClean(); + } + } + + @Test + public void testNormalWorkerTransactionCleanupControl() throws Exception { + try (Fixture fixture = new Fixture("normal_worker_control")) { + AtomicBoolean opened = new AtomicBoolean(); + HugeTask task = new HugeTask<>(IdGenerator.of(-9999950L), null, new EphemeralJob() { + @Override + public String type() { + return "test"; + } + + @Override + public Object execute() { + this.graph().vertices().hasNext(); + opened.set(this.graph().tx().isOpen()); + return null; + } + }); + task.type("test"); + task.name("normal-worker-cleanup"); + fixture.scheduler.schedule(task).get(10L, TimeUnit.SECONDS); + Assert.assertTrue(opened.get()); + Assert.assertFalse(task.isCancelled()); + Assert.assertEquals(TaskStatus.SUCCESS, task.status()); + fixture.assertWorkerClean(); + Assert.assertFalse(fixture.worker.submit(() -> fixture.graph.tx().isOpen()).get(10L, TimeUnit.SECONDS)); + } + } + + @Test + public void testQueuedDefaultCancellationPreservesCallerTransaction() throws Exception { + this.assertDefaultCancellationPreservesCallerTransaction(false); + } + + @Test + public void testRunningDefaultCancellationPreservesCallerTransaction() throws Exception { + this.assertDefaultCancellationPreservesCallerTransaction(true); + } + + private void assertDefaultCancellationPreservesCallerTransaction(boolean running) throws Exception { + try (Fixture fixture = new Fixture("caller_tx_" + running)) { + CountDownLatch started = new CountDownLatch(1); + CountDownLatch finish = new CountDownLatch(1); + if (!running) { + fixture.worker.armed.set(true); + } + HugeTask task = new HugeTask<>(IdGenerator.of(running ? -9999921L : -9999922L), null, + new EphemeralJob() { + @Override + public String type() { + return "test"; + } + + @Override + public Object execute() { + this.graph().vertices().hasNext(); + started.countDown(); + awaitUninterruptibly(finish); + return null; + } + }); + task.type("test"); + task.name("default-callback-caller-tx"); + try { + fixture.scheduler.schedule(task); + Assert.assertTrue((running ? started : fixture.worker.reached) + .await(10L, TimeUnit.SECONDS)); + this.openCallerTransaction(fixture.graph); + fixture.scheduler.cancel(task); + Assert.assertTrue(task.isCancelled()); + this.assertCallerTransactionCommits(fixture); + if (running) { + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + } + } finally { + finish.countDown(); + fixture.worker.release.countDown(); + } + fixture.assertWorkerClean(); + Assert.assertEquals(running ? 0L : 1L, started.getCount()); + } + } + + @Test + public void testCompletedCancellationPreservesCallerTransaction() throws Exception { + try (Fixture fixture = new Fixture("completed_caller_tx")) { + HugeTask task = task(true, new AtomicBoolean(), 9999923L); + fixture.scheduler.schedule(task).get(10L, TimeUnit.SECONDS); + fixture.assertWorkerClean(); + this.openCallerTransaction(fixture.graph); + fixture.scheduler.cancel(task); + Assert.assertFalse(task.isCancelled()); + this.assertCallerTransactionCommits(fixture); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + } + } + + private void openCallerTransaction(HugeGraph graph) { + graph.schema().vertexLabel("caller").useCustomizeStringId().create(); + graph.tx().commit(); + graph.addVertex(org.apache.tinkerpop.gremlin.structure.T.id, "pending", + org.apache.tinkerpop.gremlin.structure.T.label, "caller"); + Assert.assertTrue(graph.tx().isOpen()); + } + + private void assertCallerTransactionCommits(Fixture fixture) throws Exception { + Assert.assertTrue(fixture.graph.tx().isOpen()); + Assert.assertTrue(fixture.graph.vertices("pending").hasNext()); + fixture.graph.tx().commit(); + Assert.assertFalse(fixture.graph.tx().isOpen()); + Assert.assertTrue(fixture.database.submit(() -> { + try { + return fixture.graph.vertices("pending").hasNext(); + } finally { + fixture.params.closeTx(); + } + }).get(10L, TimeUnit.SECONDS)); + } + + @Test + public void testQueuedOuterCancellationPreservesCallerTransaction() throws Exception { + try (Fixture fixture = new Fixture("queued_future")) { + fixture.worker.armed.set(true); + AtomicBoolean ran = new AtomicBoolean(); + HugeTask task = task(true, ran, 9999913L); + Future dispatch = fixture.scheduler.schedule(task); + Assert.assertTrue(fixture.worker.reached.await(10L, TimeUnit.SECONDS)); + fixture.graph.schema().vertexLabel("caller").useCustomizeStringId().create(); + fixture.graph.addVertex(org.apache.tinkerpop.gremlin.structure.T.id, "pending", + org.apache.tinkerpop.gremlin.structure.T.label, "caller"); + Assert.assertTrue(dispatch.cancel(false)); + Assert.assertTrue(fixture.graph.tx().isOpen()); + Assert.assertTrue(fixture.graph.vertices("pending").hasNext()); + fixture.graph.tx().rollback(); + Assert.assertFalse(task.isCancelled()); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + Assert.assertTrue(fixture.scheduler.close()); + fixture.worker.release.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertFalse(ran.get()); + } + } + + @Test + public void testActiveOuterCancellationRetainsActualWorkerOwner() throws Exception { + try (Fixture fixture = new Fixture("active_future")) { + CountDownLatch started = new CountDownLatch(1); + CountDownLatch finish = new CountDownLatch(1); + HugeTask task = new HugeTask<>(IdGenerator.of(-9999914L), null, new EphemeralJob() { + @Override + public String type() { + return "test"; + } + + @Override + public Object execute() { + this.graph().vertices().hasNext(); + started.countDown(); + awaitUninterruptibly(finish); + return null; + } + }); + task.type("test"); + task.name("active-owner"); + Future dispatch = fixture.scheduler.schedule(task); + ExecutorService requests = Executors.newSingleThreadExecutor(); + try { + Assert.assertTrue(started.await(10L, TimeUnit.SECONDS)); + Assert.assertTrue(dispatch.cancel(true)); + Future close = requests.submit(fixture.scheduler::close); + Assert.assertTrue(fixture.scheduler.pendingTasks() > 0); + Assert.assertFalse(close.isDone()); + finish.countDown(); + Assert.assertTrue(close.get(10L, TimeUnit.SECONDS)); + fixture.assertWorkerClean(); + } finally { + finish.countDown(); + stop(requests); + } + } + } + + @Test + public void testAcceptedLocalCancellationRetainsCallbackAndSupportsNestedCancel() throws Exception { + try (Fixture fixture = new Fixture("nested_callback")) { + fixture.worker.armed.set(true); + HugeTask other = task(true, new AtomicBoolean(), 9999919L); + GatedTask task = new GatedTask(-9999915L, new EphemeralJob() { + @Override + public String type() { + return "test"; + } + + @Override + public Object execute() { + Assert.fail("Cancelled queued work must not execute"); + return null; + } + + @Override + protected void cancelled() { + fixture.scheduler.cancel(other); + } + }); + Future dispatch = fixture.scheduler.schedule(task); + Assert.assertTrue(fixture.worker.reached.await(10L, TimeUnit.SECONDS)); + fixture.scheduler.schedule(other); + task.armed.set(true); + ExecutorService requests = Executors.newSingleThreadExecutor(); + try { + Future cancel = requests.submit(() -> { + TaskManager.setContext("nested-cancel"); + try { + fixture.scheduler.cancel(task); + Assert.assertEquals("nested-cancel", TaskManager.getContext()); + } finally { + TaskManager.resetContext(); + } + }); + Assert.assertTrue(task.reached.await(10L, TimeUnit.SECONDS)); + Assert.assertTrue(dispatch.cancel(false)); + Assert.assertTrue(fixture.scheduler.pendingTasks() > 0); + task.release.countDown(); + cancel.get(10L, TimeUnit.SECONDS); + Assert.assertTrue(task.isCancelled()); + Assert.assertTrue(other.isCancelled()); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + } finally { + task.release.countDown(); + stop(requests); + } + } + } + + @Test + public void testCallbackCleanupCompletesBeforeGraphDrain() throws Exception { + try (Fixture fixture = new Fixture("callback_owner")) { + fixture.worker.armed.set(true); + CountDownLatch callback = new CountDownLatch(1); + CountDownLatch finish = new CountDownLatch(1); + HugeTask task = new HugeTask<>(IdGenerator.of(-9999916L), null, new EphemeralJob() { + @Override + public String type() { + return "test"; + } + + @Override + public Object execute() { + Assert.fail("Cancelled queued work must not execute"); + return null; + } + + @Override + protected void done() { + this.graph().vertices().hasNext(); + callback.countDown(); + awaitUninterruptibly(finish); + super.done(); + } + }); + task.type("test"); + task.name("callback-owner"); + Future dispatch = fixture.scheduler.schedule(task); + Assert.assertTrue(fixture.worker.reached.await(10L, TimeUnit.SECONDS)); + ExecutorService requests = Executors.newSingleThreadExecutor(); + ExecutorService callbacks = Executors.newSingleThreadExecutor(); + try { + Future cancel = callbacks.submit(() -> fixture.scheduler.cancel(task)); + Assert.assertTrue(callback.await(10L, TimeUnit.SECONDS)); + Assert.assertTrue(dispatch.cancel(true)); + Future close = requests.submit(fixture.scheduler::close); + Assert.assertTrue(fixture.scheduler.pendingTasks() > 0); + Assert.assertFalse(close.isDone()); + finish.countDown(); + cancel.get(10L, TimeUnit.SECONDS); + Assert.assertTrue(close.get(10L, TimeUnit.SECONDS)); + ThreadLocal owners = Whitebox.getInternalState(fixture.graph.tx(), "transactions"); + Assert.assertNull(callbacks.submit(owners::get).get(10L, TimeUnit.SECONDS)); + Assert.assertNull(callbacks.submit(TaskManager::getContext).get(10L, TimeUnit.SECONDS)); + } finally { + finish.countDown(); + stop(requests); + stop(callbacks); + } + } + } + + @Test + public void testBlockedCronStillFencesAdmissionAndTimesOutClose() throws Exception { + try (Fixture fixture = new Fixture("blocked_cron", 1L)) { + CountDownLatch querying = new CountDownLatch(1); + CountDownLatch finishQuery = new CountDownLatch(1); + fixture.scheduler.beforeTaskQuery = () -> { + querying.countDown(); + awaitUninterruptibly(finishQuery); + }; + ExecutorService requests = Executors.newSingleThreadExecutor(); + Future cron = fixture.cron.submit(fixture.scheduler::cronSchedule); + try { + Assert.assertTrue(querying.await(10L, TimeUnit.SECONDS)); + Future close = requests.submit(fixture.scheduler::close); + AtomicBoolean closed = Whitebox.getInternalState(fixture.scheduler, "closed"); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L); + while (!closed.get() && System.nanoTime() < deadline) { + Thread.yield(); + } + Assert.assertTrue(closed.get()); + Assert.assertThrows(IllegalStateException.class, + () -> fixture.scheduler.schedule(task(true, new AtomicBoolean(), 9999919L))); + // This exercises the real cron barrier, not a mocked drain method. + Assert.assertFalse(close.get(10L, TimeUnit.SECONDS)); + Assert.assertFalse(cron.isDone()); + finishQuery.countDown(); + cron.get(10L, TimeUnit.SECONDS); + Assert.assertTrue(fixture.scheduler.close()); + fixture.assertWorkerClean(); + } finally { + finishQuery.countDown(); + cron.get(10L, TimeUnit.SECONDS); + stop(requests); + } + } + } + + @Test + public void testDeleteAfterLocalOwnerFinishesStillRemovesRecord() throws Exception { + try (Fixture fixture = new Fixture("delete_finished_owner")) { + DeleteCompletionCallable.started = new CountDownLatch(1); + DeleteCompletionCallable.finish = new CountDownLatch(1); + HugeTask task = new HugeTask<>(IdGenerator.of(9999952L), null, new DeleteCompletionCallable()); + task.type("test"); + task.name("delete-finished-owner"); + Map> running = Whitebox.getInternalState(fixture.scheduler, "runningTasks"); + Map> observed = Mockito.spy(running); + AtomicBoolean intercept = new AtomicBoolean(); + Mockito.doAnswer(invocation -> { + HugeTask value = (HugeTask) invocation.callRealMethod(); + if (intercept.compareAndSet(true, false)) { + Assert.assertSame(task, value); + DeleteCompletionCallable.finish.countDown(); + // The actual worker completes persistence and releases ownership before delete continues. + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertEquals(TaskStatus.SUCCESS, task.status()); + Assert.assertEquals(TaskStatus.SUCCESS, fixture.scheduler.task(task.id(), false).status()); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + } + return value; + }).when(observed).get(task.id()); + Whitebox.setInternalState(fixture.scheduler, "runningTasks", observed); + try { + fixture.scheduler.schedule(task); + Assert.assertTrue(DeleteCompletionCallable.started.await(10L, TimeUnit.SECONDS)); + intercept.set(true); + Assert.assertNotNull(fixture.scheduler.delete(task.id(), false)); + Assert.assertFalse(intercept.get()); + Assert.assertThrows(org.apache.hugegraph.exception.NotFoundException.class, + () -> fixture.scheduler.task(task.id(), false)); + fixture.assertWorkerClean(); + } finally { + DeleteCompletionCallable.finish.countDown(); + } + } + } + + public static class DeleteCompletionCallable extends org.apache.hugegraph.job.UserJob { + + private static CountDownLatch started; + private static CountDownLatch finish; + + @Override + public String type() { + return "test"; + } + + @Override + public Object execute() { + started.countDown(); + awaitUninterruptibly(finish); + return null; + } + } + + private static void awaitUninterruptibly(CountDownLatch release) { + while (release.getCount() != 0L) { + try { + Assert.assertTrue(release.await(10L, TimeUnit.SECONDS)); + } catch (InterruptedException ignored) { + // Keep the real owner active after logical cancellation. + } + } + } + + private static void stop(ExecutorService executor) throws InterruptedException { + executor.shutdownNow(); + Assert.assertTrue(executor.awaitTermination(10L, TimeUnit.SECONDS)); + } + + private static HugeTask task(boolean ephemeral, AtomicBoolean ran, long id) { + HugeTask task; + if (ephemeral) { + task = new HugeTask<>(IdGenerator.of(-id), null, new EphemeralJob() { + @Override + public String type() { + return "test"; + } + + @Override + public Object execute() { + ran.set(true); + return null; + } + }); + } else { + task = new HugeTask<>(IdGenerator.of(id), null, new EmptyCallable()); + } + task.type("test"); + task.name("admission-test"); + return task; + } + + private static class Fixture implements AutoCloseable { + + private final HugeGraph graph; + private final ScheduledThreadPoolExecutor cron = new ScheduledThreadPoolExecutor(1); + private final ExecutorService database = Executors.newSingleThreadExecutor(); + private final GatedExecutor worker = new GatedExecutor(); + private final TestScheduler scheduler; + private final HugeGraphParams params; + private final TaskScheduler originalScheduler; + private final Map registry; + + Fixture(String suffix) { + this(suffix, 600L); + } + + Fixture(String suffix, long schedulePeriod) { + RegisterUtil.registerBackends(); + HugeConfig config = FakeObjects.newConfig(); + config.setProperty("backend", "memory"); + config.setProperty("serializer", "text"); + config.setProperty("store", "distributed_admission_" + suffix); + config.setProperty("task.schedule_period", schedulePeriod); + config.setProperty("task.retry", 0); + this.graph = HugeFactory.open(config); + this.graph.initBackend(); + this.graph.serverStarted(GlobalMasterInfo.master("distributed-admission-test")); + this.params = Whitebox.getInternalState(this.graph, "params"); + this.registry = Whitebox.getInternalState(TaskManager.instance(), "schedulers"); + this.originalScheduler = this.registry.get(this.params); + Assert.assertTrue(this.originalScheduler.close()); + this.scheduler = new TestScheduler(this.params, this.cron, this.database, this.worker); + this.scheduler.init(); + Assert.assertTrue(this.registry.replace(this.params, this.originalScheduler, this.scheduler)); + } + + void assertWorkerClean() throws Exception { + this.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertEquals(0, this.scheduler.pendingTasks()); + ThreadLocal owners = Whitebox.getInternalState(this.graph.tx(), "transactions"); + Assert.assertNull(this.worker.submit(owners::get).get(10L, TimeUnit.SECONDS)); + Assert.assertNull(this.worker.submit(TaskManager::getContext).get(10L, TimeUnit.SECONDS)); + } + + @Override + public void close() throws Exception { + this.worker.release.countDown(); + try { + Assert.assertTrue(this.scheduler.close()); + this.scheduler.closeDatabaseOwner(); + } finally { + Assert.assertTrue(this.registry.replace(this.params, this.scheduler, this.originalScheduler)); + for (ExecutorService executor : new ExecutorService[]{this.worker, this.database, this.cron}) { + stop(executor); + } + try { + this.graph.close(); + } finally { + HugeFactory.remove(this.graph); + } + } + } + } + + private static class GatedTask extends HugeTask { + + private final AtomicBoolean armed = new AtomicBoolean(); + private final CountDownLatch reached = new CountDownLatch(1); + private final CountDownLatch release = new CountDownLatch(1); + + GatedTask(long id, TaskCallable callable) { + super(IdGenerator.of(id), null, callable); + this.type("test"); + this.name("accepted-cancel"); + } + + @Override + public boolean completed() { + if (this.armed.compareAndSet(true, false)) { + this.reached.countDown(); + awaitUninterruptibly(this.release); + } + return super.completed(); + } + } + + private static class GatedExecutor extends ThreadPoolExecutor { + + private final AtomicBoolean armed = new AtomicBoolean(); + private final CountDownLatch reached = new CountDownLatch(1); + private final CountDownLatch release = new CountDownLatch(1); + + GatedExecutor() { + super(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); + } + + @Override + protected void beforeExecute(Thread thread, Runnable work) { + if (this.armed.compareAndSet(true, false)) { + this.reached.countDown(); + awaitUninterruptibly(this.release); + } + } + } + + private static class TestScheduler extends DistributedTaskScheduler { + + private volatile Runnable beforeTaskRead = () -> { }; + private volatile Runnable beforeTaskQuery = () -> { }; + + @Override + protected Iterator> queryTaskWithoutResultByStatus(TaskStatus status) { + this.beforeTaskQuery.run(); + return super.queryTaskWithoutResultByStatus(status); + } + + @Override + public HugeTask task(Id id, boolean withResult) { + this.beforeTaskRead.run(); + return super.task(id, withResult); + } + + TestScheduler(HugeGraphParams graph, ScheduledThreadPoolExecutor cron, + ExecutorService database, ExecutorService worker) { + super(graph, cron, database, worker, worker, worker, worker); + } + + void closeDatabaseOwner() { + this.call(() -> { + this.tx().close(); + this.graph.closeTx(); + return null; + }); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/GraphDropPendingTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/GraphDropPendingTest.java new file mode 100644 index 0000000000..a7a0c57125 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/GraphDropPendingTest.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.hugegraph; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.commons.io.FileUtils; +import org.apache.hugegraph.config.CoreOptions; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.ServerOptions; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.dist.RegisterUtil; +import org.apache.hugegraph.event.EventHub; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.job.SysJob; +import org.apache.hugegraph.masterelection.GlobalMasterInfo; +import org.apache.hugegraph.meta.MetaManager; +import org.apache.hugegraph.space.GraphSpace; +import org.apache.hugegraph.task.HugeTask; +import org.apache.hugegraph.task.TaskScheduler; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.unit.FakeObjects; +import org.apache.hugegraph.util.Events; +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.structure.T; +import org.junit.Test; +import org.mockito.Mockito; + +public class GraphDropPendingTest { + + @Test + public void testLocalDropKeepsDataAndRegistrationUntilCancelledWorkerExits() throws Exception { + try (Fixture fixture = new Fixture("local_pending", false)) { + BlockingJob job = fixture.start(); + fixture.scheduler.cancel(job.task()); + Assert.assertTrue(job.task().isCancelled()); + HugeException refusal = Assert.assertThrows(HugeException.class, fixture::drop); + Assert.assertContains("Tasks are still active", refusal.getMessage()); + Assert.assertContains("task admission remains closed", refusal.getMessage()); + Assert.assertContains("restart to resume task admission", refusal.getMessage()); + BlockingJob late = new BlockingJob(); + late.release.countDown(); + Assert.assertThrows(IllegalStateException.class, () -> fixture.scheduler.schedule( + new HugeTask<>(IdGenerator.of(9997001L), null, late))); + fixture.assertRetained(); + job.release.countDown(); + fixture.scheduler.waitUntilAllTasksCompleted(10L); + fixture.drop(); + Assert.assertTrue(fixture.graph.closed()); + Assert.assertNull(fixture.manager.graph(fixture.graph.spaceGraphName())); + Assert.assertFalse(Files.exists(fixture.file)); + Assert.assertEquals(1, fixture.dropEvents); + } + } + + @Test + public void testPdDropPreservesMetadataUntilLocalTasksDrain() throws Exception { + try (Fixture fixture = new Fixture("pd_pending", true)) { + BlockingJob job = fixture.start(); + HugeException refusal = Assert.assertThrows(HugeException.class, fixture::drop); + Assert.assertContains("Tasks are still active", refusal.getMessage()); + Assert.assertContains("task admission remains closed", refusal.getMessage()); + Assert.assertContains("restart to resume task admission", refusal.getMessage()); + BlockingJob late = new BlockingJob(); + late.release.countDown(); + Assert.assertThrows(IllegalStateException.class, () -> fixture.scheduler.schedule( + new HugeTask<>(IdGenerator.of(9997001L), null, late))); + fixture.assertRetained(); + Assert.assertTrue(fixture.metadataPresent.get()); + job.release.countDown(); + fixture.scheduler.waitUntilAllTasksCompleted(10L); + fixture.drop(); + Assert.assertFalse(fixture.metadataPresent.get()); + Assert.assertTrue(fixture.graph.closed()); + Assert.assertNull(fixture.manager.graph(fixture.graph.spaceGraphName())); + } + } + + @Test + public void testConfiguredWaitAllowsShortTaskToFinishBeforePdDrop() throws Exception { + try (Fixture fixture = new Fixture("pd_wait", true)) { + fixture.graph.configuration().setProperty(CoreOptions.TASK_WAIT_TIMEOUT.name(), 2L); + BlockingJob job = fixture.start(); + CountDownLatch fenced = new CountDownLatch(1); + TaskScheduler scheduler = Mockito.mock(TaskScheduler.class, + org.mockito.AdditionalAnswers.delegatesTo(fixture.scheduler)); + Mockito.doAnswer(invocation -> { + boolean drained = fixture.scheduler.close(); + if (!drained) { + fenced.countDown(); + } + return drained; + }).when(scheduler).close(); + HugeGraph proxy = Mockito.mock(HugeGraph.class, org.mockito.AdditionalAnswers.delegatesTo(fixture.graph)); + Mockito.doReturn(scheduler).when(proxy).taskScheduler(); + fixture.graphs.put(fixture.graph.spaceGraphName(), proxy); + ExecutorService caller = Executors.newSingleThreadExecutor(); + try { + Future deletion = caller.submit(fixture::drop); + Assert.assertTrue(fenced.await(10L, TimeUnit.SECONDS)); + Assert.assertTrue(fixture.metadataPresent.get()); + job.release.countDown(); + deletion.get(10L, TimeUnit.SECONDS); + Assert.assertTrue(fixture.graph.closed()); + Assert.assertFalse(fixture.metadataPresent.get()); + } finally { + job.release.countDown(); + caller.shutdown(); + Assert.assertTrue(caller.awaitTermination(10L, TimeUnit.SECONDS)); + fixture.graphs.replace(fixture.graph.spaceGraphName(), proxy, fixture.graph); + } + } + } + + @Test + public void testSchedulerCloseFailureDoesNotRemovePdMetadata() throws Exception { + try (Fixture fixture = new Fixture("pd_close_failure", true)) { + Graph proxy = Mockito.mock(HugeGraph.class, Mockito.withSettings().defaultAnswer( + org.mockito.AdditionalAnswers.delegatesTo(fixture.graph))); + TaskScheduler failed = Mockito.spy(fixture.scheduler); + Mockito.doThrow(new IllegalStateException("scheduler close failed")).when(failed).close(); + Mockito.doReturn(failed).when((HugeGraph) proxy).taskScheduler(); + fixture.graphs.put(fixture.graph.spaceGraphName(), proxy); + try { + RuntimeException error = Assert.assertThrows(RuntimeException.class, fixture::drop); + Assert.assertContains("scheduler close failed", error.getMessage()); + Assert.assertTrue(fixture.metadataPresent.get()); + Assert.assertFalse(fixture.graph.closed()); + Assert.assertTrue(Files.exists(fixture.file)); + } finally { + fixture.graphs.put(fixture.graph.spaceGraphName(), fixture.graph); + } + } + } + + @Test + public void testMissingGraphReportsArgumentErrorFromInitializedManager() throws Exception { + try (Fixture fixture = new Fixture("missing_graph", true)) { + IllegalArgumentException error = Assert.assertThrows(IllegalArgumentException.class, + () -> fixture.manager.dropGraph("DEFAULT", "missing", true)); + Assert.assertContains("doesn't exist", error.getMessage()); + fixture.assertRetained(); + Assert.assertTrue(fixture.metadataPresent.get()); + } + } + + @Test + public void testGraphSpaceSharedMetadataSurvivesRefusedDrop() throws Exception { + try (Fixture fixture = new Fixture("space_pending", true)) { + BlockingJob job = fixture.start(); + HugeException refusal = Assert.assertThrows(HugeException.class, + () -> fixture.manager.clearGraphSpace("DEFAULT")); + Assert.assertContains("Tasks are still active", refusal.getMessage()); + Assert.assertFalse(fixture.sharedMetadataCleared.get()); + Assert.assertSame(fixture.graph, fixture.manager.graph(fixture.graph.spaceGraphName())); + job.release.countDown(); + } + } + + private static final class Fixture implements AutoCloseable { + + private final Path directory; + private final Path file; + private final GraphManager manager; + private final Map graphs; + private final HugeGraph graph; + private final HugeGraph neighbor; + private final HugeGraphParams params; + private final TaskScheduler scheduler; + private final boolean pd; + private final AtomicBoolean metadataPresent = new AtomicBoolean(true); + private final AtomicBoolean sharedMetadataCleared = new AtomicBoolean(); + private final List jobs = new ArrayList<>(); + private int dropEvents; + + private Fixture(String name, boolean pd) throws Exception { + RegisterUtil.registerBackends(); + this.pd = pd; + this.directory = Files.createTempDirectory("graph-drop-core-"); + HugeConfig config = FakeObjects.newConfig(); + config.setProperty(ServerOptions.USE_PD.name(), false); + config.setProperty(ServerOptions.GRAPH_LOAD_FROM_LOCAL_CONFIG.name(), false); + config.setProperty(ServerOptions.ENABLE_DYNAMIC_CREATE_DROP.name(), true); + config.setProperty(ServerOptions.GRAPHS.name(), this.directory.toString()); + EventHub events = new EventHub("drop-core-" + name); + this.manager = new GraphManager(config, events); + this.graphs = Whitebox.getInternalState(this.manager, "graphs"); + this.graph = this.open(name); + this.neighbor = this.open(name + "_neighbor"); + this.params = Whitebox.getInternalState(this.graph, "params"); + this.scheduler = this.graph.taskScheduler(); + this.file = this.directory.resolve(name + ".properties"); + events.listen(Events.GRAPH_DROP, event -> { + if (event.args()[0] == this.graph) { + this.dropEvents++; + } + return null; + }); + if (pd) { + // Replace the external PD service, never bypass manager initialization. + Whitebox.setInternalState(this.manager, "PDExist", true); + MetaManager meta = Mockito.mock(MetaManager.class); + Whitebox.setInternalState(this.manager, "metaManager", meta); + Mockito.when(meta.graphConfigs("DEFAULT")).thenReturn(java.util.Collections.emptyMap()); + Mockito.doAnswer(invocation -> { + this.metadataPresent.set(false); + return null; + }).when(meta).removeGraphConfig(Mockito.eq("DEFAULT"), Mockito.eq(name)); + Mockito.doAnswer(invocation -> { + this.sharedMetadataCleared.set(true); + return null; + }).when(meta).clearGraphAuth("DEFAULT"); + Map spaces = Whitebox.getInternalState(this.manager, "graphSpaces"); + spaces.put("DEFAULT", Mockito.mock(GraphSpace.class)); + } + } + + private HugeGraph open(String name) throws Exception { + HugeConfig config = FakeObjects.newConfig(); + config.setProperty(Graph.GRAPH, HugeFactory.class.getName()); + config.setProperty("backend", "memory"); + config.setProperty("serializer", "text"); + config.setProperty("store", name); + config.setProperty(CoreOptions.TASK_WAIT_TIMEOUT.name(), 0L); + Path path = this.directory.resolve(name + ".properties"); + config.save(path.toFile()); + config.file(path.toString()); + HugeGraph result = HugeFactory.open(config); + result.initBackend(); + result.serverStarted(GlobalMasterInfo.master("drop-core-test")); + result.schema().vertexLabel("person").useCustomizeStringId().create(); + result.addVertex(T.id, "kept", T.label, "person"); + result.tx().commit(); + ((HugeGraphParams) Whitebox.getInternalState(result, "params")).closeTx(); + this.graphs.put(result.spaceGraphName(), result); + return result; + } + + private BlockingJob start() throws Exception { + BlockingJob job = new BlockingJob(); + this.jobs.add(job); + HugeTask task = new HugeTask<>(IdGenerator.of(9997000L), null, job); + task.type("test"); + task.name("drop-owner"); + this.scheduler.schedule(task); + Assert.assertTrue(job.started.await(10L, TimeUnit.SECONDS)); + return job; + } + + private void drop() { + if (this.pd) { + this.manager.dropGraph("DEFAULT", this.graph.name(), true); + } else { + this.manager.dropGraphLocal(this.graph.name()); + } + } + + private void assertRetained() { + Assert.assertSame(this.graph, this.manager.graph(this.graph.spaceGraphName())); + Assert.assertFalse(this.graph.closed()); + Assert.assertTrue(Files.exists(this.file)); + Assert.assertEquals(IdGenerator.of("kept"), this.graph.vertices("kept").next().id()); + this.params.closeTx(); + Map registered = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + Assert.assertSame(this.graph, registered.get(this.graph.spaceGraphName())); + Assert.assertEquals(0, this.dropEvents); + } + + @Override + public void close() throws Exception { + for (BlockingJob job : this.jobs) { + job.release.countDown(); + } + this.scheduler.waitUntilAllTasksCompleted(10L); + this.manager.close(); + for (HugeGraph current : new HugeGraph[]{this.graph, this.neighbor}) { + if (!current.closed()) { + current.close(); + } + HugeFactory.remove(current); + } + this.graphs.clear(); + FileUtils.deleteDirectory(this.directory.toFile()); + } + } + + public static class BlockingJob extends SysJob { + + private final CountDownLatch started = new CountDownLatch(1); + private final CountDownLatch release = new CountDownLatch(1); + + @Override + public String type() { + return "test"; + } + + @Override + public Object execute() { + this.graph().vertices("kept").next(); + this.started.countDown(); + boolean interrupted = false; + while (this.release.getCount() != 0L) { + try { + this.release.await(); + } catch (InterruptedException ignored) { + interrupted = true; + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + return null; + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/HugeFactoryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/HugeFactoryTest.java new file mode 100644 index 0000000000..702fd28289 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/HugeFactoryTest.java @@ -0,0 +1,754 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph; + +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hugegraph.backend.store.BackendStoreProvider; +import org.apache.hugegraph.backend.tx.GraphTransaction; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.dist.RegisterUtil; +import org.apache.hugegraph.dist.HugeGraphServer; +import org.apache.hugegraph.dist.MemoryMonitor; +import org.apache.hugegraph.job.SysJob; +import org.apache.hugegraph.server.RestServer; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.masterelection.GlobalMasterInfo; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.task.DistributedTaskScheduler; +import org.apache.hugegraph.task.HugeTask; +import org.apache.hugegraph.task.TaskAndResultSchedulerTest; +import org.apache.hugegraph.task.TaskStatus; +import org.apache.hugegraph.task.TaskManager; +import org.apache.hugegraph.task.TaskScheduler; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.unit.FakeObjects; +import org.apache.hugegraph.util.Consumers; +import org.apache.hugegraph.util.Events; +import org.junit.Test; +import org.mockito.AdditionalAnswers; +import org.mockito.Mockito; +import org.apache.tinkerpop.gremlin.server.GremlinServer; + +public class HugeFactoryTest { + + @Test + public void testCleanupContinuesAfterGraphFailure() { + StandardHugeGraph failed = Mockito.mock(StandardHugeGraph.class); + StandardHugeGraph cleaned = Mockito.mock(StandardHugeGraph.class); + Mockito.doThrow(new HugeException("test")) + .when(failed).closeCurrentThreadTransaction(); + + Assert.assertThrows(HugeException.class, () -> { + HugeFactory.closeCurrentThreadTransactions( + Arrays.asList(failed, cleaned)); + }); + + Mockito.verify(failed).closeCurrentThreadTransaction(); + Mockito.verify(cleaned).closeCurrentThreadTransaction(); + } + + @Test + public void testGraphCloseRetainsSchedulerAfterReleasedWorkerOwnerFails() throws Exception { + this.checkGraphCloseAfterOwnerFailure(false); + } + + @Test + public void testGraphCloseRetainsSchedulerAfterWorkerOwnerFailsBeforeRelease() throws Exception { + this.checkGraphCloseAfterOwnerFailure(true); + } + + private void checkGraphCloseAfterOwnerFailure(boolean beforeRelease) throws Exception { + RegisterUtil.registerBackends(); + HugeConfig config = FakeObjects.newConfig(); + config.setProperty("backend", "memory"); + config.setProperty("serializer", "text"); + config.setProperty("store", "graph_close_owner_failure_" + (beforeRelease ? "before" : "after")); + HugeGraph graph = HugeFactory.open(config); + TaskScheduler scheduler = graph.taskScheduler(); + AtomicReference workerOwner = new AtomicReference<>(); + try { + graph.initBackend(); + graph.serverStarted(GlobalMasterInfo.master("close-owner-test")); + HugeGraphParams params = Whitebox.getInternalState(graph, "params"); + TaskManager manager = TaskManager.instance(); + ExecutorService workers = Whitebox.getInternalState(manager, "taskExecutor"); + ThreadLocal owners = Whitebox.getInternalState(graph.tx(), "transactions"); + RuntimeException first = new IllegalStateException("released task DB owner failed"); + RuntimeException second = new IllegalArgumentException("released caller owner failed"); + AtomicInteger workerClosed = new AtomicInteger(); + AtomicInteger callerClosed = new AtomicInteger(); + AtomicInteger otherWorkerClosed = new AtomicInteger(); + BackendStoreProvider provider = Mockito.spy(graph.storeProvider()); + Whitebox.setInternalState(graph, "storeProvider", provider); + + scheduler.call(() -> { + workerOwner.set(failOwnerClose(graph, params, first, workerClosed, beforeRelease)); + return null; + }); + workers.submit(() -> { + GraphTransaction realOwner = params.graphTransaction(); + GraphTransaction owner = Mockito.mock(GraphTransaction.class, + AdditionalAnswers.delegatesTo(realOwner)); + Mockito.doAnswer(invocation -> { + realOwner.close(); + otherWorkerClosed.incrementAndGet(); + return null; + }).when(owner).close(); + Whitebox.setInternalState(owners.get(), "graphTx", owner); + }).get(10L, TimeUnit.SECONDS); + failOwnerClose(graph, params, second, callerClosed, false); + + Throwable failure = Assert.assertThrows(HugeException.class, graph::close); + Assert.assertSame(first, HugeException.rootCause(failure)); + Assert.assertTrue(Arrays.asList(failure.getSuppressed()).contains(second)); + Assert.assertFalse(graph.closed()); + Assert.assertSame(scheduler, manager.getScheduler(params)); + Assert.assertTrue((boolean) Whitebox.getInternalState(scheduler.serverManager(), "closed")); + Assert.assertNull(scheduler.call(owners::get)); + Assert.assertNull(owners.get()); + Assert.assertEquals(1, workerClosed.get()); + Assert.assertEquals(1, callerClosed.get()); + Assert.assertEquals(1, otherWorkerClosed.get()); + Mockito.verify(provider, Mockito.never()).close(); + + // A close failure is not an ACK, even if its owner released first. + if (beforeRelease) { + scheduler.call(() -> { + workerOwner.get().close(); + return null; + }); + } + graph.close(); + Assert.assertTrue(graph.closed()); + Assert.assertNull(manager.getScheduler(params)); + Assert.assertEquals(1, otherWorkerClosed.get()); + Mockito.verify(provider).close(); + } finally { + try { + // Only the memory fixture owns this retained failed transaction. + // Logical graph closure deliberately does not claim it was released. + GraphTransaction retained = workerOwner.get(); + if (beforeRelease && retained != null) { + scheduler.call(() -> { + retained.close(); + return null; + }); + } + if (!graph.closed()) { + graph.close(); + } + } finally { + HugeFactory.remove(graph); + } + } + } + + private static GraphTransaction failOwnerClose(HugeGraph graph, HugeGraphParams params, + RuntimeException failure, AtomicInteger closed, + boolean beforeRelease) { + GraphTransaction realOwner = params.graphTransaction(); + GraphTransaction owner = Mockito.mock(GraphTransaction.class, + AdditionalAnswers.delegatesTo(realOwner)); + Mockito.doAnswer(invocation -> { + if (!beforeRelease) { + realOwner.close(); + } + closed.incrementAndGet(); + throw failure; + }).when(owner).close(); + ThreadLocal owners = Whitebox.getInternalState(graph.tx(), "transactions"); + Whitebox.setInternalState(owners.get(), "graphTx", owner); + return realOwner; + } + + @Test + public void testGraphCloseReportsEveryTaskWorkerOwnerFailure() throws Exception { + RegisterUtil.registerBackends(); + HugeConfig config = FakeObjects.newConfig(); + config.setProperty("backend", "memory"); + config.setProperty("serializer", "text"); + config.setProperty("store", "graph_close_task_worker_failures"); + HugeGraph graph = HugeFactory.open(config); + CountDownLatch ready = new CountDownLatch(4); + CountDownLatch release = new CountDownLatch(1); + List> initialized = new ArrayList<>(); + try { + graph.initBackend(); + graph.serverStarted(GlobalMasterInfo.master("close-task-worker-test")); + HugeGraphParams params = Whitebox.getInternalState(graph, "params"); + TaskManager manager = TaskManager.instance(); + ExecutorService workers = Whitebox.getInternalState(manager, "taskExecutor"); + ThreadLocal owners = Whitebox.getInternalState(graph.tx(), "transactions"); + Queue failures = new ConcurrentLinkedQueue<>(); + AtomicInteger closed = new AtomicInteger(); + for (int i = 0; i < 4; i++) { + initialized.add(workers.submit(() -> { + try { + RuntimeException failure = new IllegalStateException( + "Task owner failed: " + Thread.currentThread().getName()); + failures.add(failure); + failOwnerClose(graph, params, failure, closed, false); + } finally { + ready.countDown(); + } + Assert.assertTrue(release.await(10L, TimeUnit.SECONDS)); + return null; + })); + } + Assert.assertTrue(ready.await(10L, TimeUnit.SECONDS)); + release.countDown(); + for (Future future : initialized) { + future.get(10L, TimeUnit.SECONDS); + } + + Throwable failure = Assert.assertThrows(HugeException.class, graph::close); + Throwable first = HugeException.rootCause(failure); + Assert.assertTrue(failures.contains(first)); + Assert.assertEquals(3, first.getSuppressed().length); + for (RuntimeException workerFailure : failures) { + Assert.assertTrue(workerFailure == first || + Arrays.asList(first.getSuppressed()).contains(workerFailure)); + } + Assert.assertEquals(4, closed.get()); + Assert.assertTrue(graph.closed()); + Assert.assertNull(manager.getScheduler(params)); + AtomicInteger cleared = new AtomicInteger(); + Consumers.executeOncePerThread(workers, 4, () -> { + if (owners.get() == null) { + cleared.incrementAndGet(); + } + }, 10L); + Assert.assertEquals(4, cleared.get()); + } finally { + release.countDown(); + try { + if (!graph.closed()) { + graph.close(); + } + } finally { + HugeFactory.remove(graph); + } + } + } + + @Test + public void testGraphClosePreservesRollbackFailureWhenOwnerCloseFails() throws Exception { + RegisterUtil.registerBackends(); + HugeConfig config = FakeObjects.newConfig(); + config.setProperty("backend", "memory"); + config.setProperty("serializer", "text"); + config.setProperty("store", "graph_close_rollback_and_owner_failure"); + HugeGraph graph = HugeFactory.open(config); + try { + graph.initBackend(); + graph.serverStarted(GlobalMasterInfo.master("close-rollback-test")); + HugeGraphParams params = Whitebox.getInternalState(graph, "params"); + graph.tx().open(); + GraphTransaction realOwner = params.graphTransaction(); + GraphTransaction owner = Mockito.mock(GraphTransaction.class, + AdditionalAnswers.delegatesTo(realOwner)); + RuntimeException first = new IllegalStateException("Rollback failed"); + RuntimeException second = new IllegalArgumentException("Owner close failed"); + Mockito.doThrow(first).when(owner).rollback(); + Mockito.doAnswer(invocation -> { + realOwner.close(); + throw second; + }).when(owner).close(); + ThreadLocal owners = Whitebox.getInternalState(graph.tx(), "transactions"); + Whitebox.setInternalState(owners.get(), "graphTx", owner); + + Throwable failure = Assert.assertThrows(IllegalStateException.class, graph::close); + Assert.assertSame(first, failure); + Assert.assertTrue(Arrays.asList(first.getSuppressed()).contains(second)); + Assert.assertTrue(graph.closed()); + Assert.assertNull(owners.get()); + Assert.assertNull(TaskManager.instance().getScheduler(params)); + Mockito.verify(owner).rollback(); + Mockito.verify(owner).close(); + } finally { + try { + if (!graph.closed()) { + graph.close(); + } + } finally { + HugeFactory.remove(graph); + } + } + } + + @Test + public void testLocalCloseKeepsPendingTaskPersistenceUsable() throws Exception { + RegisterUtil.registerBackends(); + HugeConfig config = FakeObjects.newConfig(); + config.setProperty("backend", "memory"); + config.setProperty("serializer", "text"); + config.setProperty("store", "graph_close_pending_local_task"); + HugeGraph graph = HugeFactory.open(config); + Map> pending = null; + HugeTask task = new HugeTask<>(IdGenerator.of(9999998L), null, + new TaskAndResultSchedulerTest.EmptyCallable()); + task.type("test"); + task.name("pending-task-close"); + try { + graph.initBackend(); + graph.serverStarted(GlobalMasterInfo.master("pending-task-close-test")); + TaskScheduler scheduler = graph.taskScheduler(); + pending = Whitebox.getInternalState(scheduler, "tasks"); + task.overwriteStatus(TaskStatus.RUNNING); + scheduler.save(task); + pending.put(task.id(), task); + + Assert.assertThrows(IllegalStateException.class, graph::close); + Assert.assertFalse(graph.closed()); + HugeGraphParams params = Whitebox.getInternalState(graph, "params"); + Assert.assertSame(scheduler, TaskManager.instance().getScheduler(params)); + Assert.assertFalse((boolean) Whitebox.getInternalState(scheduler.serverManager(), "closed")); + + // This is the same persistence path a running job uses from done(). + task.overwriteStatus(TaskStatus.SUCCESS); + scheduler.save(task); + Assert.assertEquals(TaskStatus.SUCCESS, scheduler.task(task.id()).status()); + pending.remove(task.id()); + graph.close(); + Assert.assertTrue(graph.closed()); + Assert.assertNull(TaskManager.instance().getScheduler(params)); + } finally { + if (pending != null) { + pending.remove(task.id()); + } + try { + if (!graph.closed()) { + graph.close(); + } + } finally { + HugeFactory.remove(graph); + } + } + } + + @Test + public void testIncompleteSchedulerCloseDoesNotQueueOwnerCleanup() { + HugeGraphParams params = Mockito.mock(HugeGraphParams.class); + TaskScheduler scheduler = Mockito.mock(TaskScheduler.class); + TaskManager manager = TaskManager.instance(); + Map schedulers = Whitebox.getInternalState(manager, "schedulers"); + schedulers.put(params, scheduler); + try { + Mockito.when(scheduler.close()).thenReturn(false); + manager.closeScheduler(params); + Assert.assertSame(scheduler, manager.getScheduler(params)); + Mockito.verify(params, Mockito.never()).closeTx(); + + Mockito.when(scheduler.close()).thenReturn(true); + manager.closeScheduler(params); + Assert.assertNull(manager.getScheduler(params)); + Mockito.verify(params, Mockito.atLeastOnce()).closeTx(); + } finally { + schedulers.remove(params); + } + } + + @Test + public void testServerStopClosesGraphAfterPendingTaskDrains() throws Exception { + // Server stop terminates process-wide executors; isolate it from the suite. + String classpath = System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")); + File output = File.createTempFile("hugegraph-server-stop-", ".log"); + Process child = new ProcessBuilder(new File(System.getProperty("java.home"), "bin/java").toString(), + "-cp", classpath, ServerStopFixture.class.getName()) + .redirectErrorStream(true).redirectOutput(output).start(); + try { + Assert.assertTrue("Server stop child timed out: " + output, child.waitFor(45L, TimeUnit.SECONDS)); + Assert.assertEquals(Files.readString(output.toPath()), 0, child.exitValue()); + } finally { + child.destroyForcibly(); + Files.deleteIfExists(output.toPath()); + } + } + + public static class ServerStopFixture { + + public static void main(String[] args) throws Exception { + RegisterUtil.registerBackends(); + HugeConfig config = FakeObjects.newConfig(); + config.setProperty("backend", "memory"); + config.setProperty("serializer", "text"); + config.setProperty("store", "server_stop_pending_task"); + HugeGraph graph = HugeFactory.open(config); + TaskManager manager = TaskManager.instance(); + HugeGraphParams params = Whitebox.getInternalState(graph, "params"); + ExecutorService workers = Whitebox.getInternalState(manager, "taskExecutor"); + ExecutorService database = Whitebox.getInternalState(manager, "taskDbExecutor"); + CountDownLatch running = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + try { + graph.initBackend(); + graph.serverStarted(GlobalMasterInfo.master("server-stop-test")); + TaskScheduler scheduler = graph.taskScheduler(); + AtomicBoolean completionPersisted = new AtomicBoolean(); + HugeTask task = new HugeTask<>(IdGenerator.of(9999997L), null, new SysJob() { + @Override + public String type() { + return "test"; + } + + @Override + public Object execute() throws Exception { + graph.traversal().V().hasNext(); + running.countDown(); + Assert.assertTrue(release.await(10L, TimeUnit.SECONDS)); + return null; + } + + @Override + protected void done() { + super.done(); + completionPersisted.set(scheduler.task(this.task().id()).status() == TaskStatus.SUCCESS); + } + }); + task.type("test"); + task.name("server-stop-pending-task"); + scheduler.schedule(task); + Assert.assertTrue(running.await(10L, TimeUnit.SECONDS)); + params.closeTx(); + Object taskDbOwner = Whitebox.getInternalState(scheduler, "taskTx"); + Assert.assertNotNull(taskDbOwner); + Assert.assertFalse((boolean) Whitebox.getInternalState(taskDbOwner, "closed")); + AtomicInteger earlyCloses = new AtomicInteger(); + Runnable earlyClose = () -> { + Assert.assertThrows(IllegalStateException.class, graph::close); + Assert.assertFalse(graph.closed()); + Assert.assertSame(scheduler, manager.getScheduler(params)); + earlyCloses.incrementAndGet(); + }; + GremlinServer gremlin = Mockito.mock(GremlinServer.class); + Mockito.when(gremlin.stop()).thenAnswer(invocation -> { + earlyClose.run(); + return CompletableFuture.completedFuture(null); + }); + RestServer rest = Mockito.mock(RestServer.class); + Mockito.when(rest.shutdown()).thenAnswer(invocation -> { + earlyClose.run(); + return CompletableFuture.completedFuture(null); + }); + ExecutorService stoppedWorkers = Mockito.spy(workers); + Mockito.doAnswer(invocation -> { + Assert.assertEquals(2, earlyCloses.get()); + release.countDown(); + return invocation.callRealMethod(); + }).when(stoppedWorkers).shutdown(); + Whitebox.setInternalState(manager, "taskExecutor", stoppedWorkers); + AtomicBoolean storeClosed = new AtomicBoolean(); + BackendStoreProvider provider = Mockito.spy(graph.storeProvider()); + provider.listen(event -> { + if (Events.STORE_CLOSE.equals(event.name())) { + storeClosed.set(true); + } + return null; + }); + Mockito.doAnswer(invocation -> { + Assert.assertTrue(workers.isTerminated()); + Assert.assertFalse(database.isShutdown()); + return invocation.callRealMethod(); + }).when(provider).close(); + Whitebox.setInternalState(graph, "storeProvider", provider); + HugeGraphServer server = Mockito.mock(HugeGraphServer.class, Mockito.CALLS_REAL_METHODS); + Whitebox.setInternalState(server, "gremlinServer", gremlin); + Whitebox.setInternalState(server, "restServer", rest); + Whitebox.setInternalState(server, "memoryMonitor", Mockito.mock(MemoryMonitor.class)); + + server.stop(); + + Assert.assertEquals(2, earlyCloses.get()); + Assert.assertEquals(TaskStatus.SUCCESS, task.status()); + Assert.assertTrue("Pending task must persist completion before owner close", completionPersisted.get()); + Assert.assertTrue("Server stop must close the retained graph", graph.closed()); + Assert.assertNull(manager.getScheduler(params)); + Assert.assertTrue((boolean) Whitebox.getInternalState(taskDbOwner, "closed")); + Assert.assertTrue(database.isTerminated()); + Assert.assertTrue("Store-close listener must run before event shutdown", storeClosed.get()); + Mockito.verify(provider).close(); + } finally { + release.countDown(); + HugeFactory.shutdown(10L, true); + HugeFactory.remove(graph); + } + } + } + + @Test + public void testShutdownDrainsWorkersWithRegisteredScheduler() throws Exception { + Constructor constructor = TaskManager.class.getDeclaredConstructor(int.class); + constructor.setAccessible(true); + TaskManager manager = constructor.newInstance(1); + Map schedulers = Whitebox.getInternalState(manager, "schedulers"); + HugeGraphParams params = Mockito.mock(HugeGraphParams.class); + TaskScheduler scheduler = Mockito.mock(TaskScheduler.class); + schedulers.put(params, scheduler); + ExecutorService workers = Whitebox.getInternalState(manager, "taskExecutor"); + ExecutorService database = Whitebox.getInternalState(manager, "taskDbExecutor"); + CountDownLatch started = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicBoolean saved = new AtomicBoolean(); + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(params.graph()).thenReturn(graph); + Mockito.when(scheduler.close()).thenAnswer(invocation -> saved.get()); + Mockito.doAnswer(invocation -> { + manager.closeScheduler(params); + return null; + }).when(graph).close(); + ExecutorService stoppedWorkers = Mockito.spy(workers); + Mockito.doAnswer(invocation -> { + release.countDown(); + return invocation.callRealMethod(); + }).when(stoppedWorkers).shutdown(); + Whitebox.setInternalState(manager, "taskExecutor", stoppedWorkers); + try { + Future task = workers.submit(() -> { + started.countDown(); + Assert.assertTrue(release.await(10L, TimeUnit.SECONDS)); + database.submit(() -> saved.set(true)).get(10L, TimeUnit.SECONDS); + return null; + }); + Assert.assertTrue(started.await(10L, TimeUnit.SECONDS)); + manager.closeScheduler(params); + Assert.assertSame(scheduler, manager.getScheduler(params)); + manager.shutdown(10L); + task.get(10L, TimeUnit.SECONDS); + Assert.assertTrue(saved.get()); + Assert.assertNull(manager.getScheduler(params)); + for (String field : new String[]{"taskExecutor", "taskDbExecutor", "schemaTaskExecutor", + "olapTaskExecutor", "ephemeralTaskExecutor", + "distributedSchedulerExecutor"}) { + ExecutorService executor = Whitebox.getInternalState(manager, field); + Assert.assertTrue(executor.isTerminated()); + } + } finally { + release.countDown(); + schedulers.clear(); + manager.shutdown(10L); + } + } + + @Test + public void testShutdownCloseFailureKeepsDatabaseAvailableForRetry() throws Exception { + Constructor constructor = TaskManager.class.getDeclaredConstructor(int.class); + constructor.setAccessible(true); + TaskManager manager = constructor.newInstance(1); + Map schedulers = Whitebox.getInternalState(manager, "schedulers"); + HugeGraphParams failed = Mockito.mock(HugeGraphParams.class); + HugeGraphParams healthy = Mockito.mock(HugeGraphParams.class); + HugeGraph failedGraph = Mockito.mock(HugeGraph.class); + HugeGraph healthyGraph = Mockito.mock(HugeGraph.class); + Mockito.when(failed.graph()).thenReturn(failedGraph); + Mockito.when(healthy.graph()).thenReturn(healthyGraph); + schedulers.put(failed, Mockito.mock(TaskScheduler.class)); + schedulers.put(healthy, Mockito.mock(TaskScheduler.class)); + RuntimeException failure = new IllegalStateException("Task DB owner close failed"); + Mockito.doThrow(failure).when(failedGraph).close(); + Mockito.doAnswer(invocation -> { + schedulers.remove(healthy); + return null; + }).when(healthyGraph).close(); + ExecutorService database = Whitebox.getInternalState(manager, "taskDbExecutor"); + try { + Throwable thrown = Assert.assertThrows(HugeException.class, () -> manager.shutdown(10L)); + Assert.assertSame(failure, HugeException.rootCause(thrown)); + Mockito.verify(healthyGraph).close(); + Assert.assertTrue(schedulers.containsKey(failed)); + Assert.assertFalse(database.isShutdown()); + Assert.assertEquals(1, (int) database.submit(() -> 1).get(10L, TimeUnit.SECONDS)); + + Mockito.doAnswer(invocation -> { + schedulers.remove(failed); + return null; + }).when(failedGraph).close(); + manager.shutdown(10L); + Assert.assertTrue(schedulers.isEmpty()); + Assert.assertTrue(database.isTerminated()); + } finally { + schedulers.clear(); + manager.shutdown(10L); + } + } + + @Test + public void testShutdownClosesRegisteredDistributedScheduler() throws Exception { + Constructor constructor = TaskManager.class.getDeclaredConstructor(int.class); + constructor.setAccessible(true); + TaskManager manager = constructor.newInstance(1); + RegisterUtil.registerBackends(); + HugeConfig config = FakeObjects.newConfig(); + config.setProperty("backend", "memory"); + config.setProperty("serializer", "text"); + config.setProperty("store", "shutdown_registered_distributed_scheduler"); + HugeGraph graph = HugeFactory.open(config); + HugeGraphParams params = Whitebox.getInternalState(graph, "params"); + DistributedTaskScheduler scheduler = null; + try { + graph.initBackend(); + graph.serverStarted(GlobalMasterInfo.master("shutdown-distributed-test")); + TaskManager.instance().closeScheduler(params); + Whitebox.setInternalState(graph, "taskManager", manager); + ScheduledThreadPoolExecutor cron = Whitebox.getInternalState(manager, "distributedSchedulerExecutor"); + ExecutorService database = Whitebox.getInternalState(manager, "taskDbExecutor"); + ExecutorService workers = Whitebox.getInternalState(manager, "taskExecutor"); + ExecutorService schema = Whitebox.getInternalState(manager, "schemaTaskExecutor"); + ExecutorService olap = Whitebox.getInternalState(manager, "olapTaskExecutor"); + ExecutorService ephemeral = Whitebox.getInternalState(manager, "ephemeralTaskExecutor"); + scheduler = new DistributedTaskScheduler( + params, cron, database, schema, olap, workers, ephemeral); + Map schedulers = Whitebox.getInternalState(manager, "schedulers"); + schedulers.put(params, scheduler); + scheduler.init(); + params.closeTx(); + Object taskDbOwner = Whitebox.getInternalState(scheduler, "taskTx"); + Assert.assertFalse((boolean) Whitebox.getInternalState(taskDbOwner, "closed")); + + manager.shutdown(10L); + + Assert.assertTrue(graph.closed()); + Assert.assertNull(manager.getScheduler(params)); + Assert.assertTrue((boolean) Whitebox.getInternalState(taskDbOwner, "closed")); + Assert.assertTrue(cron.isTerminated()); + Assert.assertTrue(database.isTerminated()); + } finally { + try { + if (!graph.closed() && scheduler != null) { + GraphTransaction owner = Whitebox.getInternalState(scheduler, "taskTx"); + scheduler.call(() -> { + owner.close(); + params.closeTx(); + }); + manager.forceRemoveScheduler(params); + graph.close(); + } + manager.shutdown(10L); + } finally { + HugeFactory.remove(graph); + } + } + } + + @Test + public void testShutdownRetryWaitsForTaskDbTermination() throws Exception { + this.checkShutdownRetryWaitsForTermination("taskDbExecutor"); + } + + @Test + public void testShutdownRetryWaitsForEphemeralTermination() throws Exception { + this.checkShutdownRetryWaitsForTermination("ephemeralTaskExecutor"); + } + + @Test + public void testShutdownRetryWaitsForSchemaTermination() throws Exception { + this.checkShutdownRetryWaitsForTermination("schemaTaskExecutor"); + } + + @Test + public void testShutdownRetryWaitsForOlapTermination() throws Exception { + this.checkShutdownRetryWaitsForTermination("olapTaskExecutor"); + } + + private void checkShutdownRetryWaitsForTermination(String field) throws Exception { + Constructor constructor = TaskManager.class.getDeclaredConstructor(int.class); + constructor.setAccessible(true); + TaskManager manager = constructor.newInstance(1); + ExecutorService executor = Whitebox.getInternalState(manager, field); + CountDownLatch started = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + Future task = executor.submit(() -> { + started.countDown(); + Assert.assertTrue(release.await(10L, TimeUnit.SECONDS)); + return null; + }); + try { + Assert.assertTrue(started.await(10L, TimeUnit.SECONDS)); + Assert.assertThrows(HugeException.class, () -> manager.shutdown(0L)); + Assert.assertTrue(executor.isShutdown()); + Assert.assertFalse(executor.isTerminated()); + Assert.assertThrows(HugeException.class, () -> manager.shutdown(0L)); + Assert.assertFalse(executor.isTerminated()); + Thread.currentThread().interrupt(); + Throwable interrupted = Assert.assertThrows(HugeException.class, () -> manager.shutdown(10L)); + Assert.assertTrue(HugeException.rootCause(interrupted) instanceof InterruptedException); + Assert.assertTrue(Thread.currentThread().isInterrupted()); + } finally { + Thread.interrupted(); + release.countDown(); + task.get(10L, TimeUnit.SECONDS); + manager.shutdown(10L); + } + } + + @Test + public void testDistributedCloseRetriesDrainAfterTimeout() throws Exception { + HugeGraphParams params = Mockito.mock(HugeGraphParams.class); + Mockito.when(params.graph()).thenReturn(Mockito.mock(HugeGraph.class)); + Mockito.when(params.configuration()).thenReturn(FakeObjects.newConfig()); + ScheduledThreadPoolExecutor cron = Mockito.mock(ScheduledThreadPoolExecutor.class); + Mockito.doReturn(Mockito.mock(ScheduledFuture.class)).when(cron).scheduleWithFixedDelay( + Mockito.any(Runnable.class), Mockito.anyLong(), Mockito.anyLong(), Mockito.eq(TimeUnit.SECONDS)); + Mockito.doReturn(CompletableFuture.completedFuture(null)).when(cron).submit(Mockito.any(Runnable.class)); + ExecutorService executor = Mockito.mock(ExecutorService.class); + Mockito.when(executor.isShutdown()).thenReturn(true); + DistributedTaskScheduler scheduler = Mockito.spy(new DistributedTaskScheduler( + params, cron, executor, executor, executor, executor, executor)); + AtomicBoolean active = new AtomicBoolean(true); + AtomicInteger drainAttempts = new AtomicInteger(); + Mockito.doAnswer(invocation -> { + drainAttempts.incrementAndGet(); + if (active.get()) { + throw new TimeoutException("Task still running"); + } + return null; + }).when(scheduler).waitUntilAllTasksCompleted(Mockito.anyLong()); + + Assert.assertFalse(scheduler.close()); + Assert.assertFalse(scheduler.close()); + Assert.assertEquals(2, drainAttempts.get()); + Assert.assertFalse((boolean) Whitebox.getInternalState(scheduler.serverManager(), "closed")); + active.set(false); + Assert.assertTrue(scheduler.close()); + Assert.assertEquals(3, drainAttempts.get()); + Assert.assertTrue((boolean) Whitebox.getInternalState(scheduler.serverManager(), "closed")); + Assert.assertTrue(scheduler.close()); + Assert.assertEquals(3, drainAttempts.get()); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/StandardTaskAdmissionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/StandardTaskAdmissionTest.java new file mode 100644 index 0000000000..0248e7cd24 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/StandardTaskAdmissionTest.java @@ -0,0 +1,978 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.dist.RegisterUtil; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.job.SysJob; +import org.apache.hugegraph.masterelection.GlobalMasterInfo; +import org.apache.hugegraph.task.HugeTask; +import org.apache.hugegraph.task.StandardTaskScheduler; +import org.apache.hugegraph.task.TaskCallable; +import org.apache.hugegraph.task.TaskStatus; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.unit.FakeObjects; +import org.apache.tinkerpop.gremlin.structure.T; +import org.junit.Test; +import org.mockito.Mockito; + +public class StandardTaskAdmissionTest { + + @Test + public void testCancellationFailurePreservesCallerTransaction() throws Exception { + try (Fixture fixture = new Fixture("standard_failed_caller_tx")) { + CountDownLatch occupied = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + fixture.worker.submit(() -> { + occupied.countDown(); + await(release); + }); + Assert.assertTrue(occupied.await(10L, TimeUnit.SECONDS)); + AtomicBoolean reject = new AtomicBoolean(true); + IllegalStateException failure = new IllegalStateException("Cancellation rejected"); + HugeTask task = new HugeTask<>(IdGenerator.of(9999952L), null, new EmptyJob()) { + @Override + public boolean cancel(boolean interrupt) { + if (reject.get()) { + throw failure; + } + return super.cancel(interrupt); + } + }; + task.type("test"); + task.name("failed-cancellation-caller-tx"); + fixture.scheduler.schedule(task); + try { + fixture.graph.schema().vertexLabel("caller").useCustomizeStringId().create(); + fixture.graph.tx().commit(); + fixture.graph.addVertex(T.id, "pending", T.label, "caller"); + Assert.assertThrows(IllegalStateException.class, () -> fixture.scheduler.cancel(task), + caught -> Assert.assertSame(failure, caught)); + Assert.assertFalse(task.isCancelled()); + Assert.assertTrue(fixture.graph.tx().isOpen()); + fixture.graph.tx().commit(); + Assert.assertTrue(fixture.database.submit(() -> { + try { + return fixture.graph.vertices("pending").hasNext(); + } finally { + fixture.params.closeTx(); + } + }).get(10L, TimeUnit.SECONDS)); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + } finally { + reject.set(false); + release.countDown(); + } + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + } + } + + @Test + public void testNormalWorkerTransactionCleanupControl() throws Exception { + try (Fixture fixture = new Fixture("standard_normal_worker_control")) { + BlockingJob job = new BlockingJob(); + job.release.countDown(); + HugeTask task = fixture.task(job); + fixture.scheduler.schedule(task).get(10L, TimeUnit.SECONDS); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertFalse(task.isCancelled()); + Assert.assertEquals(TaskStatus.SUCCESS, task.status()); + Assert.assertEquals(0L, job.started.getCount()); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + ThreadLocal owners = Whitebox.getInternalState(fixture.graph.tx(), "transactions"); + Assert.assertNull(fixture.worker.submit(owners::get).get(10L, TimeUnit.SECONDS)); + Assert.assertFalse(fixture.worker.submit(() -> fixture.graph.tx().isOpen()).get(10L, TimeUnit.SECONDS)); + } + } + + @Test + public void testDirectDefaultDoneCancellationPreservesCallerTransaction() throws Exception { + try (Fixture fixture = new Fixture("standard_direct_done_control")) { + CountDownLatch occupied = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + fixture.worker.submit(() -> { + occupied.countDown(); + await(release); + }); + Assert.assertTrue(occupied.await(10L, TimeUnit.SECONDS)); + HugeTask task = fixture.task(new EmptyJob()); + try { + fixture.scheduler.schedule(task); + fixture.graph.schema().vertexLabel("caller").useCustomizeStringId().create(); + fixture.graph.tx().commit(); + fixture.graph.addVertex(T.id, "pending", T.label, "caller"); + // Direct FutureTask cancellation invokes the inherited TaskCallable.done before outer guards. + task.cancel(true); + Assert.assertTrue(fixture.graph.tx().isOpen()); + Assert.assertTrue(fixture.graph.vertices("pending").hasNext()); + fixture.graph.tx().commit(); + Assert.assertTrue(fixture.database.submit(() -> { + try { + return fixture.graph.vertices("pending").hasNext(); + } finally { + fixture.params.closeTx(); + } + }).get(10L, TimeUnit.SECONDS)); + } finally { + release.countDown(); + } + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + } + } + + @Test + public void testQueuedDefaultCancellationPreservesCallerTransaction() throws Exception { + this.assertDefaultCancellationPreservesCallerTransaction(false); + } + + @Test + public void testRunningDefaultCancellationPreservesCallerTransaction() throws Exception { + this.assertDefaultCancellationPreservesCallerTransaction(true); + } + + private void assertDefaultCancellationPreservesCallerTransaction(boolean running) throws Exception { + try (Fixture fixture = new Fixture("standard_caller_tx_" + running)) { + CountDownLatch occupied = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + if (!running) { + fixture.worker.submit(() -> { + occupied.countDown(); + await(release); + }); + Assert.assertTrue(occupied.await(10L, TimeUnit.SECONDS)); + } + BlockingJob job = new BlockingJob(); + HugeTask task = fixture.task(job); + try { + fixture.scheduler.schedule(task); + if (running) { + Assert.assertTrue(job.started.await(10L, TimeUnit.SECONDS)); + } + fixture.graph.schema().vertexLabel("caller").useCustomizeStringId().create(); + fixture.graph.tx().commit(); + fixture.graph.addVertex(T.id, "pending", T.label, "caller"); + fixture.scheduler.cancel(task); + Assert.assertTrue(task.isCancelled()); + Assert.assertTrue(fixture.graph.tx().isOpen()); + Assert.assertTrue(fixture.graph.vertices("pending").hasNext()); + fixture.graph.tx().commit(); + Assert.assertFalse(fixture.graph.tx().isOpen()); + Assert.assertTrue(fixture.database.submit(() -> { + try { + return fixture.graph.vertices("pending").hasNext(); + } finally { + fixture.params.closeTx(); + } + }).get(10L, TimeUnit.SECONDS)); + if (running) { + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + } + } finally { + release.countDown(); + job.release.countDown(); + } + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + ThreadLocal owners = Whitebox.getInternalState(fixture.graph.tx(), "transactions"); + Assert.assertNull(fixture.worker.submit(owners::get).get(10L, TimeUnit.SECONDS)); + Assert.assertEquals(running ? 0L : 1L, job.started.getCount()); + } + } + + @Test + public void testCancelledWorkerFencesAdmissionUntilActualExit() throws Exception { + try (Fixture fixture = new Fixture("standard_cancel_worker")) { + BlockingJob job = new BlockingJob(); + HugeTask task = fixture.task(job); + fixture.scheduler.schedule(task); + Assert.assertTrue(job.started.await(10L, TimeUnit.SECONDS)); + fixture.scheduler.cancel(task); + Assert.assertTrue(task.isCancelled()); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + Assert.assertFalse(fixture.scheduler.close()); + HugeTask late = fixture.task(new EmptyJob()); + Assert.assertThrows(IllegalStateException.class, () -> fixture.scheduler.schedule(late)); + Assert.assertThrows(IllegalStateException.class, () -> { + fixture.scheduler.restoreTasks(); + }, error -> Assert.assertContains("closing", error.getMessage())); + Assert.assertEquals(TaskStatus.NEW, late.status()); + job.release.countDown(); + fixture.scheduler.waitUntilAllTasksCompleted(10L); + Assert.assertEquals(TaskStatus.CANCELLED, fixture.scheduler.findTask(task.id()).status()); + Assert.assertTrue(fixture.scheduler.close()); + } + } + + @Test + public void testDispatchFutureCancellationKeepsRunningOwner() throws Exception { + try (Fixture fixture = new Fixture("standard_cancel_future")) { + BlockingJob job = new BlockingJob(); + HugeTask task = fixture.task(job); + Future dispatch = fixture.scheduler.schedule(task); + Assert.assertTrue(job.started.await(10L, TimeUnit.SECONDS)); + Assert.assertTrue(dispatch.cancel(false)); + Assert.assertFalse(task.isCancelled()); + Assert.assertFalse(fixture.scheduler.close()); + job.release.countDown(); + fixture.scheduler.waitUntilAllTasksCompleted(10L); + Assert.assertEquals(TaskStatus.SUCCESS, fixture.scheduler.findTask(task.id()).status()); + Assert.assertTrue(fixture.scheduler.close()); + } + } + + @Test + public void testCancellationCallbackRetainsItsTransactionAfterWorkerExits() throws Exception { + try (Fixture fixture = new Fixture("standard_cancel_callback")) { + fixture.graph.schema().vertexLabel("person").useCustomizeStringId().create(); + CountDownLatch callback = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + BlockingJob job = new BlockingJob() { + @Override + protected void cancelled() { + this.graph().addVertex(T.id, "callback", T.label, "person"); + callback.countDown(); + await(release); + this.graph().tx().commit(); + super.cancelled(); + } + }; + HugeTask task = fixture.task(job); + fixture.scheduler.schedule(task); + Assert.assertTrue(job.started.await(10L, TimeUnit.SECONDS)); + ExecutorService caller = Executors.newSingleThreadExecutor(); + try { + Future cancellation = caller.submit(() -> fixture.scheduler.cancel(task)); + Assert.assertTrue(callback.await(10L, TimeUnit.SECONDS)); + job.release.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + Assert.assertFalse(fixture.scheduler.close()); + release.countDown(); + cancellation.get(10L, TimeUnit.SECONDS); + fixture.scheduler.waitUntilAllTasksCompleted(10L); + Assert.assertTrue(fixture.graph.vertices("callback").hasNext()); + Assert.assertEquals(TaskStatus.CANCELLED, fixture.scheduler.findTask(task.id()).status()); + } finally { + release.countDown(); + caller.shutdownNow(); + Assert.assertTrue(caller.awaitTermination(10L, TimeUnit.SECONDS)); + } + } + } + + @Test + public void testQueuedCancellationAndSubmissionRejectionReleaseOwners() throws Exception { + try (Fixture fixture = new Fixture("standard_cancel_queued")) { + CountDownLatch occupied = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + fixture.worker.submit(() -> { + occupied.countDown(); + release.await(); + return null; + }); + try { + Assert.assertTrue(occupied.await(10L, TimeUnit.SECONDS)); + BlockingJob job = new BlockingJob(); + Future queued = fixture.scheduler.schedule(fixture.task(job)); + Assert.assertTrue(queued.cancel(false)); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + release.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertEquals(1L, job.started.getCount()); + fixture.worker.shutdown(); + Assert.assertTrue(fixture.worker.awaitTermination(10L, TimeUnit.SECONDS)); + Assert.assertThrows(RejectedExecutionException.class, + () -> fixture.scheduler.schedule(fixture.task(new EmptyJob()))); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + } finally { + release.countDown(); + } + } + } + + @Test + public void testDependencyRetryDoesNotReleaseEarlierCleanupOwner() throws Exception { + try (Fixture fixture = new Fixture("standard_dependency_owner")) { + CountDownLatch closing = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicBoolean first = new AtomicBoolean(true); + Mockito.doAnswer(invocation -> { + if (Thread.currentThread().getName().equals("standard-test-worker") && + first.compareAndSet(true, false)) { + closing.countDown(); + Assert.assertTrue(release.await(10L, TimeUnit.SECONDS)); + } + return invocation.callRealMethod(); + }).when(fixture.params).closeTx(); + HugeTask dependency = fixture.task(new EmptyJob()); + fixture.scheduler.save(dependency); + HugeTask task = fixture.task(new EmptyJob()); + task.depends(dependency.id()); + fixture.scheduler.schedule(task); + try { + Assert.assertTrue(closing.await(10L, TimeUnit.SECONDS)); + fixture.scheduler.cancel(task); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + Assert.assertFalse(fixture.scheduler.close()); + } finally { + release.countDown(); + } + fixture.scheduler.waitUntilAllTasksCompleted(10L); + Assert.assertEquals(TaskStatus.CANCELLED, fixture.scheduler.findTask(task.id()).status()); + } + } + + @Test + public void testCloseFencesDependencyResubmission() throws Exception { + try (Fixture fixture = new Fixture("standard_dependency_fence")) { + CountDownLatch occupied = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + fixture.worker.submit(() -> { + occupied.countDown(); + release.await(); + return null; + }); + try { + Assert.assertTrue(occupied.await(10L, TimeUnit.SECONDS)); + HugeTask dependency = fixture.task(new EmptyJob()); + fixture.scheduler.save(dependency); + HugeTask task = fixture.task(new EmptyJob()); + task.depends(dependency.id()); + Future dispatch = fixture.scheduler.schedule(task); + Assert.assertFalse(fixture.scheduler.close()); + release.countDown(); + dispatch.get(10L, TimeUnit.SECONDS); + fixture.scheduler.waitUntilAllTasksCompleted(10L); + Assert.assertEquals(TaskStatus.FAILED, fixture.scheduler.findTask(task.id()).status()); + Assert.assertContains("is closing", task.result()); + Assert.assertTrue(fixture.scheduler.close()); + } finally { + release.countDown(); + } + } + } + + @Test + public void testCloseFailureRemainsRetryable() throws Exception { + try (Fixture fixture = new Fixture("standard_close_failure")) { + Mockito.doThrow(new IllegalStateException("close failed")).doCallRealMethod() + .when(fixture.params).closeTx(); + Assert.assertThrows(RuntimeException.class, fixture.scheduler::close); + HugeTask late = fixture.task(new EmptyJob()); + Assert.assertThrows(IllegalStateException.class, () -> fixture.scheduler.schedule(late)); + Assert.assertTrue(fixture.scheduler.close()); + } + } + + @Test + public void testForceDeleteHidesPublicTaskWhileNativeWorkerRetainsOwnership() throws Exception { + CountDownLatch started = new CountDownLatch(1); + CountDownLatch interrupted = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + java.util.Map observed = new java.util.LinkedHashMap<>(); + boolean publicPresent; + boolean listPresent; + try (Fixture fixture = new Fixture("standard_force_delete_native")) { + ExecutorService observerWorker = Executors.newSingleThreadExecutor(); + ExecutorService observerDatabase = Executors.newSingleThreadExecutor(runnable -> + new Thread(runnable, "standard-independent-db")); + StandardTaskScheduler observer = new StandardTaskScheduler(fixture.params, observerWorker, + observerDatabase); + BlockingJob job = new BlockingJob() { + @Override + public Object execute() { + this.graph().vertices().hasNext(); + started.countDown(); + boolean wasInterrupted = false; + while (release.getCount() != 0L) { + try { + release.await(); + } catch (InterruptedException ignored) { + wasInterrupted = true; + interrupted.countDown(); + } + } + if (wasInterrupted) { + Thread.currentThread().interrupt(); + } + return null; + } + }; + HugeTask task = fixture.task(job); + try { + fixture.scheduler.save(task); + Assert.assertNotNull(observer.findTask(task.id())); + fixture.scheduler.schedule(task); + Assert.assertTrue(started.await(10L, TimeUnit.SECONDS)); + Assert.assertNotNull(observer.findTask(task.id())); + HugeTask deleted = fixture.scheduler.delete(task.id(), true); + Assert.assertNotNull(deleted); + Assert.assertTrue(interrupted.await(10L, TimeUnit.SECONDS)); + Assert.assertEquals(1L, release.getCount()); + try { + observed.put("public_task_status_before_release", + fixture.scheduler.task(task.id()).status().name()); + publicPresent = true; + } catch (org.apache.hugegraph.exception.NotFoundException missing) { + publicPresent = false; + } + listPresent = fixture.scheduler.tasks(java.util.Collections.singletonList(task.id())).hasNext(); + try { + fixture.scheduler.task(task.id(), false); + observed.put("public_task_without_result_present_before_release", true); + } catch (org.apache.hugegraph.exception.NotFoundException missing) { + observed.put("public_task_without_result_present_before_release", false); + } + observed.put("public_ids_without_result_present_before_release", fixture.scheduler.tasks( + java.util.Collections.singletonList(task.id()), false).hasNext()); + Assert.assertThrows(org.apache.hugegraph.exception.NotFoundException.class, + () -> observer.findTask(task.id())); + Assert.assertFalse(observer.findAllTask(10L, null, false).hasNext()); + int pending = fixture.scheduler.pendingTasks(); + boolean closed = fixture.scheduler.close(); + observed.put("public_task_present_before_release", publicPresent); + observed.put("public_task_ids_list_present_before_release", listPresent); + observed.put("independent_committed_db_present_before_release", false); + observed.put("native_worker_interrupted_and_still_held", true); + observed.put("pending_before_release", pending); + observed.put("close_before_release", closed); + observed.put("logical_task_cancelled", task.isCancelled()); + observed.put("native_scheduler_origin", StandardTaskScheduler.class.getProtectionDomain() + .getCodeSource().getLocation().toString()); + Assert.assertEquals(1, pending); + Assert.assertFalse(closed); + release.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + fixture.scheduler.waitUntilAllTasksCompleted(10L); + Assert.assertThrows(org.apache.hugegraph.exception.NotFoundException.class, + () -> fixture.scheduler.task(task.id())); + Assert.assertFalse(fixture.scheduler.tasks(java.util.Collections.singletonList(task.id())).hasNext()); + Assert.assertThrows(org.apache.hugegraph.exception.NotFoundException.class, + () -> observer.findTask(task.id())); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + Assert.assertTrue(fixture.scheduler.close()); + observed.put("public_task_present_after_release", false); + observed.put("public_task_ids_list_present_after_release", false); + observed.put("independent_committed_db_present_after_release", false); + observed.put("pending_after_release", 0); + observed.put("close_after_release", true); + observed.put("one_persisted_task_one_delete", true); + String evidence = System.getProperty("hugegraph.task.delete.evidence"); + if (evidence != null) { + java.nio.file.Files.writeString(java.nio.file.Paths.get(evidence, "standard-observation.json"), + org.apache.hugegraph.util.JsonUtil.toJson(observed)); + } + } finally { + release.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertTrue(observer.close()); + observerWorker.shutdownNow(); + observerDatabase.shutdownNow(); + Assert.assertTrue(observerWorker.awaitTermination(10L, TimeUnit.SECONDS)); + Assert.assertTrue(observerDatabase.awaitTermination(10L, TimeUnit.SECONDS)); + } + } + // Admission accounting must keep the true native owner above, while + // the established Standard force-delete API contract requires absence. + Assert.assertFalse("force-deleted task is still publicly visible", publicPresent); + Assert.assertFalse("force-deleted task is still listed by ID", listPresent); + Assert.assertEquals(false, observed.get("public_task_without_result_present_before_release")); + Assert.assertEquals(false, observed.get("public_ids_without_result_present_before_release")); + } + + @Test + public void testDeletingTaskStillVisibleWhileItsRecordExists() throws Exception { + try (Fixture fixture = new Fixture("standard_deleting_db_visible")) { + BlockingJob job = new BlockingJob(); + HugeTask task = fixture.task(job); + fixture.scheduler.schedule(task); + try { + Assert.assertTrue(job.started.await(10L, TimeUnit.SECONDS)); + task.overwriteStatus(TaskStatus.DELETING); + fixture.scheduler.save(task); + for (boolean withResult : new boolean[]{true, false}) { + Assert.assertEquals(TaskStatus.DELETING, fixture.scheduler.task(task.id(), withResult).status()); + java.util.Iterator> tasks = fixture.scheduler.tasks( + java.util.Collections.singletonList(task.id()), withResult); + Assert.assertTrue(tasks.hasNext()); + Assert.assertEquals(TaskStatus.DELETING, tasks.next().status()); + Assert.assertFalse(tasks.hasNext()); + } + } finally { + job.release.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + } + } + } + + @Test + public void testOrdinaryTaskVisibilityRetainsResultSelection() throws Exception { + try (Fixture fixture = new Fixture("standard_normal_task_visible")) { + HugeTask task = fixture.task(new ResultJob()); + fixture.scheduler.schedule(task).get(10L, TimeUnit.SECONDS); + for (boolean withResult : new boolean[]{true, false}) { + HugeTask loaded = fixture.scheduler.task(task.id(), withResult); + Assert.assertEquals(TaskStatus.SUCCESS, loaded.status()); + Assert.assertEquals(withResult ? task.result() : null, loaded.result()); + java.util.Iterator> tasks = fixture.scheduler.tasks( + java.util.Collections.singletonList(task.id()), withResult); + Assert.assertTrue(tasks.hasNext()); + HugeTask listed = tasks.next(); + Assert.assertEquals(TaskStatus.SUCCESS, listed.status()); + Assert.assertEquals(withResult ? task.result() : null, listed.result()); + Assert.assertFalse(tasks.hasNext()); + } + } + } + + @Test + public void testForceDeleteCompletedOwnerDoesNotResurrectAfterDoneSave() throws Exception { + java.util.Map observed = new java.util.LinkedHashMap<>(); + boolean publicPresent; + boolean idsPresent; + boolean dbResurrected; + try (Fixture fixture = new Fixture("standard_force_delete_done_save")) { + HugeGraphParams nativeParams = (HugeGraphParams) Whitebox.getInternalState(fixture.graph, "params"); + @SuppressWarnings("unchecked") + java.util.Map schedulers = + (java.util.Map) Whitebox + .getInternalState(org.apache.hugegraph.task.TaskManager.instance(), "schedulers"); + org.apache.hugegraph.task.TaskScheduler original = schedulers.put(nativeParams, fixture.scheduler); + ExecutorService observerWorker = Executors.newSingleThreadExecutor(); + ExecutorService observerDatabase = Executors.newSingleThreadExecutor(); + StandardTaskScheduler observer = new StandardTaskScheduler(fixture.params, observerWorker, + observerDatabase); + CompletionSaveJob job = new CompletionSaveJob(); + HugeTask task = fixture.task(job); + try { + Assert.assertSame(fixture.scheduler, fixture.graph.taskScheduler()); + fixture.scheduler.schedule(task); + Assert.assertTrue(job.beforeSave.await(10L, TimeUnit.SECONDS)); + Assert.assertEquals(TaskStatus.SUCCESS, task.status()); + Assert.assertTrue(task.completed()); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + Assert.assertNotNull(observer.findTask(task.id())); + HugeTask deleted = fixture.scheduler.delete(task.id(), true); + Assert.assertNotNull(deleted); + Assert.assertThrows(org.apache.hugegraph.exception.NotFoundException.class, + () -> observer.findTask(task.id())); + try { + observed.put("public_status_after_delete_before_done_save", + fixture.scheduler.task(task.id()).status().name()); + publicPresent = true; + } catch (org.apache.hugegraph.exception.NotFoundException missing) { + publicPresent = false; + } + idsPresent = fixture.scheduler.tasks(java.util.Collections.singletonList(task.id())).hasNext(); + observed.put("success_owner_retained_before_delete", true); + observed.put("public_present_after_delete_before_done_save", publicPresent); + observed.put("ids_present_after_delete_before_done_save", idsPresent); + observed.put("committed_db_present_after_delete_before_done_save", false); + observed.put("pending_before_done_save", fixture.scheduler.pendingTasks()); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + Assert.assertFalse(fixture.scheduler.close()); + observed.put("close_before_done_save", false); + job.releaseSave.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertTrue(job.saved.await(10L, TimeUnit.SECONDS)); + try { + observed.put("db_status_after_real_done_save", observer.findTask(task.id()).status().name()); + dbResurrected = true; + } catch (org.apache.hugegraph.exception.NotFoundException missing) { + dbResurrected = false; + } + observed.put("committed_db_resurrected_after_real_done_save", dbResurrected); + observed.put("pending_after_real_done_save", fixture.scheduler.pendingTasks()); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + Assert.assertTrue(fixture.scheduler.close()); + observed.put("close_after_real_done_save", true); + observed.put("real_sysjob_done_save_scheduler_binding", true); + String evidence = System.getProperty("hugegraph.task.delete.evidence"); + if (evidence != null) { + java.nio.file.Files.writeString( + java.nio.file.Paths.get(evidence, "completion-save-observation.json"), + org.apache.hugegraph.util.JsonUtil.toJson(observed)); + } + } finally { + job.releaseSave.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertTrue(schedulers.replace(nativeParams, fixture.scheduler, original)); + Assert.assertTrue(observer.close()); + observerWorker.shutdownNow(); + observerDatabase.shutdownNow(); + Assert.assertTrue(observerWorker.awaitTermination(10L, TimeUnit.SECONDS)); + Assert.assertTrue(observerDatabase.awaitTermination(10L, TimeUnit.SECONDS)); + } + } + Assert.assertFalse("completed owner still visible after force deletion", publicPresent); + Assert.assertFalse("completed owner still listed after force deletion", idsPresent); + Assert.assertFalse("real done save recreated force-deleted task", dbResurrected); + } + + @Test + public void testDoneSaveBeforeDeleteRetainsNativeCleanupOwner() throws Exception { + this.doneSaveBeforeDelete(true); + } + + @Test + public void testNonForceCompletedDeleteRetainsNativeCleanupOwner() throws Exception { + this.doneSaveBeforeDelete(false); + } + + @Test + public void testNonForceDeleteDatabaseFailureCanRetryWithDurableCompletion() throws Exception { + this.doneSaveBeforeDelete(false, true); + } + + private void doneSaveBeforeDelete(boolean force) throws Exception { + this.doneSaveBeforeDelete(force, false); + } + + private void doneSaveBeforeDelete(boolean force, boolean failure) throws Exception { + try (Fixture fixture = new Fixture("standard_save_before_delete"); + SchedulerBinding binding = new SchedulerBinding(fixture)) { + CountDownLatch closing = new CountDownLatch(1); + CountDownLatch close = new CountDownLatch(1); + AtomicBoolean first = new AtomicBoolean(true); + Mockito.doAnswer(invocation -> { + if (Thread.currentThread().getName().equals("standard-test-worker") && + first.compareAndSet(true, false)) { + closing.countDown(); + await(close); + } + return invocation.callRealMethod(); + }).when(fixture.params).closeTx(); + HugeTask task = fixture.task(new ResultJob()); + try { + fixture.scheduler.schedule(task); + Assert.assertTrue(closing.await(10L, TimeUnit.SECONDS)); + Assert.assertEquals(TaskStatus.SUCCESS, fixture.scheduler.findTask(task.id()).status()); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + if (failure) { + org.apache.hugegraph.task.TaskTransaction transaction = (org.apache.hugegraph.task.TaskTransaction) + Whitebox.getInternalState(fixture.scheduler, "taskTx"); + org.apache.hugegraph.task.TaskTransaction rejecting = Mockito.spy(transaction); + Error rejected = new AssertionError("native completed delete rejected before mutation"); + Mockito.doThrow(rejected).doCallRealMethod().when(rejecting) + .removeTaskVertex(Mockito.any(org.apache.hugegraph.structure.HugeVertex.class)); + Whitebox.setInternalState(fixture.scheduler, "taskTx", rejecting); + Throwable failed = Assert.assertThrows(RuntimeException.class, + () -> fixture.scheduler.delete(task.id(), force)); + Throwable cause = failed; + while (cause.getCause() != null) { + cause = cause.getCause(); + } + Assert.assertSame(rejected, cause); + Assert.assertEquals(TaskStatus.SUCCESS, fixture.scheduler.findTask(task.id()).status()); + Assert.assertEquals(TaskStatus.SUCCESS, fixture.scheduler.task(task.id()).status()); + Assert.assertEquals(TaskStatus.DELETING, task.status()); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + } + fixture.scheduler.delete(task.id(), force); + Assert.assertThrows(org.apache.hugegraph.exception.NotFoundException.class, + () -> fixture.scheduler.findTask(task.id())); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + Assert.assertFalse(fixture.scheduler.close()); + assertTaskAbsent(fixture.scheduler, task); + } finally { + close.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + } + assertTaskAbsent(fixture.scheduler, task); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + } + } + + @Test + public void testDeleteDatabaseFailureRetainsRecordForRetryAndOriginalError() throws Exception { + try (Fixture fixture = new Fixture("standard_delete_db_failure"); + SchedulerBinding binding = new SchedulerBinding(fixture)) { + CompletionSaveJob job = new CompletionSaveJob(); + HugeTask task = fixture.task(job); + try { + fixture.scheduler.schedule(task); + Assert.assertTrue(job.beforeSave.await(10L, TimeUnit.SECONDS)); + org.apache.hugegraph.task.TaskTransaction transaction = (org.apache.hugegraph.task.TaskTransaction) + Whitebox.getInternalState(fixture.scheduler, "taskTx"); + org.apache.hugegraph.task.TaskTransaction rejecting = Mockito.spy(transaction); + Error rejected = new AssertionError("native delete boundary rejected before mutation"); + Mockito.doThrow(rejected).doCallRealMethod().when(rejecting) + .removeTaskVertex(Mockito.any(org.apache.hugegraph.structure.HugeVertex.class)); + Whitebox.setInternalState(fixture.scheduler, "taskTx", rejecting); + Throwable failed = Assert.assertThrows(RuntimeException.class, + () -> fixture.scheduler.delete(task.id(), true)); + Throwable cause = failed; + while (cause.getCause() != null) { + cause = cause.getCause(); + } + Assert.assertSame(rejected, cause); + Assert.assertNotNull(fixture.scheduler.findTask(task.id())); + Assert.assertNotNull(fixture.scheduler.task(task.id())); + Assert.assertEquals(TaskStatus.DELETING, task.status()); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + fixture.scheduler.delete(task.id(), true); + assertTaskAbsent(fixture.scheduler, task); + job.releaseSave.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertTrue(job.saved.await(10L, TimeUnit.SECONDS)); + assertTaskAbsent(fixture.scheduler, task); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + } finally { + job.releaseSave.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + } + } + } + + @Test + public void testUnownedExplicitSaveCanRestoreDeletingRecord() throws Exception { + try (Fixture fixture = new Fixture("standard_unowned_explicit_save")) { + HugeTask task = fixture.task(new EmptyJob()); + task.overwriteStatus(TaskStatus.DELETING); + fixture.scheduler.save(task); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + Assert.assertEquals(TaskStatus.DELETING, fixture.scheduler.task(task.id()).status()); + fixture.scheduler.delete(task.id(), true); + assertTaskAbsent(fixture.scheduler, task); + fixture.scheduler.save(task); + Assert.assertEquals(TaskStatus.DELETING, fixture.scheduler.findTask(task.id()).status()); + Assert.assertEquals(0, fixture.scheduler.pendingTasks()); + } + } + + @Test + public void testNonForceDeleteStillRequiresPersistedCompletion() throws Exception { + try (Fixture fixture = new Fixture("standard_nonforce_durable_check"); + SchedulerBinding binding = new SchedulerBinding(fixture)) { + CompletionSaveJob job = new CompletionSaveJob(); + HugeTask task = fixture.task(job); + try { + fixture.scheduler.schedule(task); + Assert.assertTrue(job.beforeSave.await(10L, TimeUnit.SECONDS)); + Assert.assertEquals(TaskStatus.SUCCESS, task.status()); + Assert.assertEquals(TaskStatus.RUNNING, fixture.scheduler.findTask(task.id()).status()); + Assert.assertThrows(RuntimeException.class, () -> fixture.scheduler.delete(task.id(), false)); + Assert.assertEquals(TaskStatus.SUCCESS, task.status()); + Assert.assertEquals(TaskStatus.RUNNING, fixture.scheduler.findTask(task.id()).status()); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + Assert.assertFalse(task.isCancelled()); + job.releaseSave.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + Assert.assertEquals(TaskStatus.SUCCESS, fixture.scheduler.findTask(task.id()).status()); + fixture.scheduler.delete(task.id(), false); + assertTaskAbsent(fixture.scheduler, task); + } finally { + job.releaseSave.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + } + } + } + + @Test + public void testNonForceDeleteRejectsActuallyRunningTask() throws Exception { + try (Fixture fixture = new Fixture("standard_nonforce_running"); + SchedulerBinding binding = new SchedulerBinding(fixture)) { + BlockingJob job = new BlockingJob(); + HugeTask task = fixture.task(job); + try { + fixture.scheduler.schedule(task); + Assert.assertTrue(job.started.await(10L, TimeUnit.SECONDS)); + Assert.assertEquals(TaskStatus.RUNNING, task.status()); + Assert.assertThrows(IllegalArgumentException.class, () -> fixture.scheduler.delete(task.id(), false)); + Assert.assertEquals(TaskStatus.RUNNING, task.status()); + Assert.assertNotNull(fixture.scheduler.findTask(task.id())); + Assert.assertEquals(1, fixture.scheduler.pendingTasks()); + Assert.assertFalse(task.isCancelled()); + } finally { + job.release.countDown(); + fixture.worker.submit(() -> null).get(10L, TimeUnit.SECONDS); + } + } + } + + private static void assertTaskAbsent(StandardTaskScheduler scheduler, HugeTask task) { + for (boolean withResult : new boolean[]{true, false}) { + Assert.assertThrows(org.apache.hugegraph.exception.NotFoundException.class, + () -> scheduler.task(task.id(), withResult)); + Assert.assertFalse(scheduler.tasks(java.util.Collections.singletonList(task.id()), withResult).hasNext()); + } + Assert.assertThrows(org.apache.hugegraph.exception.NotFoundException.class, + () -> scheduler.findTask(task.id())); + } + + private static final class SchedulerBinding implements AutoCloseable { + private final java.util.Map schedulers; + private final HugeGraphParams nativeParams; + private final org.apache.hugegraph.task.TaskScheduler original; + private final StandardTaskScheduler owner; + @SuppressWarnings("unchecked") + private SchedulerBinding(Fixture fixture) { + this.schedulers = (java.util.Map) + Whitebox.getInternalState(org.apache.hugegraph.task.TaskManager.instance(), "schedulers"); + this.nativeParams = (HugeGraphParams) Whitebox.getInternalState(fixture.graph, "params"); + this.owner = fixture.scheduler; + this.original = this.schedulers.put(this.nativeParams, this.owner); + Assert.assertNotNull(this.original); + Assert.assertSame(this.owner, fixture.graph.taskScheduler()); + } + @Override + public void close() { + Assert.assertTrue(this.schedulers.replace(this.nativeParams, this.owner, this.original)); + } + } + + private static void await(CountDownLatch latch) { + boolean interrupted = false; + while (true) { + try { + latch.await(); + break; + } catch (InterruptedException ignored) { + interrupted = true; + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + + private static class Fixture implements AutoCloseable { + + private final HugeGraph graph; + private final HugeGraphParams params; + private final ExecutorService worker; + private final ExecutorService database; + private final StandardTaskScheduler scheduler; + private final List jobs = new ArrayList<>(); + private long nextId = 9998000L; + + private Fixture(String name) { + RegisterUtil.registerBackends(); + HugeConfig config = FakeObjects.newConfig(); + config.setProperty("backend", "memory"); + config.setProperty("serializer", "text"); + config.setProperty("store", name); + this.graph = HugeFactory.open(config); + this.graph.initBackend(); + this.graph.serverStarted(GlobalMasterInfo.master("standard-owner-test")); + this.params = Mockito.spy((HugeGraphParams) Whitebox.getInternalState(this.graph, "params")); + this.worker = Executors.newSingleThreadExecutor(runnable -> + new Thread(runnable, "standard-test-worker")); + this.database = Executors.newSingleThreadExecutor(); + this.scheduler = new StandardTaskScheduler(this.params, this.worker, this.database); + } + + private HugeTask task(TaskCallable job) { + if (job instanceof BlockingJob) { + this.jobs.add((BlockingJob) job); + } + HugeTask task = new HugeTask<>(IdGenerator.of(this.nextId++), null, job); + task.type("test"); + task.name("standard-owner"); + return task; + } + + @Override + public void close() throws Exception { + for (BlockingJob job : this.jobs) { + job.release.countDown(); + } + this.scheduler.close(); + this.worker.shutdown(); + Assert.assertTrue(this.worker.awaitTermination(10L, TimeUnit.SECONDS)); + Assert.assertTrue(this.scheduler.close()); + this.database.shutdownNow(); + Assert.assertTrue(this.database.awaitTermination(10L, TimeUnit.SECONDS)); + this.graph.close(); + HugeFactory.remove(this.graph); + } + } + + public static class EmptyJob extends SysJob { + + @Override + public String type() { + return "test"; + } + + @Override + public Object execute() { + return null; + } + } + + public static class CompletionSaveJob extends EmptyJob { + private final CountDownLatch beforeSave = new CountDownLatch(1); + private final CountDownLatch releaseSave = new CountDownLatch(1); + private final CountDownLatch saved = new CountDownLatch(1); + @Override + public Object execute() { + this.graph().vertices().hasNext(); + return "completion-result"; + } + @Override + protected void done() { + this.beforeSave.countDown(); + await(this.releaseSave); + try { + super.done(); + } finally { + this.saved.countDown(); + } + } + } + + public static class ResultJob extends EmptyJob { + @Override + public Object execute() { + return "ordinary-result"; + } + } + + public static class BlockingJob extends SysJob { + + private final CountDownLatch started = new CountDownLatch(1); + private final CountDownLatch release = new CountDownLatch(1); + + @Override + public String type() { + return "test"; + } + + @Override + public Object execute() { + this.graph().vertices().hasNext(); + this.started.countDown(); + await(this.release); + return null; + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java index 2f92324808..89e83b7bb0 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java @@ -17,7 +17,12 @@ package org.apache.hugegraph.api; -import org.junit.Before; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.hugegraph.util.JsonUtil; +import org.junit.Assert; import org.junit.Test; import com.google.common.collect.ImmutableMap; @@ -30,43 +35,99 @@ public class ArthasApiTest extends BaseApiTest { private static final String ARTHAS_API_BASE_URL = "http://127.0.0.1:8561"; private static final String ARTHAS_API_PATH = "/api"; - @Before + @Test public void testArthasStart() { Response r = client().put(ARTHAS_START_PATH, "", "", ImmutableMap.of()); - assertResponseStatus(200, r); + String result = assertResponseStatus(200, r); + Assert.assertEquals(4, JsonUtil.fromJson(result, Map.class).size()); + assertJsonContains(result, "arthas.telnetPort"); + assertJsonContains(result, "arthas.httpPort"); + assertJsonContains(result, "arthas.ip"); + assertJsonContains(result, "arthas.disabledCommands"); + Assert.assertEquals("127.0.0.1", assertJsonContains(result, "arthas.ip")); + } + + @Test + public void testAnonymousCannotActivate() { + RestClient anonymous = new RestClient(BASE_URL, false); + try { + assertResponseStatus(401, anonymous.put(ARTHAS_START_PATH, "", "", ImmutableMap.of())); + } finally { + anonymous.close(); + } + } + + @Test + public void testNonAdminCannotActivate() { + String username = "arthas_user_" + UUID.randomUUID().toString().replace("-", ""); + String usersPath = "graphspaces/DEFAULT/auth/users"; + String userBody = JsonUtil.toJson(ImmutableMap.of("user_name", username, + "user_password", "Arthas1234")); + String created = assertResponseStatus(201, client().post(usersPath, userBody)); + String userId = assertJsonContains(created, "id"); + RestClient user = new RestClient(BASE_URL, username, "Arthas1234"); + try { + assertResponseStatus(403, user.put(ARTHAS_START_PATH, "", "", ImmutableMap.of())); + } finally { + user.close(); + assertResponseStatus(204, client().delete(usersPath, userId)); + } } @Test public void testArthasApi() { - // command exec - String execBody = "{\n" + - " \"action\": \"exec\",\n" + - " \"command\": \"version\"\n" + - "}"; + testArthasStart(); RestClient arthasApiClient = new RestClient(ARTHAS_API_BASE_URL, false); - Response execResponse = arthasApiClient.post(ARTHAS_API_PATH, execBody); - String result = assertResponseStatus(200, execResponse); - assertJsonContains(result, "state"); - assertJsonContains(result, "body"); + try { + // A one-time command must succeed through its automatic session cleanup. + String execBody = JsonUtil.toJson(ImmutableMap.of("action", "exec", "command", "version")); + Response execResponse = arthasApiClient.post(ARTHAS_API_PATH, execBody); + assertVersionSucceeded(assertResponseStatus(200, execResponse)); + assertResponseStatus(404, arthasApiClient.get("/mcp")); + + String sessionBody = JsonUtil.toJson(ImmutableMap.of("action", "init_session")); + Response sessionResponse = arthasApiClient.post(ARTHAS_API_PATH, sessionBody); + String sessionResult = assertResponseStatus(200, sessionResponse); + Assert.assertEquals("SUCCEEDED", assertJsonContains(sessionResult, "state")); + String sessionId = assertJsonContains(sessionResult, "sessionId"); + assertJsonContains(sessionResult, "consumerId"); + try { + String sessionExecBody = JsonUtil.toJson(ImmutableMap.of( + "action", "exec", "command", "version", "sessionId", sessionId)); + Response sessionExecResponse = arthasApiClient.post(ARTHAS_API_PATH, sessionExecBody); + assertVersionSucceeded(assertResponseStatus(200, sessionExecResponse)); + } finally { + String closeBody = JsonUtil.toJson(ImmutableMap.of("action", "close_session", "sessionId", sessionId)); + Response closeResponse = arthasApiClient.post(ARTHAS_API_PATH, closeBody); + String closeResult = assertResponseStatus(200, closeResponse); + Assert.assertEquals("SUCCEEDED", assertJsonContains(closeResult, "state")); + } - // command session - String sessionBody = "{\n" + - " \"action\":\"init_session\"\n" + - "}"; - Response sessionResponse = arthasApiClient.post(ARTHAS_API_PATH, sessionBody); - String sessionResult = assertResponseStatus(200, sessionResponse); - assertJsonContains(sessionResult, "sessionId"); - assertJsonContains(sessionResult, "consumerId"); - assertJsonContains(sessionResult, "state"); + String infoBody = JsonUtil.toJson(ImmutableMap.of("action", "session_info", "sessionId", sessionId)); + Response infoResponse = arthasApiClient.post(ARTHAS_API_PATH, infoBody); + String infoResult = assertResponseStatus(200, infoResponse); + Assert.assertEquals("FAILED", assertJsonContains(infoResult, "state")); + Assert.assertEquals("session not found: " + sessionId, assertJsonContains(infoResult, "message")); + + String joinBody = JsonUtil.toJson(ImmutableMap.of("action", "join_session", "sessionId", "xxx")); + Response joinResponse = arthasApiClient.post(ARTHAS_API_PATH, joinBody); + String joinResult = assertResponseStatus(200, joinResponse); + Assert.assertEquals("FAILED", assertJsonContains(joinResult, "state")); + Assert.assertEquals("session not found: xxx", assertJsonContains(joinResult, "message")); + } finally { + arthasApiClient.close(); + } + } - // join session: using invalid sessionId - String joinSessionBody = "{\n" + - " \"action\":\"join_session\",\n" + - " \"sessionId\" : \"xxx\"\n" + - "}"; - Response joinSessionResponse = arthasApiClient.post(ARTHAS_API_PATH, joinSessionBody); - String joinSessionResult = assertResponseStatus(200, joinSessionResponse); - assertJsonContains(joinSessionResult, "message"); - assertJsonContains(joinSessionResult, "state"); + private static void assertVersionSucceeded(String result) { + Assert.assertEquals("SUCCEEDED", assertJsonContains(result, "state")); + Map body = assertJsonContains(result, "body"); + Assert.assertEquals("TERMINATED", body.get("jobStatus")); + Assert.assertEquals(false, body.get("timeExpired")); + List> results = assertMapContains(body, "results"); + Map version = assertArrayContains(results, "type", "version"); + Assert.assertEquals("4.1.5", version.get("version")); + Map status = assertArrayContains(results, "type", "status"); + Assert.assertEquals(0, status.get("statusCode")); } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/BaseApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/BaseApiTest.java index abe4b6839c..92f9fba3e9 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/BaseApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/BaseApiTest.java @@ -29,7 +29,7 @@ import java.util.stream.Collectors; import org.apache.http.util.TextUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.util.CollectionUtil; import org.apache.hugegraph.util.JsonUtil; import org.apache.tinkerpop.shaded.jackson.core.type.TypeReference; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherAnonymousApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherAnonymousApiTest.java new file mode 100644 index 0000000000..c8f5188780 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherAnonymousApiTest.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.hugegraph.api; + +import java.io.IOException; +import java.util.Map; + +import org.apache.hugegraph.api.BaseApiTest.RestClient; +import org.junit.Assert; +import org.junit.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.ws.rs.core.Response; + +public class CypherAnonymousApiTest { + + private static final String PATH = BaseApiTest.URL_PREFIX + "/cypher"; + private static final String QUERY = "RETURN 1 AS value"; + private static final ObjectMapper JSON = new ObjectMapper(); + + @Test + public void testAnonymousGet() throws IOException { + RestClient client = new RestClient(BaseApiTest.baseUrl(), false); + try { + assertQueryResult(client.get(PATH, Map.of("cypher", QUERY))); + } finally { + client.close(); + } + } + + @Test + public void testAnonymousPost() throws IOException { + RestClient client = new RestClient(BaseApiTest.baseUrl(), false); + try { + assertQueryResult(client.post(PATH, QUERY)); + } finally { + client.close(); + } + } + + private static void assertQueryResult(Response response) throws IOException { + JsonNode result = JSON.readTree(BaseApiTest.assertResponseStatus(200, response)); + Assert.assertEquals(200, result.path("status").path("code").asInt()); + JsonNode data = result.path("result").path("data"); + Assert.assertTrue(data.isArray()); + Assert.assertEquals(1, data.size()); + Assert.assertEquals(1, data.get(0).path("value").asInt()); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java index 3c3e3049f3..3d0c84d1b1 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java @@ -19,29 +19,72 @@ import static org.apache.hugegraph.testutil.Assert.assertContains; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.Deque; +import java.util.List; import java.util.Map; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.util.JsonUtil; import org.junit.Before; import org.junit.Test; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; public class CypherApiTest extends BaseApiTest { private static final String PATH = URL_PREFIX + "/cypher"; - private static final String QUERY = "MATCH (n:person) where n.city ='Beijing' return n"; - private static final String QUERY_RESULT = "Beijing"; + private static final String QUERY = "MATCH (n:cypher_person) WHERE n.city = 'Beijing' " + + "RETURN n.name AS name ORDER BY name"; @Before public void prepareSchema() { - BaseApiTest.initPropertyKey(); - BaseApiTest.initVertexLabel(); - BaseApiTest.initEdgeLabel(); - BaseApiTest.initIndexLabel(); - BaseApiTest.initVertex(); - BaseApiTest.initEdge(); + String schema = URL_PREFIX + "/schema/"; + for (String[] property : new String[][]{{"name", "TEXT"}, {"age", "INT"}, + {"city", "TEXT"}, {"active", "BOOLEAN"}, + {"note", "TEXT"}, {"birthday", "DATE"}}) { + createAndAssert(schema + "propertykeys", JsonUtil.toJson(ImmutableMap.of( + "name", property[0], "data_type", property[1], + "cardinality", "SINGLE")), 202); + } + createAndAssert(schema + "vertexlabels", "{\"name\":\"cypher_person\", " + + "\"id_strategy\":\"PRIMARY_KEY\",\"primary_keys\":[\"name\"], " + + "\"properties\":[\"name\",\"age\",\"city\",\"active\",\"note\"," + + "\"birthday\"],\"nullable_keys\":[\"active\",\"note\",\"birthday\"]}"); + createAndAssert(schema + "edgelabels", "{\"name\":\"cypher_knows\", " + + "\"source_label\":\"cypher_person\",\"target_label\":\"cypher_person\"," + + "\"frequency\":\"SINGLE\",\"properties\":[],\"nullable_keys\":[]}"); + // Install indexes before inserting data; no existing data needs rebuilding. + for (String[] index : new String[][]{{"city", "SECONDARY"}, {"age", "RANGE"}}) { + createAndAssert(schema + "indexlabels", JsonUtil.toJson(ImmutableMap.builder() + .put("name", "cypher_person_by_" + index[0]) + .put("base_type", "VERTEX_LABEL") + .put("base_value", "cypher_person") + .put("index_type", index[1]) + .put("fields", Collections.singletonList(index[0])) + .put("rebuild", false).build()), 202); + } + addVertex("marko", 20, "Beijing", true, "2000-01-01"); + addVertex("peter", 30, "Beijing", false); + addVertex("alice", 40, "Shanghai", true); + addVertex("dave", 30, "Shanghai", false); + addEdge("marko", "peter"); + addEdge("peter", "alice"); } @Test @@ -49,36 +92,754 @@ public void testGet() { Map params = ImmutableMap.of("cypher", QUERY); Response r = client().get(PATH, params); - this.validStatusAndTextContains(QUERY_RESULT, r); + assertColumn(assertCypherSuccessData(assertResponseStatus(200, r)), "name", "marko", "peter"); } @Test public void testPost() { - this.testCypherQueryAndContains(QUERY, QUERY_RESULT); + assertColumn(query(QUERY), "name", "marko", "peter"); + } + + @Test + public void testRejectPlainTextPost() { + Response response = client().post(PATH, Entity.entity(QUERY, MediaType.TEXT_PLAIN_TYPE)); + assertResponseStatus(415, response); + } + + @Test + public void testComputedRegexExecutesExtensionPredicate() { + String query = "MATCH (n:cypher_person) WHERE (n.name + '') =~ 'mar.*' " + + "RETURN n.name AS name"; + String content = this.testCypherQueryAndContains(query, "marko"); + Assert.assertEquals(List.of(Map.of("name", "marko")), assertCypherSuccessData(content)); + } + + @Test + public void testComputedRegexRequiresWholeStringMatch() { + String query = "MATCH (n:cypher_person) WHERE (n.name + '') =~ 'ark' " + + "RETURN n.name AS name"; + String content = this.testCypherQueryAndContains(query, "data"); + Assert.assertTrue(assertCypherSuccessData(content).isEmpty()); + } + + @Test + public void testDateFunctionMatchesDateProperty() { + assertColumn(query("MATCH (n:cypher_person) WHERE n.birthday = date('2000-01-01') " + + "RETURN n.name AS name"), "name", "marko"); + } + + @Test + public void testDateFunctionYieldsNoMatchOnTextProperty() { + Map row = assertSingleMapRow(query( + "MATCH (n:cypher_person {note: date('2024-02-05')}) RETURN count(n) AS count")); + Assert.assertEquals(0L, ((Number) row.get("count")).longValue()); } @Test public void testCreate() { - this.testCypherQueryAndContains("CREATE (n:person { name : 'test', " + - "age: 20, city: 'Hefei' }) return n", - "Hefei"); + assertColumn(query("CREATE (n:cypher_person {name:'test', age:20, city:'Hefei'}) " + + "RETURN n.name AS name"), "name", "test"); + assertNativeVertex("test", 20, "Hefei"); } @Test public void testRelationQuery() { - String cypher = "MATCH (n:person)-[r:knows]->(friend:person)\n" + + String cypher = "MATCH (n:cypher_person)-[r:cypher_knows]->(friend:cypher_person)\n" + "WHERE n.name = 'marko'\n" + - "RETURN n, friend.name AS friend"; - this.testCypherQueryAndContains(cypher, "friend"); + "RETURN friend.name AS friend"; + assertColumn(query(cypher), "friend", "peter"); + } + + @Test + public void testReturnNodeIdAsPrimitiveValue() { + String cypher = "MATCH (n:cypher_person) WHERE n.name = 'marko' " + + "RETURN id(n) AS nodeId"; + + String content = this.testCypherQueryAndContains(cypher, "nodeId"); + List data = assertCypherSuccessData(content); + Map row = assertSingleMapRow(data); + Object nodeId = row.get("nodeId"); + + Assert.assertEquals(vertexId("marko"), nodeId); + assertPrimitiveValue(nodeId); + assertNoHugeGraphIdLeak(content); + } + + @Test + public void testReturnNodeDoesNotLeakInternalIdTypes() { + String cypher = "MATCH (n:cypher_person) WHERE n.name = 'marko' RETURN n"; + + String content = this.testCypherQueryAndContains(cypher, "marko"); + Map node = assertMapValue(assertSingleMapRow(assertCypherSuccessData(content)), "n"); + Assert.assertEquals("cypher_person", node.get("_label")); + Assert.assertEquals("marko", node.get("name")); + Assert.assertEquals(20, ((Number) node.get("age")).intValue()); + Assert.assertEquals("Beijing", node.get("city")); + assertNoHugeGraphIdLeak(content); + } + + @Test + public void testReturnNestedIdDoesNotLeakInternalIdTypes() { + String cypher = "MATCH (n:cypher_person) WHERE n.name = 'marko' " + + "RETURN {nodeId: id(n), values: [id(n), n.name]} " + + "AS payload"; + + String content = this.testCypherQueryAndContains(cypher, "payload"); + List data = assertCypherSuccessData(content); + Map row = assertSingleMapRow(data); + Map payload = assertMapValue(row, "payload"); + List values = assertListValue(payload, "values"); + Object nodeId = payload.get("nodeId"); + + Assert.assertEquals(vertexId("marko"), nodeId); + assertPrimitiveValue(nodeId); + Assert.assertEquals(2, values.size()); + Assert.assertEquals(nodeId, values.get(0)); + Assert.assertEquals("marko", values.get(1)); + assertNoHugeGraphIdLeak(content); + } + + @Test + public void testReturnRelationIdDoesNotLeakInternalIdTypes() { + String cypher = "MATCH (n:cypher_person)-[r:cypher_knows]->(friend:cypher_person) " + + "WHERE n.name = 'marko' RETURN id(r) AS relationId"; + + String content = this.testCypherQueryAndContains(cypher, "relationId"); + List data = assertCypherSuccessData(content); + Map row = assertSingleMapRow(data); + Object relationId = row.get("relationId"); + + Assert.assertNotNull(relationId); + assertPrimitiveValue(relationId); + assertNoHugeGraphIdLeak(content); + } + + @Test + public void testReturnPathShape() { + String cypher = "MATCH p=(n:cypher_person)-[r:cypher_knows]->(friend:cypher_person) " + + "WHERE n.name = 'marko' RETURN p AS path"; + + String content = this.testCypherQueryAndContains(cypher, "path"); + List data = assertCypherSuccessData(content); + Map row = assertSingleMapRow(data); + List path = assertListValue(row, "path"); + + Assert.assertEquals(3, path.size()); + Map source = assertMapValue(path, 0); + Map relation = assertMapValue(path, 1); + Map target = assertMapValue(path, 2); + + Assert.assertEquals("node", source.get("_type")); + Assert.assertEquals("cypher_person", source.get("_label")); + Assert.assertEquals("marko", source.get("name")); + Assert.assertEquals("cypher_knows", relation.get("_label")); + Assert.assertEquals("node", target.get("_type")); + Assert.assertEquals("cypher_person", target.get("_label")); + Assert.assertEquals("peter", target.get("name")); + assertContains("marko", content); + assertContains("peter", content); + assertNoHugeGraphIdLeak(content); + } + + @Test + public void testExactReadsAndPredicates() { + assertColumn(query("MATCH (n:cypher_person) RETURN n.name AS name ORDER BY name"), + "name", "alice", "dave", "marko", "peter"); + assertColumn(query("MATCH (n:cypher_person) WHERE n.age >= 30 AND " + + "(n.active = true OR n.name = 'dave') " + + "RETURN n.name AS name ORDER BY name"), "name", "alice", "dave"); + assertColumn(query("MATCH (n:cypher_person)-[:cypher_knows]->(m:cypher_person) " + + "RETURN n.name AS name ORDER BY name"), "name", "marko", "peter"); + assertColumn(query("MATCH (n:cypher_person)-[:cypher_knows]->()" + + "-[:cypher_knows]->(m:cypher_person) RETURN m.name AS name"), + "name", "alice"); + Assert.assertTrue(query("MATCH (n:cypher_person) WHERE n.name = 'absent' RETURN n").isEmpty()); + } + + @Test + public void testDuplicatesDistinctAndPagination() { + assertColumn(query("MATCH (n:cypher_person) RETURN n.city AS city ORDER BY city"), + "city", "Beijing", "Beijing", "Shanghai", "Shanghai"); + assertColumn(query("MATCH (n:cypher_person) RETURN DISTINCT n.city AS city ORDER BY city"), + "city", "Beijing", "Shanghai"); + assertColumn(query("MATCH (n:cypher_person) RETURN n.name AS name " + + "ORDER BY n.age, name SKIP 1 LIMIT 2"), "name", "dave", "peter"); + } + + @Test + public void testAggregates() { + Map row = assertSingleMapRow(query("MATCH (n:cypher_person) " + + "RETURN count(n) AS count, sum(n.age) AS sum, min(n.age) AS min, " + + "max(n.age) AS max, avg(n.age) AS avg")); + Assert.assertEquals(5, row.size()); + for (String key : Arrays.asList("count", "sum", "min", "max", "avg")) { + Assert.assertInstanceOf(Number.class, row.get(key)); + } + Assert.assertEquals(4L, ((Number) row.get("count")).longValue()); + Assert.assertEquals(120L, ((Number) row.get("sum")).longValue()); + Assert.assertEquals(20L, ((Number) row.get("min")).longValue()); + Assert.assertEquals(40L, ((Number) row.get("max")).longValue()); + Assert.assertEquals(30.0D, ((Number) row.get("avg")).doubleValue(), 0.0D); + } + + @Test + public void testOrderByNullableBooleanGroupingKey() { + // #3319: sorting a Boolean-or-null grouping key once crashed with a + // ClassCastException when the translator null marker met ordered values + createAndAssert(URL_PREFIX + "/graph/vertices", JsonUtil.toJson(ImmutableMap.of( + "label", "cypher_person", + "properties", ImmutableMap.of("name", "noactive", "age", 50, + "city", "Beijing")))); + String cypher = "MATCH (n:cypher_person) " + + "OPTIONAL MATCH (n)-[r0:cypher_knows]->(n1 {city: 'Nowhere'}) " + + "WITH n.active AS g, count(n1.name) AS c " + + "RETURN g, c ORDER BY g "; + List rows = query(cypher + "DESC"); + Assert.assertEquals(3, rows.size()); + assertGroupRow(rows.get(0), null); + assertGroupRow(rows.get(1), true); + assertGroupRow(rows.get(2), false); + + rows = query(cypher + "ASC"); + Assert.assertEquals(3, rows.size()); + assertGroupRow(rows.get(0), false); + assertGroupRow(rows.get(1), true); + assertGroupRow(rows.get(2), null); + + query("MATCH (n:cypher_person {name: 'noactive'}) DETACH DELETE n"); + Assert.assertEquals(4, nativeVertices().size()); + } + + private static void assertGroupRow(Object row, Object groupKey) { + Map map = assertMapValue(Collections.singletonList(row), 0); + Assert.assertEquals(2, map.size()); + Assert.assertEquals(groupKey, map.get("g")); + Assert.assertEquals(0L, ((Number) map.get("c")).longValue()); + } + + @Test + public void testParameters() { + assertColumn(bound("MATCH (n:cypher_person) WHERE n.name = $name AND " + + "n.age >= $age AND n.active = $active RETURN n.name AS name", + ImmutableMap.of("name", "marko", "age", 20, "active", true)), + "name", "marko"); + String text = "quote ' and \" newline\n MATCH (n) DELETE n //"; + assertColumn(bound("RETURN $text AS text", ImmutableMap.of("text", text)), "text", text); + assertColumn(bound("MATCH (n:cypher_person) RETURN n.name AS name ORDER BY name", + Collections.emptyMap()), "name", "alice", "dave", "marko", "peter"); + assertColumn(assertCypherSuccessData(assertResponseStatus(200, client().post(PATH, + "{\"cypher\":\"RETURN 1 AS value\"}"))), "value", 1); + assertExecutionError(client().post(PATH, JsonUtil.toJson(ImmutableMap.of( + "cypher", "RETURN $missing AS value", "parameters", Collections.emptyMap())))); + assertColumn(bound("RETURN $value AS value", Collections.singletonMap("value", null)), + "value", (Object) null); + Map reservedNames = assertSingleMapRow(bound("RETURN $id AS id, $label AS label", + ImmutableMap.of("id", "bound-id", "label", "bound-label"))); + Assert.assertEquals(ImmutableMap.of("id", "bound-id", "label", "bound-label"), reservedNames); + assertColumn(bound("RETURN '$not_a_binding' AS value // $ignored", + Collections.emptyMap()), "value", "$not_a_binding"); + assertExecutionError(client().post(PATH, JsonUtil.toJson(ImmutableMap.of( + "cypher", "RETURN {nested: $missing} AS value", + "parameters", Collections.emptyMap())))); + Assert.assertEquals(4, nativeVertices().size()); + } + + @Test + public void testRejectTranslatorNullMarker() { + String marker = " cypher.null"; + for (Object value : Arrays.asList(marker, List.of(marker), Map.of("nested", marker))) { + assertExecutionError(client().post(PATH, JsonUtil.toJson(ImmutableMap.of( + "cypher", "RETURN $value AS value", "parameters", Map.of("value", value))))); + } + assertColumn(bound("RETURN $value AS value", Map.of("value", "cypher.null")), + "value", "cypher.null"); + } + + @Test + public void testNullAndMissingProperty() { + List rows = query("MATCH (n:cypher_person) RETURN n.note AS note"); + Assert.assertEquals(4, rows.size()); + for (Object value : rows) { + Map row = (Map) value; + Assert.assertTrue(row.containsKey("note")); + Assert.assertNull(row.get("note")); + } + Assert.assertEquals(4, query("MATCH (n:cypher_person) WHERE n.note IS NULL RETURN n.name").size()); + Assert.assertTrue(query("MATCH (n:cypher_person) WHERE n.note IS NOT NULL RETURN n.name").isEmpty()); + } + + @Test + public void testCreateSetAndDeleteWithNativeReadback() { + assertColumn(query("CREATE (n:cypher_person {name:'created', age:50, city:'Paris'}) " + + "RETURN n.name AS name"), "name", "created"); + assertNativeVertex("created", 50, "Paris"); + assertColumn(query("MATCH (n:cypher_person) WHERE n.name = 'created' " + + "SET n.age = 51, n.city = 'London' RETURN n.name AS name"), "name", "created"); + assertNativeVertex("created", 51, "London"); + query("MATCH (a:cypher_person), (b:cypher_person) " + + "WHERE a.name = 'created' AND b.name = 'marko' CREATE (a)-[:cypher_knows]->(b)"); + List edges = nativeEdges(); + Assert.assertEquals(3, edges.size()); + boolean found = false; + for (Object value : edges) { + Map edge = (Map) value; + if (vertexId("created").equals(edge.get("outV"))) { + Assert.assertEquals(vertexId("marko"), edge.get("inV")); + Assert.assertEquals("cypher_knows", edge.get("label")); + found = true; + } + } + Assert.assertTrue(found); + query("MATCH (a:cypher_person)-[r:cypher_knows]->() WHERE a.name = 'created' DELETE r"); + Assert.assertEquals(2, nativeEdges().size()); + query("MATCH (n:cypher_person) WHERE n.name = 'created' DELETE n"); + Assert.assertEquals(4, nativeVertices().size()); + Assert.assertNull(nativeVertex("created")); + } + + @Test + public void testFailedWriteDoesNotLeakIntoLaterRequests() { + assertExecutionError(client().post(PATH, + "CREATE (good:cypher_person {name:'rollback', age:33, city:'Beijing'}), " + + "(bad:cypher_person {name:'invalid', age:34, city:'Shanghai', rogue:'x'}) " + + "RETURN good.name")); + Assert.assertNull(nativeVertex("rollback")); + // Failed-write residue became visible after later reads in the regression reproducer. + // Immediate REST readback alone therefore does not establish rollback. + for (int i = 0; i < 32; i++) { + Map row = assertSingleMapRow(query("MATCH (n:cypher_person) RETURN count(n) AS count")); + Assert.assertEquals(4L, ((Number) row.get("count")).longValue()); + Assert.assertEquals(4, nativeVertices().size()); + Assert.assertNull(nativeVertex("rollback")); + Assert.assertNull(nativeVertex("invalid")); + } + } + + @Test + public void testInvalidRequests() { + assertExecutionError(client().post(PATH, "MATCH ( RETURN broken")); + assertExecutionError(client().post(PATH, + "CREATE (:cypher_person {name:'wrongtype', age:'not an integer', city:'Paris'})")); + Assert.assertNull(nativeVertex("wrongtype")); + assertResponseStatus(400, client().get(PATH, ImmutableMap.of("cypher", ""))); + for (String body : Arrays.asList(" \n", "{", "{}", "{\"cypher\":\" \"}", + "{\"cypher\":1}", "[]", "[1, 2]", "\"RETURN 1\"", + " null ", "1", "-1.5", "true", "false", + "{\"cypher\":\"RETURN 1\"} garbage", + "{\"cypher\":\"RETURN 1\"} {}", + "{\"cypher\":\"RETURN 1\",\"parameters\":false}", + "{\"cypher\":\"RETURN 1\",\"parameters\":null}", + "{\"cypher\":\"RETURN 1\",\"parameters\":[]}", + "{\"cypher\":\"RETURN 1\",\"parameters\":3}")) { + assertResponseStatus(400, client().post(PATH, body)); + } + } + + @Test + public void testAuthenticationAndGraphRouting() { + RestClient invalid = new RestClient(BASE_URL, "admin", "invalid-cypher-password"); + try { + assertResponseStatus(401, invalid.post(PATH, "RETURN 1")); + } finally { + invalid.close(); + } + Response response = client().post("graphspaces/DEFAULT/graphs/cypher_missing_graph/cypher", + "MATCH (n) RETURN n"); + assertExecutionError(response); + assertColumn(query("MATCH (n:cypher_person) RETURN n.name AS name ORDER BY name"), + "name", "alice", "dave", "marko", "peter"); + } + + @Test + public void testRejectMissingAuthorization() { + RestClient anonymous = new RestClient(baseUrl(), false); + try { + assertResponseStatus(401, anonymous.get(PATH, ImmutableMap.of("cypher", "RETURN 1"))); + assertResponseStatus(401, anonymous.post(PATH, "RETURN 1")); + } finally { + anonymous.close(); + } + } + + @Test + public void testReadOnlyUserCannotWriteOrReadAnotherGraph() { + assumeStandaloneMode(); + String graph = "cypher_reader_other"; + String graphPath = "graphspaces/DEFAULT/graphs/" + graph; + Deque created = new ArrayDeque<>(); + RestClient reader = null; + boolean graphCreated = false; + try { + String config = JsonUtil.toJson(ImmutableMap.of( + "gremlin.graph", "org.apache.hugegraph.auth.HugeFactoryAuthProxy", + "backend", "memory", "serializer", "text", "store", graph)); + Response response = client().post(graphPath, Entity.json(config)); + graphCreated = response.getStatus() == 201; + assertResponseStatus(201, response); + reader = new RestClient(baseUrl(), "cypher_reader", "cypher-reader-password"); + String group = this.createAuth(created, "auth/groups", + "{\"group_name\":\"cypher_read_only_group\"}"); + String target = this.createAuth(created, "graphspaces/DEFAULT/auth/targets", + "{\"target_name\":\"cypher_read_only_target\"," + + "\"target_graph\":\"hugegraph\"," + + "\"target_resources\":[{\"type\":\"ALL\"}]}"); + for (String permission : ImmutableList.of("READ", "EXECUTE")) { + this.createAuth(created, "graphspaces/DEFAULT/auth/accesses", String.format( + "{\"group\":\"%s\",\"target\":\"%s\",\"access_permission\":\"%s\"}", + group, target, permission)); + } + String user = this.createAuth(created, "graphspaces/DEFAULT/auth/users", + "{\"user_name\":\"cypher_reader\"," + + "\"user_password\":\"cypher-reader-password\"}"); + this.createAuth(created, "graphspaces/DEFAULT/auth/belongs", String.format( + "{\"user\":\"%s\",\"group\":\"%s\"}", user, group)); + + assertColumn(assertCypherSuccessData(assertResponseStatus(200, reader.post(PATH, QUERY))), + "name", "marko", "peter"); + assertPermissionDenied(reader.post(PATH, + "CREATE (n:cypher_person {name:'cypher_denied',age:1,city:'Beijing'}) RETURN n"), + "write", "hugegraph"); + Assert.assertNull(this.nativeVertex("cypher_denied")); + // This existing graph is accessible to the administrator but not to the reader. + assertCypherSuccessData(assertResponseStatus(200, client().post( + graphPath + "/cypher", "MATCH (n) RETURN n"))); + assertPermissionDenied(reader.post(graphPath + "/cypher", "MATCH (n) RETURN n"), + "execute", graph); + } finally { + if (reader != null) { + reader.close(); + } + try { + this.deleteAuth(created); + } finally { + if (graphCreated) { + assertResponseStatus(204, client().delete(graphPath, ImmutableMap.of( + "confirm_message", "I'm sure to drop the graph"))); + } + } + } } - private void testCypherQueryAndContains(String cypher, String containsText) { + @Test + public void testBasicAuthWithColonPassword() { + // The group, target and access setup below uses the standalone auth API; in PD mode + // groups are graphspace-scoped. The header parsing under test is backend-independent. + assumeStandaloneMode(); + // CypherAPI parses the Basic header itself, apart from AuthenticationFilter + String user = "cypher_colon_user"; + // With this user name these encode to standard Base64 containing '+' and '/' + Assert.assertTrue(basicCredential(user, "a:b:c~").contains("+")); + Assert.assertTrue(basicCredential(user, "a:b:c?").contains("/")); + Deque created = new ArrayDeque<>(); + RestClient noAuthClient = new RestClient(baseUrl(), false); + try { + String groupId = this.createAuth(created, "auth/groups", + "{\"group_name\":\"cypher_colon_group\"}"); + String targetId = this.createAuth(created, "graphspaces/DEFAULT/auth/targets", + "{\"target_name\":\"cypher_colon_target\"," + + "\"target_graph\":\"hugegraph\"," + + "\"target_url\":\"127.0.0.1:8080\"," + + "\"target_resources\":[{\"type\":\"ALL\"}]}"); + for (String permission : ImmutableList.of("READ", "EXECUTE")) { + this.createAuth(created, "graphspaces/DEFAULT/auth/accesses", String.format( + "{\"group\":\"%s\",\"target\":\"%s\",\"access_permission\":\"%s\"}", + groupId, targetId, permission)); + } + + for (String password : ImmutableList.of("a:b:c", "a:b:c~", "a:b:c?")) { + Deque member = new ArrayDeque<>(); + try { + String userId = this.createAuth(member, "graphspaces/DEFAULT/auth/users", + String.format("{\"user_name\":\"%s\"," + + "\"user_password\":\"%s\"}", + user, password)); + this.createAuth(member, "graphspaces/DEFAULT/auth/belongs", String.format( + "{\"user\":\"%s\",\"group\":\"%s\"}", userId, groupId)); + + Response r = basicAuthCypher(noAuthClient, user, password); + assertColumn(assertCypherSuccessData(assertResponseStatus(200, r)), + "name", "marko", "peter"); + + r = basicAuthCypher(noAuthClient, user, "wrong" + password); + assertResponseStatus(401, r); + } finally { + this.deleteAuth(member); + } + } + } finally { + noAuthClient.close(); + this.deleteAuth(created); + } + } + + @Test + public void testSpecifiedGraphRouting() { + assumeStandaloneMode(); + String graph = "cypher_route_test"; + String path = "graphspaces/DEFAULT/graphs/" + graph; + assertResponseStatus(201, createGraphInRocksDB("DEFAULT", graph)); + try { + List rows = assertCypherSuccessData(assertResponseStatus(200, + client().post(path + "/cypher", "MATCH (n) RETURN count(n) AS count"))); + Map emptyGraph = assertSingleMapRow(rows); + Assert.assertEquals(0L, ((Number) emptyGraph.get("count")).longValue()); + Map defaultGraph = assertSingleMapRow(query("MATCH (n) RETURN count(n) AS count")); + Assert.assertEquals(4L, ((Number) defaultGraph.get("count")).longValue()); + } finally { + assertResponseStatus(204, client().delete(path, ImmutableMap.of( + "confirm_message", "I'm sure to drop the graph"))); + } + } + + private String createAuth(Deque created, String path, String body) { + String content = assertResponseStatus(201, client().post(path, body)); + String id = assertJsonContains(content, "id"); + created.push(new String[]{path, id}); + return id; + } + + private void deleteAuth(Deque created) { + // Attempt every deletion first, then fail on any record that was left behind + List failures = new ArrayList<>(); + while (!created.isEmpty()) { + String[] entry = created.pop(); + Response r = client().delete(entry[0], entry[1]); + if (r.getStatus() != 204) { + failures.add(String.format("%s/%s: %s %s", entry[0], entry[1], + r.getStatus(), r.readEntity(String.class))); + } + } + Assert.assertTrue("Failed to delete auth records: " + failures, failures.isEmpty()); + } + + private static String basicCredential(String user, String password) { + byte[] credential = (user + ":" + password).getBytes(StandardCharsets.UTF_8); + return Base64.getEncoder().encodeToString(credential); + } + + private static Response basicAuthCypher(RestClient client, String user, String password) { + // Built by hand: Jersey's HttpAuthenticationFeature encodes the credential as ISO-8859-1 + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.AUTHORIZATION, "Basic " + basicCredential(user, password)); + return client.target().path(PATH).queryParam("cypher", QUERY) + .request().headers(headers).get(); + } + + private List query(String cypher) { + return assertCypherSuccessData(assertResponseStatus(200, client().post(PATH, cypher))); + } + + private List bound(String cypher, Map parameters) { + return query(JsonUtil.toJson(ImmutableMap.of("cypher", cypher, "parameters", parameters))); + } + + private static void assertColumn(List rows, String key, Object... expected) { + List actual = new ArrayList<>(); + for (Object value : rows) { + Assert.assertInstanceOf(Map.class, value); + Map row = (Map) value; + Assert.assertEquals(1, row.size()); + Assert.assertTrue(row.containsKey(key)); + actual.add(row.get(key)); + } + Assert.assertEquals(Arrays.asList(expected), actual); + } + + private static void assertExecutionError(Response response) { + String content = assertResponseStatus(200, response); + Map body = JsonUtil.fromJson(content, Map.class); + Map status = assertMapValue(body, "status"); + Assert.assertTrue(content, ((Number) status.get("code")).intValue() >= 400); + Assert.assertFalse(String.valueOf(status.get("message")).isEmpty()); + Assert.assertNull(assertMapValue(body, "result").get("data")); + } + + private static void assertPermissionDenied(Response response, String operation, String graph) { + String content = assertResponseStatus(200, response); + Map body = JsonUtil.fromJson(content, Map.class); + Map status = assertMapValue(body, "status"); + Assert.assertEquals(400, ((Number) status.get("code")).intValue()); + String message = String.valueOf(status.get("message")); + assertContains("Permission denied: " + operation, message); + assertContains("graph=" + graph + ",", message); + Assert.assertNull(assertMapValue(body, "result").get("data")); + } + + private void addVertex(String name, int age, String city, boolean active) { + createAndAssert(URL_PREFIX + "/graph/vertices", JsonUtil.toJson(ImmutableMap.of( + "label", "cypher_person", "properties", ImmutableMap.of( + "name", name, "age", age, "city", city, "active", active)))); + } + + private void addVertex(String name, int age, String city, boolean active, String birthday) { + createAndAssert(URL_PREFIX + "/graph/vertices", JsonUtil.toJson(ImmutableMap.of( + "label", "cypher_person", "properties", ImmutableMap.of( + "name", name, "age", age, "city", city, "active", active, + "birthday", birthday)))); + } + + private void addEdge(String source, String target) { + createAndAssert(URL_PREFIX + "/graph/edges", JsonUtil.toJson( + ImmutableMap.builder() + .put("label", "cypher_knows") + .put("outV", vertexId(source)) + .put("inV", vertexId(target)) + .put("outVLabel", "cypher_person") + .put("inVLabel", "cypher_person") + .put("properties", Collections.emptyMap()) + .build())); + } + + private List nativeVertices() { + return nativeElements("vertices"); + } + + private List nativeEdges() { + return nativeElements("edges"); + } + + private List nativeElements(String type) { + String content = assertResponseStatus(200, client().get(URL_PREFIX + "/graph/" + type, + ImmutableMap.of("limit", -1))); + return assertListValue(JsonUtil.fromJson(content, Map.class), type); + } + + private Map nativeVertex(String name) { + for (Object value : nativeVertices()) { + Map vertex = (Map) value; + if (name.equals(assertMapValue(vertex, "properties").get("name"))) { + return vertex; + } + } + return null; + } + + private Object vertexId(String name) { + Map vertex = nativeVertex(name); + Assert.assertNotNull(vertex); + return vertex.get("id"); + } + + private void assertNativeVertex(String name, int age, String city) { + Map vertex = nativeVertex(name); + Assert.assertNotNull(vertex); + Assert.assertEquals("cypher_person", vertex.get("label")); + Map properties = assertMapValue(vertex, "properties"); + Assert.assertEquals(name, properties.get("name")); + Assert.assertEquals(age, ((Number) properties.get("age")).intValue()); + Assert.assertEquals(city, properties.get("city")); + } + + @Test + public void testSyntaxErrorHasStructuredErrorWithHint() throws Exception { + this.assertError("MATCH (n:cypher_person) RETURN not_defined_var", + "HugeGraph.Cypher.SyntaxError", + "Declare the variable in a MATCH, UNWIND or WITH clause " + + "before referencing it"); + } + + @Test + public void testParserErrorHasStructuredErrorWithHint() throws Exception { + this.assertError("MATCH (n:cypher_person RETURN n", "HugeGraph.Cypher.SyntaxError", + "Check the query syntax and function names supported by " + + "the built-in Cypher translator"); + } + + @Test + public void testMultipleStatementsHaveStructuredError() throws Exception { + this.assertError("MATCH (n) RETURN n; MATCH (m) RETURN m", + "HugeGraph.Cypher.SyntaxError", + "Submit exactly one Cypher statement per request"); + } + + @Test + public void testUndefinedLabelHasSchemaHint() throws Exception { + this.assertError("MATCH (n:robot) RETURN n", "HugeGraph.Cypher.ExecutionError", + "Create the schema element via the schema API before running the query"); + } + + @Test + public void testMissingParameterHasFallbackError() throws Exception { + this.assertError("RETURN $missing AS value", + "HugeGraph.Cypher.ExecutionError", ""); + } + + private void assertError(String query, String code, String hint) throws Exception { + Response r = client().post(PATH, query); + JsonNode response = new ObjectMapper().readTree(assertResponseStatus(200, r)); + Assert.assertEquals(3, response.size()); + Assert.assertFalse(response.has("errors")); + Assert.assertEquals(400, response.at("/status/code").asInt()); + Assert.assertFalse(response.at("/status/message").asText().isEmpty()); + Assert.assertEquals(1, response.at("/status/attributes/errors").size()); + JsonNode error = response.at("/status/attributes/errors/0"); + Assert.assertEquals(code, error.get("code").asText()); + Assert.assertEquals(hint, error.get("hint").asText()); + Assert.assertFalse(error.get("message").asText().isEmpty()); + } + + private String testCypherQueryAndContains(String cypher, + String containsText) { Response r = client().post(PATH, cypher); - this.validStatusAndTextContains(containsText, r); + return this.validStatusAndTextContains(containsText, r); } - private void validStatusAndTextContains(String value, Response r) { + private String validStatusAndTextContains(String value, Response r) { String content = assertResponseStatus(200, r); assertContains(value, content); + assertCypherSuccessData(content); + return content; + } + + private static void assertNoHugeGraphIdLeak(String content) { + Assert.assertFalse(content.contains("org.apache.hugegraph.backend.id")); + Assert.assertFalse(content.contains("StringId")); + Assert.assertFalse(content.contains("LongId")); + Assert.assertFalse(content.contains("UuidId")); + Assert.assertFalse(content.contains("EdgeId")); + } + + @SuppressWarnings("unchecked") + private static List assertCypherSuccessData(String content) { + Map response = JsonUtil.fromJson(content, Map.class); + Assert.assertTrue(response.containsKey("requestId")); + + Map status = assertMapValue(response, "status"); + Assert.assertEquals(content, 200, ((Number) status.get("code")).intValue()); + Assert.assertEquals("", status.get("message")); + + Map result = assertMapValue(response, "result"); + Assert.assertInstanceOf(List.class, result.get("data")); + Assert.assertInstanceOf(Map.class, result.get("meta")); + return (List) result.get("data"); + } + + private static Map assertSingleMapRow(List data) { + Assert.assertEquals(1, data.size()); + Assert.assertInstanceOf(Map.class, data.get(0)); + return (Map) data.get(0); + } + + private static Map assertMapValue(Map map, String key) { + Assert.assertTrue(map.containsKey(key)); + Assert.assertInstanceOf(Map.class, map.get(key)); + return (Map) map.get(key); + } + + private static Map assertMapValue(List list, int index) { + Assert.assertTrue(list.size() > index); + Assert.assertInstanceOf(Map.class, list.get(index)); + return (Map) list.get(index); + } + + private static List assertListValue(Map map, String key) { + Assert.assertTrue(map.containsKey(key)); + Assert.assertInstanceOf(List.class, map.get(key)); + return (List) map.get(key); + } + + private static void assertPrimitiveValue(Object value) { + Assert.assertFalse(value instanceof Map); + Assert.assertFalse(value instanceof List); } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java index 0b6df93438..de01f7538e 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java @@ -17,7 +17,9 @@ package org.apache.hugegraph.api; +import java.nio.charset.StandardCharsets; import java.nio.file.Paths; +import java.util.Base64; import java.util.Map; import org.apache.hugegraph.testutil.Assert; @@ -136,6 +138,63 @@ public void testVerify() { assertResponseStatus(401, r); } + @Test + public void testBasicAuthWithNonAsciiOrColonPassword() { + // RFC 7617: the credential is UTF-8 and only the first colon separates the fields + String[][] users = {{"utf8user", "ädminpass1"}, + {"coloned", "new:pass1234"}}; + RestClient noAuthClient = new RestClient(baseUrl(), false); + try { + for (String[] user : users) { + Response r = this.createUser(user[0], user[1]); + String result = assertResponseStatus(201, r); + Map created = JsonUtil.fromJson( + result, new TypeReference>() {}); + String id = (String) created.get("id"); + try { + r = basicAuthGet(noAuthClient, user[0] + ":" + user[1]); + assertResponseStatus(200, r); + + r = basicAuthGet(noAuthClient, user[0] + ":wrong" + user[1]); + assertResponseStatus(401, r); + } finally { + assertResponseStatus(204, this.deleteUser(id)); + } + } + } finally { + noAuthClient.close(); + } + } + + @Test + public void testBasicAuthWithMalformedCredential() { + RestClient noAuthClient = new RestClient(baseUrl(), false); + try { + assertResponseStatus(200, basicAuthGet(noAuthClient, "test:test")); + + // No colon, an empty user-id or an empty password + for (String credential : new String[]{"admin", "admin:", ":pa"}) { + assertResponseStatus(400, basicAuthGet(noAuthClient, credential)); + } + + // Only the first colon separates the fields, so a trailing colon belongs to + // the password + assertResponseStatus(401, basicAuthGet(noAuthClient, "test:test:")); + assertResponseStatus(401, basicAuthGet(noAuthClient, "test::")); + } finally { + noAuthClient.close(); + } + } + + private static Response basicAuthGet(RestClient client, String userPass) { + // Built by hand: Jersey's HttpAuthenticationFeature encodes the credential as ISO-8859-1 + byte[] credential = userPass.getBytes(StandardCharsets.UTF_8); + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.AUTHORIZATION, + "Basic " + Base64.getEncoder().encodeToString(credential)); + return client.get("graphspaces/DEFAULT/graphs", headers); + } + private Response createUser(String name, String password) { String user = "{\"user_name\":\"%s\",\"user_password\":\"%s" + "\",\"user_email\":\"user1@baidu.com\"," + diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/MetricsApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/MetricsApiTest.java index d0a1775a16..170b198784 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/MetricsApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/MetricsApiTest.java @@ -18,7 +18,9 @@ package org.apache.hugegraph.api; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.apache.hugegraph.testutil.Assert; import org.junit.Test; @@ -29,6 +31,8 @@ public class MetricsApiTest extends BaseApiTest { private static final String PATH = "/metrics"; private static final String STATISTICS_PATH = PATH + "/statistics"; + private static final String GREMLIN_CHANNEL_METRICS_PREFIX = + "org_apache_tinkerpop_gremlin_server_GremlinServer_channels_"; @Test public void testBaseMetricsAll() { @@ -46,7 +50,21 @@ public void testBaseMetricsAll() { @Test public void testBaseMetricsPromAll() { Response r = client().get(PATH); - assertResponseStatus(200, r); + String result = assertResponseStatus(200, r); + Assert.assertContains(GREMLIN_CHANNEL_METRICS_PREFIX + "paused", + result); + Assert.assertContains(GREMLIN_CHANNEL_METRICS_PREFIX + "total", + result); + Assert.assertContains(GREMLIN_CHANNEL_METRICS_PREFIX + + "write_pauses", result); + + Set typed = new HashSet<>(); + for (String line : result.split("\n")) { + if (line.startsWith("# TYPE ")) { + String name = line.substring(7).split(" ")[0]; + Assert.assertTrue(line, typed.add(name)); + } + } } @Test @@ -60,7 +78,12 @@ public void testStatisticsMetricsAll() { @Test public void testStatisticsMetricsPromAll() { Response r = client().get(STATISTICS_PATH); - assertResponseStatus(200, r); + String result = assertResponseStatus(200, r); + for (String line : result.split("\n")) { + if (!line.startsWith("#") && line.contains("{")) { + Assert.assertTrue(line, line.matches("\\S+\\{\\w+=\"[^\"]*\",} \\S+")); + } + } } @Test diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/arthas/ArthasConfigTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/arthas/ArthasConfigTest.java new file mode 100644 index 0000000000..685086e848 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/arthas/ArthasConfigTest.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.hugegraph.api.arthas; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.hugegraph.api.filter.AuthenticationFilter.Authorizer; +import org.apache.hugegraph.auth.HugeAuthenticator.User; +import org.apache.hugegraph.auth.HugePermission; +import org.apache.hugegraph.auth.RolePermission; +import org.apache.hugegraph.config.ConfigException; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.OptionSpace; +import org.apache.hugegraph.config.ServerOptions; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.testutil.Assert; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; + +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.SecurityContext; +import jakarta.ws.rs.core.UriInfo; + +public class ArthasConfigTest { + + private final List events = new ArrayList<>(); + private Logger logger; + private Level previousLevel; + private Appender appender; + + @Before + public void captureWarnings() { + this.appender = Mockito.mock(Appender.class); + Mockito.when(this.appender.getName()).thenReturn("ArthasConfigTestAppender"); + Mockito.when(this.appender.isStarted()).thenReturn(true); + Mockito.doAnswer(invocation -> { + LogEvent event = invocation.getArgument(0); + this.events.add(event.toImmutable()); + return null; + }).when(this.appender).append(Mockito.any(LogEvent.class)); + this.logger = (Logger) LogManager.getLogger(ArthasAPI.class); + this.previousLevel = this.logger.getLevel(); + this.logger.setLevel(Level.WARN); + this.logger.addAppender(this.appender); + } + + @After + public void restoreLogger() { + this.logger.removeAppender(this.appender); + this.logger.setLevel(this.previousLevel); + } + + @BeforeClass + public static void registerOptions() { + OptionSpace.register("server", ServerOptions.instance()); + } + + @Test + public void testDefaults() { + Map config = ArthasAPI.arthasConfig(new HugeConfig(Collections.emptyMap())); + assertOptions(config, "8562", "8561", ""); + Assert.assertEquals("127.0.0.1", config.get("arthas.ip")); + Assert.assertTrue(this.events.isEmpty()); + } + + @Test + public void testDistributionKeys() { + Map properties = properties("telnet_port", "http_port", "disabled_commands"); + properties.put("arthas.ip", "127.0.0.1"); + Map config = ArthasAPI.arthasConfig(new HugeConfig(properties)); + assertOptions(config, "18562", "18561", "jad,stop"); + Assert.assertEquals("127.0.0.1", config.get("arthas.ip")); + Assert.assertTrue(this.events.isEmpty()); + } + + @Test + public void testLegacyKeys() { + Map properties = properties("telnetPort", "httpPort", "disabledCommands"); + assertOptions(ArthasAPI.arthasConfig(new HugeConfig(properties)), "18562", "18561", "jad,stop"); + Assert.assertTrue(this.events.isEmpty()); + } + + @Test + public void testCanonicalKeysTakePrecedence() { + Map properties = properties("telnet_port", "http_port", "disabled_commands"); + properties.put("arthas.telnetPort", "28562"); + properties.put("arthas.httpPort", "28561"); + properties.put("arthas.disabledCommands", "stop"); + assertOptions(ArthasAPI.arthasConfig(new HugeConfig(properties)), "18562", "18561", "jad,stop"); + Assert.assertEquals(3, this.events.size()); + assertConflictWarning(0, "telnet_port", "telnetPort"); + assertConflictWarning(1, "http_port", "httpPort"); + assertConflictWarning(2, "disabled_commands", "disabledCommands"); + } + + @Test + public void testExplicitCanonicalDefaultsTakePrecedence() { + Map properties = properties("telnetPort", "httpPort", "disabledCommands"); + properties.put("arthas.telnet_port", "8562"); + properties.put("arthas.http_port", "8561"); + properties.put("arthas.disabled_commands", ""); + assertOptions(ArthasAPI.arthasConfig(new HugeConfig(properties)), "8562", "8561", ""); + Assert.assertEquals(3, this.events.size()); + assertConflictWarning(2, "disabled_commands", "disabledCommands"); + } + + @Test + public void testMatchingCanonicalAndLegacyKeysDoNotWarn() { + Map properties = properties("telnet_port", "http_port", "disabled_commands"); + properties.putAll(properties("telnetPort", "httpPort", "disabledCommands")); + assertOptions(ArthasAPI.arthasConfig(new HugeConfig(properties)), "18562", "18561", "jad,stop"); + Assert.assertTrue(this.events.isEmpty()); + } + + @Test + public void testCanonicalAndLegacyPortsAndIpRejectEmptyValues() { + for (String key : new String[]{"telnet_port", "telnetPort", "http_port", "httpPort", "ip"}) { + Assert.assertThrows(ConfigException.class, + () -> new HugeConfig(Collections.singletonMap("arthas." + key, ""))); + } + } + + @Test + public void testCanonicalAndLegacyDisabledCommandsCanBeEmpty() { + for (String key : new String[]{"disabled_commands", "disabledCommands"}) { + Map config = ArthasAPI.arthasConfig( + new HugeConfig(Collections.singletonMap("arthas." + key, ""))); + assertOptions(config, "8562", "8561", ""); + } + } + + @Test + public void testExplicitBindAddresses() { + for (String ip : new String[]{"127.0.0.1", "127.0.0.2", "::1", "localhost", + "0.0.0.0", "::", "192.0.2.1", "2001:db8::1", "fe80::1%1"}) { + Map config = ArthasAPI.arthasConfig( + new HugeConfig(Collections.singletonMap("arthas.ip", ip))); + Assert.assertEquals("localhost".equals(ip) ? "127.0.0.1" : ip, config.get("arthas.ip")); + } + } + + @Test + public void testInvalidBindAddressesAreRejected() { + for (String ip : new String[]{"2001:db8::xyz", "2001:db8:::1", "[::1", "0.0.0.0:3658"}) { + HugeConfig config = new HugeConfig(Collections.singletonMap("arthas.ip", ip)); + Assert.assertThrows(IllegalArgumentException.class, () -> ArthasAPI.arthasConfig(config)); + } + } + + @Test + public void testNoAuthCannotActivateWithAnonymousAdminRole() { + GraphManager manager = Mockito.mock(GraphManager.class); + SecurityContext context = authorizer(manager, User.ANONYMOUS); + Assert.assertTrue(context.isUserInRole("admin")); + Throwable failure = Assert.assertThrows(ForbiddenException.class, + () -> new ArthasAPI().startArthas(manager, context)); + Assert.assertTrue(failure.getMessage().contains("bin/arthas.sh")); + } + + @Test + public void testAuthenticatedNonAdminCannotActivate() { + GraphManager manager = Mockito.mock(GraphManager.class); + Mockito.when(manager.requireAuthentication()).thenReturn(true); + User user = new User("graph_reader", RolePermission.role("DEFAULT", "hugegraph", HugePermission.READ)); + SecurityContext context = authorizer(manager, user); + Assert.assertThrows(ForbiddenException.class, + () -> new ArthasAPI().startArthas(manager, context)); + } + + @Test + public void testAuthenticatedAdminIsAllowed() { + GraphManager manager = Mockito.mock(GraphManager.class); + Mockito.when(manager.requireAuthentication()).thenReturn(true); + ArthasAPI.checkAccess(manager, authorizer(manager, User.ADMIN)); + } + + @Test + public void testAnonymousOrMissingPrincipalIsRejectedEvenInAuthMode() { + GraphManager manager = Mockito.mock(GraphManager.class); + Mockito.when(manager.requireAuthentication()).thenReturn(true); + Assert.assertThrows(ForbiddenException.class, + () -> new ArthasAPI().startArthas(manager, authorizer(manager, User.ANONYMOUS))); + Assert.assertThrows(ForbiddenException.class, + () -> new ArthasAPI().startArthas(manager, Mockito.mock(SecurityContext.class))); + } + + private static SecurityContext authorizer(GraphManager manager, User user) { + UriInfo uri = Mockito.mock(UriInfo.class); + Mockito.when(uri.getPathParameters()).thenReturn(new MultivaluedHashMap<>()); + return new Authorizer(manager, user, uri); + } + + private void assertConflictWarning(int index, String canonical, String legacy) { + LogEvent event = this.events.get(index); + Assert.assertEquals(Level.WARN, event.getLevel()); + Assert.assertEquals(String.format("Conflicting Arthas options 'arthas.%s' and 'arthas.%s': " + + "'arthas.%s' takes precedence", canonical, legacy, canonical), + event.getMessage().getFormattedMessage()); + } + + private static Map properties(String telnet, String http, String commands) { + Map properties = new HashMap<>(); + properties.put("arthas." + telnet, "18562"); + properties.put("arthas." + http, "18561"); + properties.put("arthas." + commands, "jad,stop"); + return properties; + } + + private static void assertOptions(Map config, String telnet, String http, + String commands) { + Assert.assertEquals(4, config.size()); + Assert.assertEquals(telnet, config.get("arthas.telnetPort")); + Assert.assertEquals(http, config.get("arthas.httpPort")); + Assert.assertEquals(commands, config.get("arthas.disabledCommands")); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java index 6333dbb44a..ca7ec157aa 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/auth/GraphSpaceAuthPayloadTest.java @@ -27,7 +27,7 @@ import org.apache.hugegraph.auth.HugeGroup; import org.apache.hugegraph.auth.HugePermission; import org.apache.hugegraph.auth.HugeTarget; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.testutil.Assert; import org.junit.Test; import org.mockito.Mockito; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java new file mode 100644 index 0000000000..d95b754e7d --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.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.hugegraph.api.cypher; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.configuration2.Configuration; +import org.apache.commons.configuration2.YAMLConfiguration; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.unit.BaseUnitTest; +import org.apache.tinkerpop.gremlin.process.traversal.Path; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.MutablePath; +import org.apache.tinkerpop.gremlin.util.Tokens; +import org.apache.tinkerpop.gremlin.util.message.RequestMessage; +import org.junit.Test; +import org.mockito.Mockito; + +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.core.HttpHeaders; + +public class CypherClientTest extends BaseUnitTest { + + @Test + public void testAnonymousClientDoesNotInheritCredentials() { + YAMLConfiguration source = new YAMLConfiguration(); + source.addProperty("hosts", "localhost"); + source.addProperty("port", 8182); + source.addProperty("username", "service-user"); + source.addProperty("password", "service-secret"); + CypherManager manager = CypherManager.configOf("unused.yaml"); + Whitebox.setInternalState(manager, "configuration", source); + + Configuration config = clientConfig(manager.getAnonymousClient()); + + Assert.assertFalse(config.containsKey("username")); + Assert.assertFalse(config.containsKey("password")); + Assert.assertEquals(8182, config.getInt("port")); + Assert.assertEquals("service-user", source.getString("username")); + Assert.assertEquals("service-secret", source.getString("password")); + } + + @Test + public void testAnonymousServerDoesNotRequireAuthorization() { + Configuration config = clientConfig(apiClient(false, null)); + + Assert.assertFalse(config.containsKey("username")); + Assert.assertFalse(config.containsKey("password")); + } + + @Test + public void testAuthenticatedServerRejectsMissingAuthorization() { + Assert.assertThrows(NotAuthorizedException.class, + () -> apiClient(true, null)); + } + + @Test + public void testAuthenticatedServerRejectsUnsupportedAuthorization() { + Assert.assertThrows(NotAuthorizedException.class, + () -> apiClient(true, "Unsupported credential")); + } + + @Test + public void testAuthenticatedServerPreservesBasicCredentials() { + Configuration config = clientConfig(apiClient(true, "Basic dXNlcjpwYXNz")); + + Assert.assertEquals("user", config.getString("username")); + Assert.assertEquals("pass", config.getString("password")); + } + + @Test + public void testAuthenticatedServerPreservesBearerCredentials() { + Configuration config = clientConfig(apiClient(true, "Bearer test-token")); + + Assert.assertEquals("test-token", config.getString("username")); + Assert.assertEquals("", config.getString("password")); + } + + @Test + public void testAuthenticatedServerRejectsMalformedCredentials() { + Assert.assertThrows(RuntimeException.class, + () -> apiClient(true, "Basic invalid-base64")); + Assert.assertThrows(IllegalArgumentException.class, + () -> apiClient(true, "Bearer ")); + } + + @Test + public void testRequestPreservesQueryAndParameterValues() { + String query = "MATCH (n:person) WHERE n.name = $name RETURN n.name"; + Map parameters = new LinkedHashMap<>(); + parameters.put("name", "quote'\"\nRETURN 999 //"); + parameters.put("age", 29); + parameters.put("active", true); + + RequestMessage request = CypherClient.createRequest(query, parameters); + + Assert.assertEquals("cypher", request.getProcessor()); + Assert.assertEquals(query, request.getArgs().get(Tokens.ARGS_GREMLIN)); + Assert.assertEquals(parameters, request.getArgs().get(Tokens.ARGS_BINDINGS)); + } + + @Test + public void testRequestAcceptsEmptyParameters() { + RequestMessage request = CypherClient.createRequest("RETURN 1", Collections.emptyMap()); + + Assert.assertEquals(Collections.emptyMap(), request.getArgs().get(Tokens.ARGS_BINDINGS)); + } + + + @Test + public void testNormalizeHandlesNullMapAndArrayValues() { + Map value = new LinkedHashMap<>(); + value.put(IdGenerator.of(1L), + new Object[]{IdGenerator.of("marko"), null}); + + Object normalized = CypherClient.normalize(value); + + Assert.assertInstanceOf(Map.class, normalized); + Map map = (Map) normalized; + Assert.assertTrue(map.containsKey(1L)); + Assert.assertInstanceOf(List.class, map.get(1L)); + + List values = (List) map.get(1L); + Assert.assertEquals("marko", values.get(0)); + Assert.assertNull(values.get(1)); + } + + @Test + public void testNormalizeHandlesCyclicReferences() { + Map value = new LinkedHashMap<>(); + value.put("private-value", value); + + Assert.assertThrows(IllegalArgumentException.class, + () -> CypherClient.normalize(value), e -> { + Assert.assertContains("cyclic Cypher result", e.getMessage()); + Assert.assertFalse(e.getMessage().contains("private-value")); + }); + } + + @Test + public void testNormalizePreservesThirtyTwoContainerLayers() { + Object value = "leaf"; + for (int i = 0; i < 32; i++) { + value = new Object[]{value}; + } + + Object normalized = CypherClient.normalize(value); + Object current = normalized; + for (int i = 0; i < 32; i++) { + Assert.assertInstanceOf(List.class, current); + List list = (List) current; + Assert.assertEquals(1, list.size()); + current = list.get(0); + } + + Assert.assertEquals("leaf", current); + } + + @Test + public void testNormalizeRejectsThirtyThirdContainerLayer() { + Object value = "leaf"; + for (int i = 0; i < 33; i++) { + value = new Object[]{value}; + } + Object deeplyNestedValue = value; + + Assert.assertThrows(IllegalArgumentException.class, + () -> CypherClient.normalize(deeplyNestedValue), + e -> Assert.assertContains( + "max normalization depth 32", + e.getMessage())); + } + + @Test + public void testNormalizePreservesPathLabelsAndObjects() { + Path path = MutablePath.make() + .extend(IdGenerator.of("marko"), + Set.of("a")) + .extend(IdGenerator.of("lop"), + Set.of("b", "software")); + + Object normalized = CypherClient.normalize(path); + + Assert.assertInstanceOf(Map.class, normalized); + Map map = (Map) normalized; + Assert.assertTrue(map.containsKey("labels")); + Assert.assertTrue(map.containsKey("objects")); + + Assert.assertInstanceOf(List.class, map.get("labels")); + Assert.assertInstanceOf(List.class, map.get("objects")); + + List labels = (List) map.get("labels"); + List objects = (List) map.get("objects"); + Assert.assertEquals(2, labels.size()); + Assert.assertEquals(2, objects.size()); + + Assert.assertEquals("marko", objects.get(0)); + Assert.assertEquals("lop", objects.get(1)); + List firstLabels = (List) labels.get(0); + List secondLabels = (List) labels.get(1); + Assert.assertTrue(firstLabels.contains("a")); + Assert.assertTrue(secondLabels.contains("b")); + Assert.assertTrue(secondLabels.contains("software")); + } + + private static CypherClient apiClient(boolean authentication, String authorization) { + GraphManager graphManager = Mockito.mock(GraphManager.class); + Mockito.when(graphManager.requireAuthentication()).thenReturn(authentication); + HttpHeaders headers = Mockito.mock(HttpHeaders.class); + Mockito.when(headers.getHeaderString(HttpHeaders.AUTHORIZATION)).thenReturn(authorization); + CypherManager manager = CypherManager.configOf("unused.yaml"); + Whitebox.setInternalState(manager, "configuration", new YAMLConfiguration()); + CypherAPI api = new CypherAPI(); + Whitebox.setInternalState(api, "cypherManager", manager); + return Whitebox.invoke(CypherAPI.class, + new Class[]{GraphManager.class, HttpHeaders.class}, + "client", api, graphManager, headers); + } + + private static Configuration clientConfig(CypherClient client) { + return Whitebox.invoke(CypherClient.class, "getConfig", client); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/traversers/SameNeighborsBatchApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/traversers/SameNeighborsBatchApiTest.java new file mode 100644 index 0000000000..ac6a137641 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/traversers/SameNeighborsBatchApiTest.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.hugegraph.api.traversers; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import org.apache.hugegraph.api.BaseApiTest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import jakarta.ws.rs.core.Response; + +public class SameNeighborsBatchApiTest extends BaseApiTest { + + private static final String PATH = TRAVERSERS_API + "/sameneighborsbatch"; + + @Before + public void prepareSchema() { + BaseApiTest.initPropertyKey(); + BaseApiTest.initVertexLabel(); + BaseApiTest.initEdgeLabel(); + BaseApiTest.initVertex(); + BaseApiTest.initEdge(); + } + + @Test + public void testPairOrderAndMeasures() { + Map ids = listAllVertexName2Ids(); + String body = String.format("{\"vertex_list\":[[\"%s\",\"%s\"],[\"%s\",\"%s\"]]}", + ids.get("marko"), ids.get("josh"), + ids.get("marko"), ids.get("peter")); + try (Response response = client().post(PATH, body)) { + String content = assertResponseStatus(200, response); + List> results = assertJsonContains(content, "same_neighbors"); + Assert.assertEquals(2, results.size()); + Assert.assertEquals(1, results.get(0).size()); + Assert.assertTrue(results.get(0).contains(ids.get("peter"))); + Assert.assertEquals(1, results.get(1).size()); + Assert.assertTrue(results.get(1).contains(ids.get("ripple"))); + Map measures = assertJsonContains(content, "measure"); + Assert.assertEquals(4L, ((Number) measures.get("vertice_iterations")).longValue()); + Assert.assertEquals(9L, ((Number) measures.get("edge_iterations")).longValue()); + } + } + + @Test + public void testPerPairLimitAndDuplicates() { + String marko = listAllVertexName2Ids().get("marko"); + for (int limit : new int[]{1, 2, -1}) { + String body = String.format("{\"vertex_list\":[[\"%s\",\"%s\"],[\"%s\",\"%s\"]]," + + "\"limit\":%s}", marko, marko, marko, marko, limit); + try (Response response = client().post(PATH, body)) { + String content = assertResponseStatus(200, response); + List> results = assertJsonContains(content, "same_neighbors"); + Assert.assertEquals(2, results.size()); + Assert.assertEquals(limit == 1 ? 1 : 2, results.get(0).size()); + Assert.assertEquals(new HashSet<>(results.get(0)), new HashSet<>(results.get(1))); + } + } + } + + @Test + public void testLabelAndDirection() { + Map ids = listAllVertexName2Ids(); + String body = String.format("{\"vertex_list\":[[\"%s\",\"%s\"]]," + + "\"direction\":\"OUT\",\"label\":\"created\"}", + ids.get("marko"), ids.get("peter")); + try (Response response = client().post(PATH, body)) { + String content = assertResponseStatus(200, response); + List> results = assertJsonContains(content, "same_neighbors"); + Assert.assertEquals(1, results.size()); + Assert.assertEquals(1, results.get(0).size()); + Assert.assertTrue(results.get(0).contains(ids.get("ripple"))); + } + body = String.format("{\"vertex_list\":[[\"%s\",\"%s\"]]," + + "\"direction\":\"IN\",\"label\":\"created\"}", + ids.get("marko"), ids.get("peter")); + try (Response response = client().post(PATH, body)) { + String content = assertResponseStatus(200, response); + List> results = assertJsonContains(content, "same_neighbors"); + Assert.assertEquals(1, results.size()); + Assert.assertTrue(results.get(0).isEmpty()); + } + } + + @Test + public void testMaximumLimit() { + String marko = listAllVertexName2Ids().get("marko"); + String body = String.format("{\"vertex_list\":[[\"%s\",\"%s\"]],\"limit\":2147483647}", + marko, marko); + try (Response response = client().post(PATH, body)) { + String content = assertResponseStatus(200, response); + List results = assertJsonContains(content, "same_neighbors"); + Assert.assertEquals(1, results.size()); + } + } + + @Test + public void testInvalidNumericIds() { + for (String id : new String[]{"1.0", "1.5", "9007199254740993.0", + "9223372036854775808", "-9223372036854775809"}) { + String body = "{\"vertex_list\":[[" + id + ",1]]}"; + try (Response response = client().post(PATH, body)) { + String content = assertResponseStatus(400, response); + Assert.assertTrue(content, content.contains( + "Numeric vertex ids must be whole numbers in the long range")); + } + } + } + + @Test + public void testInvalidInput() { + String marko = listAllVertexName2Ids().get("marko"); + String validPair = String.format("[[\"%s\",\"%s\"]]", marko, marko); + String[] bodies = { + "null", "{}", "{\"vertex_list\":null}", "{\"vertex_list\":[]}", + "{\"vertex_list\":[null]}", "{\"vertex_list\":[[]]}", + "{\"vertex_list\":[[1]]}", "{\"vertex_list\":[[1,2,3]]}", + "{\"vertex_list\":[[null,1]]}", "{\"vertex_list\":[[true,1]]}", + "{\"vertex_list\":[[{},1]]}", "{\"vertex_list\":[[[],1]]}", + "{\"vertex_list\":[[\"missing-a\",\"missing-b\"]]}", + "{\"vertex_list\":" + validPair + ",\"limit\":0}", + "{\"vertex_list\":" + validPair + ",\"limit\":-2}", + "{\"vertex_list\":" + validPair + ",\"limit\":2147483648}", + "{\"vertex_list\":" + validPair + ",\"max_degree\":0}", + "{\"vertex_list\":" + validPair + ",\"max_degree\":-2}", + "{\"vertex_list\":" + validPair + ",\"direction\":\"invalid\"}" + }; + for (String body : bodies) { + try (Response response = client().post(PATH, body)) { + assertResponseStatus(400, response); + } + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/traversers/TraversersApiTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/traversers/TraversersApiTestSuite.java index 8146375c1d..e985caf58d 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/traversers/TraversersApiTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/traversers/TraversersApiTestSuite.java @@ -38,6 +38,7 @@ RaysApiTest.class, RingsApiTest.class, SameNeighborsApiTest.class, + SameNeighborsBatchApiTest.class, ShortestPathApiTest.class, SingleSourceShortestPathApiTest.class, TemplatePathsApiTest.class, diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/BackendLeaseCleanupTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/BackendLeaseCleanupTest.java new file mode 100644 index 0000000000..222b48a8c6 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/BackendLeaseCleanupTest.java @@ -0,0 +1,372 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.auth; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +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.function.Consumer; + +import org.apache.hugegraph.HugeFactory; +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.auth.HugeGraphAuthProxy.Context; +import org.apache.hugegraph.auth.HugeGraphAuthProxy.ContextTask; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBStore; +import org.apache.hugegraph.core.BaseCoreTest; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.traversal.algorithm.OltpTraverser; +import org.apache.hugegraph.util.Consumers; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Transaction; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +public class BackendLeaseCleanupTest extends BaseCoreTest { + + @Before + public void initSchema() { + graph().schema().vertexLabel("lifecycle").useCustomizeNumberId().create(); + HugeFactory.closeCurrentThreadTransactions(); + } + + @After + public void cleanup() { + try { + HugeFactory.closeCurrentThreadTransactions(); + } finally { + HugeGraphAuthProxy.resetContext(); + } + } + + @Test + public void testFailedTaskRollsBackDespiteCommitOnClose() { + AtomicInteger rollbackEvents = new AtomicInteger(); + HugeGraphAuthProxy.setContext(Context.admin()); + IllegalStateException failure = new IllegalStateException("task failed after writing"); + ContextTask task = new ContextTask(() -> { + Transaction tx = graph().tx(); + tx.onClose(Transaction.CLOSE_BEHAVIOR.COMMIT); + tx.onReadWrite(Transaction.READ_WRITE_BEHAVIOR.MANUAL); + tx.open(); + tx.addTransactionListener(status -> rollbackEvents.incrementAndGet()); + graph().addVertex(T.label, "lifecycle", T.id, 1L); + throw failure; + }); + HugeGraphAuthProxy.resetContext(); + + Assert.assertSame(failure, Assert.assertThrows(IllegalStateException.class, task::run)); + Assert.assertFalse(graph().tx().isOpen()); + ThreadLocal backend = Whitebox.getInternalState(graph().tx(), "transactions"); + Assert.assertNull(backend.get()); + Assert.assertNull(HugeGraphAuthProxy.getContext()); + Assert.assertEquals(1, rollbackEvents.get()); + // Reuse the worker thread: AUTO and default rollback must be restored. + Assert.assertEquals(0L, graph().traversal().V().count().next().longValue()); + graph().addVertex(T.label, "lifecycle", T.id, 2L); + graph().tx().close(); + Assert.assertEquals(0L, graph().traversal().V().count().next().longValue()); + Assert.assertEquals(1, rollbackEvents.get()); + } + + @Test + public void testSuccessfulTaskKeepsExplicitCommitAndResetsThreadState() { + AtomicInteger commitEvents = new AtomicInteger(); + HugeGraphAuthProxy.setContext(Context.admin()); + ContextTask task = new ContextTask(() -> { + Transaction tx = graph().tx(); + tx.onClose(Transaction.CLOSE_BEHAVIOR.COMMIT); + tx.onReadWrite(Transaction.READ_WRITE_BEHAVIOR.MANUAL); + tx.open(); + tx.addTransactionListener(status -> commitEvents.incrementAndGet()); + graph().addVertex(T.label, "lifecycle", T.id, 1L); + tx.commit(); + }); + HugeGraphAuthProxy.resetContext(); + + task.run(); + + Assert.assertFalse(graph().tx().isOpen()); + ThreadLocal backend = Whitebox.getInternalState(graph().tx(), "transactions"); + Assert.assertNull(backend.get()); + Assert.assertNull(HugeGraphAuthProxy.getContext()); + Assert.assertEquals(1L, graph().traversal().V().count().next().longValue()); + graph().addVertex(T.label, "lifecycle", T.id, 2L); + graph().tx().close(); + Assert.assertEquals(1L, graph().traversal().V().count().next().longValue()); + Assert.assertEquals(1, commitEvents.get()); + } + @Test + public void testCommittedRocksDBWorkerReturnsBackendLeases() throws Exception { + Assume.assumeTrue("rocksdb".equals(graph().backend())); + ExecutorService worker = Executors.newSingleThreadExecutor(); + try { + worker.submit(() -> { + try { + graph().addVertex(T.label, "lifecycle", T.id, 1L); + graph().tx().commit(); + RocksDBStore store = Whitebox.getInternalState(params().graphTransaction(), "store"); + Map databases = Whitebox.getInternalState(store, "dbs"); + long threadId = Thread.currentThread().getId(); + for (RocksDBSessions sessions : databases.values()) { + Map leases = Whitebox.getInternalState(sessions, "sessions"); + Assert.assertTrue(leases.containsKey(threadId)); + } + HugeFactory.closeCurrentThreadTransactions(); + for (RocksDBSessions sessions : databases.values()) { + Map leases = Whitebox.getInternalState(sessions, "sessions"); + Assert.assertFalse(leases.containsKey(threadId)); + } + // Reuse this exact worker and confirm the committed row survives. + Assert.assertEquals(1L, graph().traversal().V().count().next().longValue()); + } finally { + HugeFactory.closeCurrentThreadTransactions(); + } + }).get(30, TimeUnit.SECONDS); + } finally { + worker.shutdownNow(); + } + } + + @Test + public void testOltpSingleItemReturnsWorkerLeases() { + this.assertOltpWorkerCleanup(false, null); + } + + @Test + public void testOltpBatchReturnsWorkerLeases() { + this.assertOltpWorkerCleanup(true, null); + } + + @Test + public void testOltpFailureReturnsWorkerLeasesAndPreservesFailure() { + this.assertOltpWorkerCleanup(false, new IllegalStateException("worker failed")); + } + + @Test + public void testOltpStopExecutionReturnsWorkerLeases() { + this.assertOltpWorkerCleanup(true, new Consumers.StopExecution("stop traversal")); + } + + @Test + public void testOltpInlineTraversalKeepsCallerTransaction() { + Assume.assumeTrue("rocksdb".equals(graph().backend())); + graph().addVertex(T.label, "lifecycle", T.id, 99L); + Transaction caller = graph().tx(); + try (LeaseTraverser traverser = new LeaseTraverser(graph())) { + traverser.inline(() -> Assert.assertTrue(graph().vertices(99L).hasNext())); + } + Assert.assertTrue(caller.isOpen()); + ThreadLocal owners = Whitebox.getInternalState(caller, "transactions"); + Assert.assertNotNull(owners.get()); + caller.rollback(); + Assert.assertFalse(graph().vertices(99L).hasNext()); + } + + @Test + public void testLazyBfsQueryReturnsWorkerLeases() { + this.assertLazyBfsWorkerCleanup(OltpTraverser.NO_LIMIT, null); + } + + @Test + public void testLazyBfsFailureReturnsWorkerLeases() { + this.assertLazyBfsWorkerCleanup(OltpTraverser.NO_LIMIT, + new IllegalStateException("BFS consumer failed")); + } + + @Test + public void testLazyBfsCapacityStopReturnsWorkerLeases() { + this.assertLazyBfsWorkerCleanup(1L, null); + } + + private void assertLazyBfsWorkerCleanup(long capacity, RuntimeException failure) { + Assume.assumeTrue("rocksdb".equals(graph().backend())); + Id label = graph().schema().edgeLabel("lifecycle_link") + .sourceLabel("lifecycle").targetLabel("lifecycle") + .create().id(); + Vertex source = graph().addVertex(T.label, "lifecycle", T.id, 1L); + Vertex target = graph().addVertex(T.label, "lifecycle", T.id, 2L); + source.addEdge("lifecycle_link", target); + graph().tx().commit(); + HugeFactory.closeCurrentThreadTransactions(); + graph().addVertex(T.label, "lifecycle", T.id, 99L); + Transaction caller = graph().tx(); + RocksDBStore store = Whitebox.getInternalState(params().graphTransaction(), "store"); + Map databases = Whitebox.getInternalState(store, "dbs"); + AtomicReference leases = new AtomicReference<>(); + AtomicInteger visited = new AtomicInteger(); + try (LeaseTraverser traverser = new LeaseTraverser(graph())) { + Runnable run = () -> traverser.edges(label, capacity, () -> { + visited.incrementAndGet(); + WorkerLeases observed = new WorkerLeases(Thread.currentThread().getId(), + new ArrayList<>(databases.values())); + observed.assertPresent(); + leases.set(observed); + if (failure != null) { + throw failure; + } + }); + if (failure != null) { + Assert.assertSame(failure, Assert.assertThrows(IllegalStateException.class, run::run)); + } else { + run.run(); + } + if (capacity != OltpTraverser.NO_LIMIT) { + Assert.assertEquals(capacity, traverser.edgeIterCounter.get()); + Assert.assertNotNull(traverser.stop.get()); + } else { + Assert.assertNull(traverser.stop.get()); + } + } + Assert.assertEquals(1, visited.get()); + Assert.assertNotNull(leases.get()); + leases.get().assertAbsent(); + Assert.assertTrue(caller.isOpen()); + Assert.assertTrue(graph().vertices(99L).hasNext()); + caller.rollback(); + Assert.assertFalse(graph().vertices(99L).hasNext()); + } + + private void assertOltpWorkerCleanup(boolean batch, RuntimeException failure) { + Assume.assumeTrue("rocksdb".equals(graph().backend())); + // The caller's uncommitted write must survive worker and traverser close. + graph().addVertex(T.label, "lifecycle", T.id, 99L); + Transaction caller = graph().tx(); + long callerThreadId = Thread.currentThread().getId(); + AtomicReference leases = new AtomicReference<>(); + Runnable work = () -> { + Assert.assertTrue(Thread.currentThread().getId() != callerThreadId); + graph().addVertex(T.label, "lifecycle", T.id, 7L); + RocksDBStore store = Whitebox.getInternalState(params().graphTransaction(), "store"); + Map databases = Whitebox.getInternalState(store, "dbs"); + WorkerLeases observed = new WorkerLeases(Thread.currentThread().getId(), + new ArrayList<>(databases.values())); + observed.assertPresent(); + leases.set(observed); + if (failure != null) { + throw failure; + } + }; + try (LeaseTraverser traverser = new LeaseTraverser(graph())) { + Runnable run = () -> { + if (batch) { + traverser.batch(work); + } else { + traverser.one(work); + } + }; + if (failure != null && !(failure instanceof Consumers.StopExecution)) { + Assert.assertSame(failure, Assert.assertThrows(IllegalStateException.class, run::run)); + } else { + run.run(); + } + } + Assert.assertNotNull(leases.get()); + leases.get().assertAbsent(); + Assert.assertTrue(caller.isOpen()); + Assert.assertTrue(graph().vertices(99L).hasNext()); + // Worker cleanup rolls back, even if the caller commits its own write. + caller.commit(); + Assert.assertFalse(graph().vertices(7L).hasNext()); + Assert.assertTrue(graph().vertices(99L).hasNext()); + } + + private static final class WorkerLeases { + + private final long threadId; + private final List databases; + + private WorkerLeases(long threadId, List databases) { + this.threadId = threadId; + this.databases = databases; + } + + private void assertPresent() { + Assert.assertFalse(this.databases.isEmpty()); + for (RocksDBSessions sessions : this.databases) { + Map owners = Whitebox.getInternalState(sessions, "sessions"); + Assert.assertTrue(owners.containsKey(this.threadId)); + } + } + + private void assertAbsent() { + for (RocksDBSessions sessions : this.databases) { + Map owners = Whitebox.getInternalState(sessions, "sessions"); + Assert.assertFalse(owners.containsKey(this.threadId)); + } + } + } + + private static final class LeaseTraverser extends OltpTraverser { + + private final AtomicReference stop = new AtomicReference<>(); + + private LeaseTraverser(HugeGraph graph) { + super(graph); + } + + private void one(Runnable work) { + this.traverseByOne(Collections.singletonList(1).iterator(), + ignored -> work.run(), "lease-single-item"); + } + + private void batch(Runnable work) { + this.traverseByBatch(Collections.singletonList(Collections.singletonList(1).iterator()).iterator(), + ignored -> work.run(), "lease-batch", 1); + } + + private void edges(Id label, long capacity, Runnable work) { + this.traverseIdsByBfs(Collections.singletonList(IdGenerator.of(1L)).iterator(), + Directions.OUT, label, NO_LIMIT, capacity, + ignored -> work.run()); + } + + @Override + protected long traverseByBatch(Iterator> sources, + Consumer> consumer, + String taskName, int concurrentWorkers) { + return super.traverseByBatch(sources, batch -> { + try { + consumer.accept(batch); + } catch (Consumers.StopExecution stop) { + this.stop.set(stop); + throw stop; + } + }, taskName, concurrentWorkers); + } + + private void inline(Runnable work) { + this.traverseIds(Collections.singletonList(IdGenerator.of(1L)).iterator(), + ignored -> work.run(), false); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/ContextGremlinServerHttpTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/ContextGremlinServerHttpTest.java new file mode 100644 index 0000000000..03c2bea0d0 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/ContextGremlinServerHttpTest.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.auth; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.hugegraph.event.EventHub; +import org.apache.hugegraph.server.HugeGraphWsAndHttpChannelizer; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.util.JsonUtil; +import org.apache.tinkerpop.gremlin.driver.Client; +import org.apache.tinkerpop.gremlin.driver.Cluster; +import org.apache.tinkerpop.gremlin.server.Settings; +import org.apache.tinkerpop.gremlin.server.channel.WebSocketChannelizer; +import org.apache.tinkerpop.gremlin.server.channel.WsAndHttpChannelizer; +import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph; +import org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3; +import org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1; +import org.junit.Test; + +import io.netty.channel.Channel; + +public class ContextGremlinServerHttpTest { + + @Test + public void testOnlyStandardChannelizerIsAdapted() { + try { + Settings settings = new Settings(); + settings.channelizer = WsAndHttpChannelizer.class.getName(); + Assert.assertSame(settings, + ContextGremlinServer.configureChannelizer(settings)); + Assert.assertEquals(HugeGraphWsAndHttpChannelizer.class.getName(), + settings.channelizer); + settings.channelizer = "example.CustomChannelizer"; + ContextGremlinServer.configureChannelizer(settings); + Assert.assertEquals("example.CustomChannelizer", settings.channelizer); + settings.channelizer = WebSocketChannelizer.class.getName(); + ContextGremlinServer.configureChannelizer(settings); + Assert.assertEquals(WebSocketChannelizer.class.getName(), + settings.channelizer); + } finally { + HugeGraphAuthProxy.resetContext(); + } + } + + @Test + public void testUnknownAliasThenValidQueriesOnSameTcpConnection() + throws Exception { + Settings settings = new Settings(); + settings.host = "127.0.0.1"; + settings.port = 0; + settings.gremlinPool = 1; + settings.threadPoolBoss = 1; + settings.threadPoolWorker = 1; + settings.channelizer = WsAndHttpChannelizer.class.getName(); + Settings.SerializerSettings serializer = new Settings.SerializerSettings(); + serializer.className = GraphSONUntypedMessageSerializerV1.class.getName(); + Settings.SerializerSettings webSocketSerializer = new Settings.SerializerSettings(); + webSocketSerializer.className = GraphSONMessageSerializerV3.class.getName(); + settings.serializers = Arrays.asList(serializer, webSocketSerializer); + ContextGremlinServer server = new ContextGremlinServer( + settings, new EventHub("http-lifecycle-test", 1)); + try { + server.getServerGremlinExecutor().getGraphManager() + .putGraph("known", EmptyGraph.instance()); + server.start().get(30, TimeUnit.SECONDS); + Assert.assertTrue(server.getChannelizer() instanceof + HugeGraphWsAndHttpChannelizer); + Channel listening = Whitebox.getInternalState(server, + "serverSocketChannel"); + int port = ((InetSocketAddress) listening.localAddress()).getPort(); + try (Socket socket = new Socket("127.0.0.1", port)) { + socket.setSoTimeout(10000); + InputStream input = new BufferedInputStream(socket.getInputStream()); + send(socket, "missing", "101"); + Map rejected = read(input, 400); + Assert.assertTrue(rejected.get("message").toString() + .contains("Could not rebind")); + send(socket, "known", "202"); + Map accepted = read(input, 200); + Assert.assertEquals(Collections.singletonList(202), + ((Map) accepted.get("result")).get("data")); + send(socket, "known", "303"); + Map next = read(input, 200); + Assert.assertEquals(Collections.singletonList(303), + ((Map) next.get("result")).get("data")); + } + Cluster cluster = Cluster.build().addContactPoint("127.0.0.1") + .port(port) + .serializer(new GraphSONMessageSerializerV3()) + .create(); + try { + Client client = cluster.connect(); + try { + Assert.assertEquals(404, client.submit("404").all() + .get(10, TimeUnit.SECONDS) + .get(0).getInt()); + } finally { + client.close(); + } + } finally { + cluster.close(); + } + } finally { + server.stop().get(30, TimeUnit.SECONDS); + HugeGraphAuthProxy.resetContext(); + } + } + + private static void send(Socket socket, String alias, String script) + throws IOException { + String body = "{\"gremlin\":\"" + script + "\",\"language\":" + + "\"gremlin-groovy\",\"aliases\":{\"graph\":\"" + alias + "\"}}"; + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + String headers = "POST /gremlin HTTP/1.1\r\n" + + "Host: 127.0.0.1\r\n" + + "Content-Type: application/json\r\n" + + "Accept: application/json\r\n" + + "Connection: keep-alive\r\n" + + "Content-Length: " + bytes.length + "\r\n\r\n"; + socket.getOutputStream().write(headers.getBytes(StandardCharsets.US_ASCII)); + socket.getOutputStream().write(bytes); + socket.getOutputStream().flush(); + } + + private static Map read(InputStream input, int expectedStatus) + throws IOException { + String status = line(input); + Assert.assertTrue(status, status.startsWith("HTTP/1.1 " + expectedStatus + " ")); + int length = -1; + String header; + while (!(header = line(input)).isEmpty()) { + if (header.toLowerCase(Locale.ROOT).startsWith("content-length:")) { + length = Integer.parseInt(header.substring(header.indexOf(':') + 1).trim()); + } + } + Assert.assertTrue("Expected a bounded response body", length >= 0 && length < 65536); + byte[] bytes = input.readNBytes(length); + Assert.assertEquals(length, bytes.length); + return JsonUtil.fromJson(new String(bytes, StandardCharsets.UTF_8), Map.class); + } + + private static String line(InputStream input) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int value; + while ((value = input.read()) != '\n') { + if (value < 0 || bytes.size() >= 8192) { + throw new IOException("Incomplete or excessive HTTP header"); + } + if (value != '\r') { + bytes.write(value); + } + } + return bytes.toString(StandardCharsets.US_ASCII); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/ContextTaskTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/ContextTaskTest.java new file mode 100644 index 0000000000..a5ef4ad8d7 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/ContextTaskTest.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.hugegraph.auth; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.hugegraph.auth.HugeGraphAuthProxy.Context; +import org.apache.hugegraph.auth.HugeGraphAuthProxy.ContextTask; +import org.apache.hugegraph.testutil.Assert; +import org.junit.After; +import org.junit.Test; + +public class ContextTaskTest { + + @After + public void teardown() { + HugeGraphAuthProxy.resetContext(); + } + + @Test + public void testCleanupRunsBeforeContextReset() { + Context context = Context.admin(); + AtomicBoolean runnerCalled = new AtomicBoolean(false); + AtomicBoolean cleanupCalled = new AtomicBoolean(false); + HugeGraphAuthProxy.setContext(context); + + ContextTask task = new ContextTask(() -> { + Assert.assertSame(context, HugeGraphAuthProxy.getContext()); + runnerCalled.set(true); + }, () -> { + Assert.assertSame(context, HugeGraphAuthProxy.getContext()); + cleanupCalled.set(true); + }); + HugeGraphAuthProxy.resetContext(); + + task.run(); + + Assert.assertTrue(runnerCalled.get()); + Assert.assertTrue(cleanupCalled.get()); + Assert.assertNull(HugeGraphAuthProxy.getContext()); + } + + @Test + public void testCleanupRunsWhenTaskFails() { + AtomicBoolean cleanupCalled = new AtomicBoolean(false); + ContextTask task = new ContextTask(() -> { + throw new IllegalStateException("test"); + }, () -> cleanupCalled.set(true)); + + Assert.assertThrows(IllegalStateException.class, task::run); + Assert.assertTrue(cleanupCalled.get()); + Assert.assertNull(HugeGraphAuthProxy.getContext()); + } + + @Test + public void testContextResetRunsWhenCleanupFails() { + Context context = Context.admin(); + HugeGraphAuthProxy.setContext(context); + ContextTask task = new ContextTask(() -> { + Assert.assertSame(context, HugeGraphAuthProxy.getContext()); + }, () -> { + throw new IllegalStateException("test"); + }); + HugeGraphAuthProxy.resetContext(); + + task.run(); + + Assert.assertNull(HugeGraphAuthProxy.getContext()); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/HttpGremlinRequestHandlerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/HttpGremlinRequestHandlerTest.java new file mode 100644 index 0000000000..dc5446e177 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/HttpGremlinRequestHandlerTest.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.hugegraph.auth; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hugegraph.server.HttpGremlinRequestHandler; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.util.JsonUtil; +import org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutor; +import org.apache.tinkerpop.gremlin.server.GraphManager; +import org.apache.tinkerpop.gremlin.server.Settings; +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV1; +import org.junit.Test; +import org.mockito.Mockito; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; + +public class HttpGremlinRequestHandlerTest { + + @Test + public void testInvalidAliasRespondsOnceThenValidRequestWorks() + throws Exception { + try (GremlinExecutor executor = GremlinExecutor.build().create()) { + EmbeddedChannel channel = channel(executor); + try { + FullHttpRequest invalid = request("missing", "101"); + channel.writeInbound(invalid); + FullHttpResponse rejected = response(channel); + Assert.assertEquals(HttpResponseStatus.BAD_REQUEST, + rejected.status()); + rejected.release(); + Assert.assertEquals(0, invalid.refCnt()); + Assert.assertNull(channel.readOutbound()); + Assert.assertTrue(channel.isActive()); + + FullHttpRequest valid = request("known", "202"); + channel.writeInbound(valid); + FullHttpResponse accepted = response(channel); + try { + Assert.assertEquals(HttpResponseStatus.OK, + accepted.status()); + Map body = JsonUtil.fromJson( + accepted.content().toString(StandardCharsets.UTF_8), + Map.class); + Map result = (Map) body.get("result"); + Assert.assertEquals(Collections.singletonList(202), + result.get("data")); + } finally { + accepted.release(); + } + Assert.assertEquals(0, valid.refCnt()); + Assert.assertNull(channel.readOutbound()); + } finally { + channel.finishAndReleaseAll(); + } + } + } + + @Test + public void testEarlyErrorsReleaseRequestExactlyOnce() throws Exception { + try (GremlinExecutor executor = GremlinExecutor.build().create()) { + EmbeddedChannel channel = channel(executor); + try { + assertRejected(channel, requestBody("{"), + HttpResponseStatus.BAD_REQUEST); + assertRejected(channel, requestBody("{}"), + HttpResponseStatus.BAD_REQUEST); + FullHttpRequest method = request("known", "1"); + method.setMethod(HttpMethod.PUT); + assertRejected(channel, method, + HttpResponseStatus.METHOD_NOT_ALLOWED); + FullHttpRequest accept = request("known", "1"); + accept.headers().set(HttpHeaderNames.ACCEPT, + "application/unsupported"); + assertRejected(channel, accept, + HttpResponseStatus.BAD_REQUEST); + FullHttpRequest favicon = request("known", "1"); + favicon.setUri("/favicon.ico"); + assertRejected(channel, favicon, HttpResponseStatus.NOT_FOUND); + } finally { + channel.finishAndReleaseAll(); + } + } + } + + @Test + public void testConnectionCloseReleasesRequestOnSuccessAndFailure() + throws Exception { + try (GremlinExecutor executor = GremlinExecutor.build().create()) { + for (String alias : new String[]{"missing", "known"}) { + EmbeddedChannel channel = channel(executor); + FullHttpRequest request = request(alias, "202"); + request.headers().set(HttpHeaderNames.CONNECTION, + HttpHeaderValues.CLOSE); + try { + channel.writeInbound(request); + FullHttpResponse response = response(channel); + try { + Assert.assertEquals("known".equals(alias) ? + HttpResponseStatus.OK : + HttpResponseStatus.BAD_REQUEST, + response.status()); + } finally { + response.release(); + } + awaitClose(channel); + Assert.assertFalse(channel.isActive()); + Assert.assertEquals(0, request.refCnt()); + Assert.assertNull(channel.readOutbound()); + } finally { + channel.finishAndReleaseAll(); + } + } + } + } + + @Test + public void testSharedBufferRetainsCallersReference() throws Exception { + try (GremlinExecutor executor = GremlinExecutor.build().create()) { + EmbeddedChannel channel = channel(executor); + FullHttpRequest request = request("missing", "1"); + request.retain(); + try { + channel.writeInbound(request); + FullHttpResponse rejected = response(channel); + Assert.assertEquals(HttpResponseStatus.BAD_REQUEST, + rejected.status()); + rejected.release(); + Assert.assertEquals(1, request.refCnt()); + Assert.assertNull(channel.readOutbound()); + } finally { + request.release(); + channel.finishAndReleaseAll(); + } + } + } + + @Test + public void testAsyncWriteFailureDoesNotRetainRequest() throws Exception { + CountDownLatch evaluating = new CountDownLatch(1); + CountDownLatch proceed = new CountDownLatch(1); + AtomicReference attempted = new AtomicReference<>(); + try (GremlinExecutor executor = GremlinExecutor.build() + .beforeEval(bindings -> { + evaluating.countDown(); + try { + Assert.assertTrue(proceed.await(10, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + }).create()) { + EmbeddedChannel channel = channel(executor); + channel.pipeline().addFirst("failed-write", + new ChannelOutboundHandlerAdapter() { + @Override + public void write(ChannelHandlerContext context, + Object message, + ChannelPromise promise) { + FullHttpResponse response = + (FullHttpResponse) message; + attempted.set(response); + response.release(); + promise.setFailure(new IOException("test write failure")); + } + }); + FullHttpRequest request = request("known", "202"); + try { + channel.writeInbound(request); + Assert.assertEquals(0, request.refCnt()); + Assert.assertTrue(evaluating.await(10, TimeUnit.SECONDS)); + proceed.countDown(); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while (attempted.get() == null && System.nanoTime() < deadline) { + channel.runPendingTasks(); + Thread.sleep(5L); + } + Assert.assertNotNull(attempted.get()); + Assert.assertEquals(0, attempted.get().refCnt()); + Assert.assertEquals(0, request.refCnt()); + Assert.assertNull(channel.readOutbound()); + } finally { + proceed.countDown(); + channel.finishAndReleaseAll(); + } + } + } + + private static void assertRejected(EmbeddedChannel channel, + FullHttpRequest request, + HttpResponseStatus expected) + throws InterruptedException { + channel.writeInbound(request); + FullHttpResponse response = response(channel); + try { + Assert.assertEquals(expected, response.status()); + } finally { + response.release(); + } + Assert.assertEquals(0, request.refCnt()); + Assert.assertNull(channel.readOutbound()); + Assert.assertTrue(channel.isActive()); + } + + private static EmbeddedChannel channel(GremlinExecutor executor) { + GraphManager manager = Mockito.mock(GraphManager.class); + Mockito.when(manager.getGraph("known")) + .thenReturn(Mockito.mock(Graph.class)); + return new EmbeddedChannel(new HttpGremlinRequestHandler( + Collections.singletonMap("application/json", + new GraphSONUntypedMessageSerializerV1()), + executor, manager, new Settings())); + } + + private static FullHttpRequest request(String alias, String script) { + String body = "{\"gremlin\":\"" + script + "\",\"language\":" + + "\"gremlin-groovy\",\"aliases\":{\"graph\":\"" + alias + "\"}}"; + return requestBody(body); + } + + private static FullHttpRequest requestBody(String body) { + FullHttpRequest request = new DefaultFullHttpRequest( + HttpVersion.HTTP_1_1, HttpMethod.POST, "/gremlin", + Unpooled.copiedBuffer(body, StandardCharsets.UTF_8)); + request.headers().set(HttpHeaderNames.CONTENT_TYPE, + HttpHeaderValues.APPLICATION_JSON); + request.headers().set(HttpHeaderNames.ACCEPT, + HttpHeaderValues.APPLICATION_JSON); + request.headers().set(HttpHeaderNames.CONNECTION, + HttpHeaderValues.KEEP_ALIVE); + HttpUtil.setContentLength(request, request.content().readableBytes()); + return request; + } + + private static FullHttpResponse response(EmbeddedChannel channel) + throws InterruptedException { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while (System.nanoTime() < deadline) { + channel.runPendingTasks(); + FullHttpResponse response = channel.readOutbound(); + if (response != null) { + return response; + } + Thread.sleep(5L); + } + throw new AssertionError("Gremlin HTTP response did not arrive"); + } + + private static void awaitClose(EmbeddedChannel channel) + throws InterruptedException { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while (!channel.closeFuture().isDone() && System.nanoTime() < deadline) { + channel.runPendingTasks(); + Thread.sleep(5L); + } + Assert.assertTrue("Gremlin HTTP channel did not close: active=" + channel.isActive() + + ", pendingTasks=" + channel.hasPendingTasks(), + channel.closeFuture().isDone()); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2Test.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2Test.java index 1b05e4ee24..5d77bd25a0 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2Test.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/StandardAuthManagerV2Test.java @@ -20,8 +20,8 @@ import java.util.Arrays; import java.util.Collections; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.testutil.Assert; import org.junit.Test; import org.mockito.Mockito; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/TransactionLifecycleTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/TransactionLifecycleTest.java new file mode 100644 index 0000000000..bf4864b628 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/TransactionLifecycleTest.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.hugegraph.auth; + +import java.lang.reflect.Constructor; + +import org.apache.hugegraph.HugeFactory; +import org.apache.hugegraph.HugeGraphParams; +import org.apache.hugegraph.backend.store.BackendStore; +import org.apache.hugegraph.backend.tx.GraphTransaction; +import org.apache.hugegraph.backend.tx.ISchemaTransaction; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.core.BaseCoreTest; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.traversal.algorithm.KneighborTraverser; +import org.apache.hugegraph.type.define.Directions; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Transaction; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class TransactionLifecycleTest extends BaseCoreTest { + + @Before + public void initSchema() { + graph().schema().vertexLabel("lifecycle").useCustomizeNumberId().create(); + HugeFactory.closeCurrentThreadTransactions(); + } + + @After + public void cleanup() { + try { + HugeFactory.closeCurrentThreadTransactions(); + } finally { + HugeGraphAuthProxy.resetContext(); + } + } + + @Test + public void testTraverserClosePreservesCallerWrites() { + graph().addVertex(T.label, "lifecycle", T.id, 1L); + graph().tx().commit(); + + try (KneighborTraverser traverser = new KneighborTraverser(graph())) { + Assert.assertTrue(traverser.kneighbor(IdGenerator.of(1L), Directions.BOTH, + null, 1, 100L, 100L).isEmpty()); + graph().addVertex(T.label, "lifecycle", T.id, 2L); + } + + Assert.assertTrue(graph().tx().isOpen()); + graph().tx().commit(); + HugeFactory.closeCurrentThreadTransactions(); + Assert.assertEquals(2L, graph().traversal().V().count().next().longValue()); + } + + @Test + public void testFailedCleanupReleasesOwnersBeforeWorkerReuse() throws Exception { + this.checkFailedCleanupReleasesOwnersBeforeWorkerReuse(false); + } + + @Test + public void testFailedRollbackPreservesCauseAndReleasesOwnersBeforeWorkerReuse() throws Exception { + this.checkFailedCleanupReleasesOwnersBeforeWorkerReuse(true); + } + + private void checkFailedCleanupReleasesOwnersBeforeWorkerReuse(boolean failRollback) throws Exception { + Class holderType = Class.forName("org.apache.hugegraph.StandardHugeGraph$Txs"); + Class systemType = Class.forName("org.apache.hugegraph.StandardHugeGraph$SysTransaction"); + GraphTransaction graphTx = Mockito.mock(GraphTransaction.class); + BackendStore systemStore = Mockito.mock(BackendStore.class); + Constructor systemConstructor = systemType.getDeclaredConstructor( + HugeGraphParams.class, BackendStore.class); + systemConstructor.setAccessible(true); + GraphTransaction systemTx = + (GraphTransaction) systemConstructor.newInstance(this.params(), systemStore); + ISchemaTransaction schemaTx = Mockito.mock(ISchemaTransaction.class); + RuntimeException first = new IllegalStateException("graph close failed"); + RuntimeException second = new IllegalArgumentException("system close failed"); + RuntimeException rollbackFailure = new IllegalStateException("rollback failed"); + if (failRollback) { + Mockito.doThrow(rollbackFailure).when(graphTx).rollback(); + } + Mockito.doThrow(first).when(graphTx).close(); + Mockito.doThrow(second).when(systemStore).close(); + Constructor constructor = holderType.getDeclaredConstructor( + ISchemaTransaction.class, systemType, GraphTransaction.class); + constructor.setAccessible(true); + Object failedOwners = constructor.newInstance(schemaTx, systemTx, graphTx); + ThreadLocal owners = Whitebox.getInternalState(graph().tx(), "transactions"); + Assert.assertNull(owners.get()); + owners.set(failedOwners); + + // Dirty request behavior must also be reset when backend close throws. + graph().tx().onReadWrite(Transaction.READ_WRITE_BEHAVIOR.MANUAL); + graph().tx().onClose(Transaction.CLOSE_BEHAVIOR.COMMIT); + if (failRollback) { + graph().tx().open(); + } + Throwable failure = Assert.assertThrows( + HugeException.class, HugeFactory::closeCurrentThreadTransactions); + if (failRollback) { + Assert.assertSame(rollbackFailure, failure.getCause()); + Assert.assertEquals(1, rollbackFailure.getSuppressed().length); + Assert.assertSame(first, rollbackFailure.getSuppressed()[0]); + } else { + Assert.assertSame(first, failure.getCause()); + } + Assert.assertEquals(1, first.getSuppressed().length); + Assert.assertSame(second, first.getSuppressed()[0]); + Mockito.verify(graphTx).close(); + // SysTransaction owns both its primary transaction and index transaction. + Mockito.verify(systemStore, Mockito.times(2)).close(); + Mockito.verify(schemaTx).close(); + Assert.assertNull(owners.get()); + Assert.assertFalse(graph().tx().isOpen()); + + // The same thread must acquire fresh owners and retain default AUTO/ROLLBACK. + graph().addVertex(T.label, "lifecycle", T.id, 1L); + Assert.assertNotNull(owners.get()); + Assert.assertNotSame(failedOwners, owners.get()); + graph().tx().close(); + Assert.assertEquals(0L, graph().traversal().V().count().next().longValue()); + graph().addVertex(T.label, "lifecycle", T.id, 2L); + graph().tx().commit(); + HugeFactory.closeCurrentThreadTransactions(); + Assert.assertNull(owners.get()); + Assert.assertEquals(1L, graph().traversal().V().count().next().longValue()); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/page/QueryListTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/page/QueryListTest.java index 7fc020889b..7d49c15dc9 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/page/QueryListTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/page/QueryListTest.java @@ -24,13 +24,13 @@ import java.util.List; import java.util.Set; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.page.IdHolder.BatchIdHolder; import org.apache.hugegraph.backend.page.IdHolder.FixedIdHolder; import org.apache.hugegraph.backend.page.IdHolder.PagingIdHolder; -import org.apache.hugegraph.backend.query.ConditionQuery.OptimizedType; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.query.ConditionQuery.OptimizedType; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.query.QueryBatch; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.serializer.TextBackendEntry; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBIteratorPoolTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBIteratorPoolTest.java new file mode 100644 index 0000000000..3f65597852 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBIteratorPoolTest.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.hugegraph.backend.store.rocksdb; + +import java.nio.charset.StandardCharsets; + +import org.apache.hugegraph.backend.store.rocksdb.RocksDBIteratorPool.ReusedRocksIterator; +import org.apache.hugegraph.testutil.Assert; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksIterator; + +public class RocksDBIteratorPoolTest { + + private static final String CF = "iter-pool-cf"; + + @Rule + public TemporaryFolder temporary = new TemporaryFolder(); + + private Options options; + private RocksDB rocksdb; + private ColumnFamilyHandle cfh; + private RocksDBIteratorPool pool; + + @Before + public void setup() throws Exception { + this.options = new Options().setCreateIfMissing(true); + this.rocksdb = RocksDB.open(this.options, this.temporary.newFolder().getAbsolutePath()); + this.cfh = this.rocksdb.createColumnFamily(new ColumnFamilyDescriptor(bytes(CF))); + this.rocksdb.put(this.cfh, bytes("k1"), bytes("v1")); + this.rocksdb.put(this.cfh, bytes("k2"), bytes("v2")); + this.pool = new RocksDBIteratorPool(this.rocksdb, this.cfh); + } + + @After + public void teardown() { + // Native objects must be released child-first: pool, CF, DB, options. Any of them may + // be missing when setup() failed, and closing it must not hide that failure. + try { + if (this.pool != null) { + this.pool.close(); + } + } finally { + if (this.cfh != null) { + this.cfh.close(); + } + if (this.rocksdb != null) { + this.rocksdb.close(); + } + if (this.options != null) { + this.options.close(); + } + } + } + + @Test + public void testIteratorCloseReleasesNativeHandleAndIsIdempotent() { + ReusedRocksIterator reused = this.pool.newIterator(); + RocksIterator iter = reused.iterator(); + Assert.assertTrue(iter.isOwningHandle()); + + iter.seekToFirst(); + Assert.assertTrue(iter.isValid()); + Assert.assertEquals("k1", string(iter.key())); + Assert.assertEquals("v1", string(iter.value())); + + reused.close(); + Assert.assertFalse(iter.isOwningHandle()); + // A second close must not double-free the native iterator + reused.close(); + Assert.assertFalse(iter.isOwningHandle()); + } + + @Test + public void testIteratorsAreNotSharedBetweenCallers() { + ReusedRocksIterator first = this.pool.newIterator(); + ReusedRocksIterator second = this.pool.newIterator(); + try { + Assert.assertNotSame(first.iterator(), second.iterator()); + + first.iterator().seekToFirst(); + second.iterator().seekToLast(); + Assert.assertEquals("k1", string(first.iterator().key())); + Assert.assertEquals("k2", string(second.iterator().key())); + + // Closing one caller's iterator leaves the other usable + first.close(); + second.iterator().prev(); + Assert.assertTrue(second.iterator().isValid()); + Assert.assertEquals("k1", string(second.iterator().key())); + } finally { + first.close(); + second.close(); + } + } + + @Test + public void testPoolCloseDoesNotInvalidateOutstandingIterators() throws Exception { + Assert.assertEquals("IteratorPool-" + CF, this.pool.toString()); + + ReusedRocksIterator reused = this.pool.newIterator(); + try { + // Outstanding iterators are owned by their callers, not the pool + this.pool.close(); + this.pool.close(); + reused.iterator().seek(bytes("k2")); + Assert.assertTrue(reused.iterator().isValid()); + Assert.assertEquals("v2", string(reused.iterator().value())); + + // The pool can still hand out new iterators after close() + ReusedRocksIterator another = this.pool.newIterator(); + another.iterator().seekToFirst(); + Assert.assertEquals("k1", string(another.iterator().key())); + another.close(); + } finally { + reused.close(); + } + } + + private static byte[] bytes(String str) { + return str.getBytes(StandardCharsets.UTF_8); + } + + private static String string(byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSnapshotRestoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSnapshotRestoreTest.java new file mode 100644 index 0000000000..6cb8c9e540 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/store/rocksdb/RocksDBSnapshotRestoreTest.java @@ -0,0 +1,2544 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.backend.store.rocksdb; + +import java.io.File; +import java.io.FilterOutputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +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.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.io.FileUtils; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.exception.ConnectionException; +import org.apache.hugegraph.unit.FakeObjects; +import org.apache.hugegraph.testutil.Whitebox; +import org.junit.Rule; +import org.junit.Test; +import org.rocksdb.Checkpoint; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.Status; +import org.junit.rules.TemporaryFolder; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class RocksDBSnapshotRestoreTest { + + @Rule + public TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void testParentBindCheckpointSameDataRejectsBeforeDeletion() throws Exception { + this.checkParentBindCheckpointOverlap("same"); + } + + @Test + public void testParentBindCheckpointInsideDataRejectsBeforeDeletion() throws Exception { + this.checkParentBindCheckpointOverlap("inside"); + } + + @Test + public void testParentBindCheckpointContainingDataRejectsBeforeDeletion() throws Exception { + this.checkParentBindCheckpointOverlap("parent"); + } + + @Test + public void testParentBindCheckpointContainingWalRejectsBeforeDeletion() throws Exception { + this.checkParentBindCheckpointOverlap("wal"); + } + + private void checkParentBindCheckpointOverlap(String layout) throws Exception { + String original = System.getProperty("hugegraph.test.recovery.parent.original"); + String alias = System.getProperty("hugegraph.test.recovery.parent.alias"); + org.junit.Assume.assumeTrue("requires the explicit parent-bind kernel fixture", + original != null && alias != null); + assertTrue(Files.isSameFile(Path.of(original), Path.of(alias))); + Path root = Files.createTempDirectory(Path.of(original), "checkpoint-overlap-" + layout + "-"); + Path aliased = Path.of(alias).resolve(root.getFileName()); + File data = root.resolve("live").toFile(); + File wal = root.resolve("wal").toFile(); + File snapshot = aliased.resolve("live").toFile(); + if ("parent".equals(layout) || "wal".equals(layout)) { + File physicalCheckpoint = root.resolve("checkpoint").toFile(); + RocksDB.loadLibrary(); + try (Options options = new Options().setCreateIfMissing(true); + RocksDB db = RocksDB.open(options, physicalCheckpoint.toString())) { + db.put(new byte[]{8}, new byte[]{9}); + } + snapshot = aliased.resolve("checkpoint").toFile(); + if ("parent".equals(layout)) { + data = new File(physicalCheckpoint, "live"); + } else { + wal = new File(physicalCheckpoint, "wal"); + } + } + RocksDBStdSessions owner = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), wal.toString()); + try { + owner.createTable("test"); + owner.session().put("test", new byte[]{1}, new byte[]{2}); + owner.session().commit(); + if ("inside".equals(layout)) { + AtomicReference shared = Whitebox.getInternalState(owner, "rocksdb"); + try (Checkpoint checkpoint = Checkpoint.create(shared.get().rocksdb())) { + checkpoint.createCheckpoint(new File(data, "checkpoint").toString()); + } + snapshot = aliased.resolve("live/checkpoint").toFile(); + } + byte[] liveCurrent = Files.readAllBytes(new File(data, "CURRENT").toPath()); + byte[] sourceCurrent = Files.readAllBytes(new File(snapshot, "CURRENT").toPath()); + boolean rejected = false; + try { + owner.resumeSnapshot(snapshot.toString()); + } catch (BackendException expected) { + rejected = true; + } + Path marker = Path.of(data + ".resume-pending"); + // Keep the actual fixture and observations, including destructive negative controls. + String observed = "layout=" + layout + "\nlive_exists=" + data.exists() + + "\nsource_exists=" + snapshot.exists() + + "\nmarker_exists=" + Files.exists(marker) + "\n"; + Files.writeString(root.resolve("observed.txt"), observed); + System.out.print(observed); + assertTrue("Physical overlap must be rejected: " + observed, rejected); + assertFalse("Refusal must precede marker publication: " + observed, Files.exists(marker)); + assertArrayEquals(liveCurrent, Files.readAllBytes(new File(data, "CURRENT").toPath())); + assertArrayEquals(sourceCurrent, Files.readAllBytes(new File(snapshot, "CURRENT").toPath())); + } finally { + owner.close(); + } + RocksDBStdSessions fresh = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), wal.toString(), + Collections.singletonList("test")); + try { + assertArrayEquals(new byte[]{2}, fresh.session().get("test", new byte[]{1})); + } finally { + fresh.close(); + } + } + + @Test + public void testParentBindUnsafePendingRejectsBeforeDeletingNativeData() throws Exception { + String original = System.getProperty("hugegraph.test.recovery.parent.original"); + String alias = System.getProperty("hugegraph.test.recovery.parent.alias"); + org.junit.Assume.assumeTrue("requires the explicit parent-bind kernel fixture", + original != null && alias != null); + Path root = Files.createTempDirectory(Path.of(original), "unsafe-pending-"); + File data = root.resolve("live").toFile(); + File wal = root.resolve("wal").toFile(); + File snapshot = Path.of(alias).resolve(root.getFileName()).resolve("live").toFile(); + RocksDBStdSessions owner = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), wal.toString()); + try { + owner.createTable("test"); + owner.session().put("test", new byte[]{1}, new byte[]{2}); + owner.session().commit(); + } finally { + owner.close(); + } + // Represent a checksum-valid pending record accepted by the previous lexical guard. + Properties state = new Properties(); + state.setProperty("operation", java.util.UUID.randomUUID().toString()); + state.setProperty("data", data.getCanonicalPath()); + state.setProperty("snapshot", snapshot.getCanonicalPath()); + state.setProperty("wal", wal.getCanonicalPath()); + state.setProperty("configured-wal", wal.getCanonicalPath()); + state.setProperty("wal-links", "0"); + java.lang.reflect.Method generation = RocksDBSnapshotRestore.class.getDeclaredMethod("generation", File.class); + generation.setAccessible(true); + state.setProperty("generation", (String) generation.invoke(null, snapshot)); + java.lang.reflect.Method checksum = RocksDBSnapshotRestore.class.getDeclaredMethod("metadataDigest", + Properties.class); + checksum.setAccessible(true); + state.setProperty("metadata-sha256", (String) checksum.invoke(null, state)); + Path marker = Path.of(data + ".resume-pending"); + writeRecoveryMetadata(marker, state); + byte[] pending = Files.readAllBytes(marker); + byte[] current = Files.readAllBytes(new File(data, "CURRENT").toPath()); + try { + RocksDBStdSessions fresh = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), wal.toString(), + Collections.singletonList("test")); + fresh.close(); + fail("Previously accepted physical-overlap pending record must fail closed"); + } catch (BackendException expected) { + assertArrayEquals(pending, Files.readAllBytes(marker)); + assertArrayEquals(current, Files.readAllBytes(new File(data, "CURRENT").toPath())); + assertTrue(snapshot.exists()); + } + } + + @Test + public void testParentBindDataWalAliasPreservesNativeWalTail() throws Exception { + String original = System.getProperty("hugegraph.test.recovery.parent.original"); + String alias = System.getProperty("hugegraph.test.recovery.parent.alias"); + org.junit.Assume.assumeTrue("requires the explicit parent-bind kernel fixture", + original != null && alias != null); + assertTrue(Files.isSameFile(Path.of(original), Path.of(alias))); + for (boolean aliased : new boolean[]{false, true}) { + Path root = Files.createTempDirectory(Path.of(original), "wal-tail-"); + File data = root.resolve("data").toFile(); + File wal = aliased ? Path.of(alias).resolve(root.getFileName()).resolve("data").toFile() : data; + File checkpoint = root.resolve("checkpoint").toFile(); + Files.createDirectories(data.toPath()); + assertTrue(Files.isSameFile(data.toPath(), wal.toPath())); + try { + nativeCheckpointWithWalTail(data, wal, checkpoint); + try (RecoveryLock held = RocksDBSnapshotRestore.lock(data.toString())) { + RocksDBSnapshotRestore.start(data.toString(), wal.toString(), checkpoint.toString()); + RocksDBSnapshotRestore restore = + RocksDBSnapshotRestore.prepareOpen(data.toString(), wal.toString()); + assertNotNull(restore); + try (Options options = new Options().setWalDir(wal.toString()); + RocksDB reopened = RocksDB.open(options, data.toString())) { + assertArrayEquals(new byte[]{2}, reopened.get(new byte[]{1})); + assertArrayEquals("Restored WAL-only data must survive physical directory aliases", + new byte[]{4}, reopened.get(new byte[]{3})); + restore.complete(); + } + } + assertFalse(new File(data + ".resume-pending").exists()); + assertFalse(checkpoint.exists()); + } finally { + FileUtils.deleteDirectory(root.toFile()); + } + } + } + + private static void nativeCheckpointWithWalTail(File data, File wal, File checkpoint) throws Exception { + RocksDB.loadLibrary(); + try (Options options = new Options().setCreateIfMissing(true).setWalDir(wal.toString()); + RocksDB db = RocksDB.open(options, data.toString())) { + db.put(new byte[]{1}, new byte[]{2}); + try (Checkpoint snapshot = Checkpoint.create(db)) { + snapshot.createCheckpoint(checkpoint.toString()); + } + db.put(new byte[]{3}, new byte[]{4}); + db.flushWal(true); + File[] logs = wal.listFiles(file -> file.getName().matches("[0-9]+\\.log")); + assertNotNull(logs); + assertTrue(logs.length > 0); + for (File log : logs) { + FileUtils.copyFile(log, new File(checkpoint, log.getName())); + } + // Verify the actual checkpoint can replay its tail before testing restore. + try (Options readOptions = new Options(); + RocksDB source = RocksDB.openReadOnly(readOptions, checkpoint.toString())) { + assertArrayEquals(new byte[]{2}, source.get(new byte[]{1})); + assertArrayEquals(new byte[]{4}, source.get(new byte[]{3})); + } + } + } + + @Test + public void testDirectDataSymlinkIsRejectedBeforeNativeOpen() throws Exception { + File root = this.temporary.newFolder("direct-data-link"); + File data = new File(root, "physical-data"); + Path alias = new File(root, "configured-data").toPath(); + RocksDB.loadLibrary(); + try (Options options = new Options().setCreateIfMissing(true); + RocksDB db = RocksDB.open(options, data.toString())) { + db.put(new byte[]{1}, new byte[]{2}); + } + byte[] current = Files.readAllBytes(new File(data, "CURRENT").toPath()); + Files.createSymbolicLink(alias, data.toPath()); + for (String configured : Arrays.asList(alias.toString(), alias.resolve(".").toString())) { + for (boolean withTables : new boolean[]{false, true}) { + RocksDBStdSessions opened = null; + try { + opened = withTables ? new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + configured, data.toString(), Collections.emptyList()) : + new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + configured, data.toString()); + fail("Direct data symlink must be rejected before opening a native owner"); + } catch (RocksDBException expected) { + assertTrue(expected.getCause().getCause().getMessage().contains("symbolic link")); + } finally { + if (opened != null) { + opened.forceCloseRocksDB(); + } + } + assertArrayEquals(current, Files.readAllBytes(new File(data, "CURRENT").toPath())); + assertFalse(new File(data + ".resume-pending").exists()); + } + } + FileUtils.deleteDirectory(data); + try { + RocksDBSnapshotRestore.lock(alias.toString()); + fail("A dangling direct DB link must not change recovery guard identity"); + } catch (BackendException expected) { + assertTrue(expected.getCause().getMessage().contains("symbolic link")); + } + assertFalse(new File(alias + ".resume-lock").exists()); + } + + @Test + public void testParentDataSymlinkStillSupportsNativeRestore() throws Exception { + File root = this.temporary.newFolder("snapshot-parent-link"); + Path physical = new File(root, "physical").toPath(); + Files.createDirectories(physical); + Path parent = new File(root, "parent").toPath(); + Files.createSymbolicLink(parent, physical); + File data = parent.resolve("db").toFile(); + File snapshot = new File(root, "checkpoint"); + RocksDBStdSessions owner = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()); + try { + owner.createTable("test"); + owner.session().put("test", new byte[]{1}, new byte[]{2}); + owner.session().commit(); + owner.createSnapshot(snapshot.toString()); + owner.session().put("test", new byte[]{1}, new byte[]{3}); + owner.session().commit(); + owner.resumeSnapshot(snapshot.toString()); + assertArrayEquals(new byte[]{2}, owner.session().get("test", new byte[]{1})); + assertTrue(Files.isSymbolicLink(parent)); + } finally { + owner.close(); + } + } + + @Test + public void testUnknownMarkerExistenceRejectsRecovery() throws Exception { + Path root = this.temporary.newFolder("inaccessible-marker").toPath(); + org.junit.Assume.assumeTrue(Files.getFileStore(root).supportsFileAttributeView("posix")); + Path data = root.resolve("data"); + Files.createDirectories(data); + Path marker = root.resolve("data.resume-pending"); + byte[] record = new byte[]{4, 2}; + Files.write(marker, record); + Set permissions = Files.getPosixFilePermissions(root); + try { + Files.setPosixFilePermissions(root, Collections.emptySet()); + org.junit.Assume.assumeTrue("runner bypasses directory permissions; marker remains observable", + !Files.exists(marker, java.nio.file.LinkOption.NOFOLLOW_LINKS) && + !Files.notExists(marker, java.nio.file.LinkOption.NOFOLLOW_LINKS)); + assertFalse(Files.exists(marker, java.nio.file.LinkOption.NOFOLLOW_LINKS)); + assertFalse(Files.notExists(marker, java.nio.file.LinkOption.NOFOLLOW_LINKS)); + try { + RocksDBSnapshotRestore.prepareOpen(data.toString(), data.toString()); + fail("Unknown marker existence must not be treated as no pending restore"); + } catch (BackendException expected) { + assertTrue(expected.getMessage().contains("preserve")); + } + } finally { + Files.setPosixFilePermissions(root, permissions); + } + assertArrayEquals(record, Files.readAllBytes(marker)); + assertEquals(0, data.toFile().list().length); + } + + @Test + public void testUnrecordedWalLinksRejectBeforeDeletingNativeData() throws Exception { + for (boolean targetChain : new boolean[]{false, true}) { + File root = this.temporary.newFolder("unsafe-wal-" + targetChain); + File data = new File(root, "data"); + Files.createDirectories(data.toPath()); + Path physical = new File(root, "physical/child").toPath(); + Files.createDirectories(physical); + Path inner = new File(data, "link").toPath(); + Files.createSymbolicLink(inner, physical); + Path wal; + if (targetChain) { + Path external = new File(root, "external-link").toPath(); + Files.createSymbolicLink(external, inner); + wal = external.resolve("logs"); + } else { + wal = inner.resolve("../logs"); + } + FileUtils.forceMkdir(wal.toFile()); + File snapshot = new File(root, "checkpoint"); + nativeCheckpointWithWalTail(data, wal.toFile(), snapshot); + byte[] current = Files.readAllBytes(new File(data, "CURRENT").toPath()); + try { + RocksDBSnapshotRestore.start(data.toString(), wal.toString(), snapshot.toString()); + fail("WAL links lost when data is replaced must be rejected before mutation"); + } catch (IOException expected) { + assertTrue(expected.getMessage().contains("WAL")); + } + assertTrue(Files.isSymbolicLink(inner)); + assertArrayEquals(current, Files.readAllBytes(new File(data, "CURRENT").toPath())); + assertTrue(new File(snapshot, "CURRENT").isFile()); + assertFalse(new File(data + ".resume-pending").exists()); + try (Options options = new Options().setWalDir(wal.toString()); + RocksDB reopened = RocksDB.open(options, data.toString())) { + assertArrayEquals(new byte[]{2}, reopened.get(new byte[]{1})); + assertArrayEquals(new byte[]{4}, reopened.get(new byte[]{3})); + } + } + } + + @Test + public void testFailedStoreRestoreCanCloseAndReopenSameInstance() throws Exception { + File root = this.temporary.newFolder("store-reopen"); + File data = new File(root, "snapshot-data/store"); + File snapshot = new File(root, "snapshot_snapshot-data/store"); + HugeConfig config = FakeObjects.newConfig(); + config.setProperty(RocksDBOptions.DATA_PATH.name(), data.getParent()); + config.setProperty(RocksDBOptions.WAL_PATH.name(), data.getParent()); + RocksDBStdSessions failed = new RocksDBStdSessions(config, "db", "store", + data.toString(), data.toString()) { + @Override + public synchronized void resumeSnapshot(String path) { + AtomicReference reference = Whitebox.getInternalState(this, "rocksdb"); + RecoveryLock lease = reference.get().closeForRestore(); + try { + RocksDBSnapshotRestore restore = new RocksDBSnapshotRestore( + data.toString(), data.toString(), path, new FaultyFiles("data-copy")); + restore.begin(); + restore.install(); + fail("Expected checkpoint copy failure"); + } catch (IOException e) { + throw new BackendException("Injected copy failure", e); + } finally { + RocksDBSnapshotRestore.unlock(lease); + } + } + }; + RocksDBStore.RocksDBGraphStore store = (RocksDBStore.RocksDBGraphStore) cleanupStore(failed); + List tables = store.tableNames(); + String table = tables.get(0); + try { + // Match the real Store CF layout so reopen exercises recovery rather + // than an unrelated missing-column-family fallback. + failed.createTable(tables.toArray(new String[0])); + failed.session().open(); + failed.session().put(table, new byte[]{1}, new byte[]{2}); + failed.session().commit(); + failed.createSnapshot(snapshot.toString()); + try { + store.resumeSnapshot("snapshot", true); + fail("Expected native owner to close before injected copy failure"); + } catch (BackendException expected) { + assertTrue(expected.getMessage().contains("Injected copy failure")); + } + assertFalse(store.opened()); + assertTrue(new File(data + ".resume-pending").exists()); + store.close(); + assertTrue(failed.closed()); + store.open(config); + assertTrue(store.opened()); + RocksDBSessions reopened = Whitebox.getInternalState(store, "sessions"); + assertArrayEquals(new byte[]{2}, reopened.session().get(table, new byte[]{1})); + assertFalse(new File(data + ".resume-pending").exists()); + assertFalse(snapshot.exists()); + store.close(); + } finally { + failed.forceCloseRocksDB(); + RocksDBSessions remaining = Whitebox.getInternalState(store, "sessions"); + remaining.forceCloseRocksDB(); + } + } + + @Test + public void testPendingRestoreRejectsRepeatedCopiesBeforeOpeningSource() throws Exception { + for (boolean malformed : new boolean[]{false, true}) { + File root = this.temporary.newFolder("snapshot-pending-copy-" + malformed); + File data = new File(root, "data"); + File source = new File(root, "source"); + AtomicInteger sourceOpens = new AtomicInteger(); + RocksDBStdSessions owner = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()) { + @Override + OpenedRocksDB openSnapshot(String path) throws RocksDBException { + sourceOpens.incrementAndGet(); + return super.openSnapshot(path); + } + }; + try { + owner.createSnapshot(source.toString()); + String pendingCopy = owner.hardLinkSnapshot(source.toString()); + RocksDBSnapshotRestore.start(data.toString(), data.toString(), pendingCopy); + Path marker = new File(data + ".resume-pending").toPath(); + if (malformed) { + Files.writeString(marker, "incomplete pending record"); + // Exercise the acquired-lock path as well as a live owner's lease. + owner.forceCloseRocksDB(); + } + byte[] pendingBytes = Files.readAllBytes(marker); + byte[] current = Files.readAllBytes(new File(pendingCopy, "CURRENT").toPath()); + sourceOpens.set(0); + for (int attempt = 0; attempt < 3; attempt++) { + try { + owner.hardLinkSnapshot(source.toString()); + fail("Existing recovery must reject a new checkpoint copy"); + } catch (BackendException expected) { + assertTrue(expected.getMessage().contains("reopen the database")); + } + assertEquals(0, sourceOpens.get()); + assertArrayEquals(new String[]{new File(pendingCopy).getName()}, + root.list((parent, name) -> name.startsWith("data_temp-"))); + assertArrayEquals(pendingBytes, Files.readAllBytes(marker)); + assertArrayEquals(current, Files.readAllBytes(new File(pendingCopy, "CURRENT").toPath())); + assertTrue(new File(source, "CURRENT").isFile()); + } + } finally { + owner.forceCloseRocksDB(); + } + } + } + + @Test + public void testLaterPendingDatabaseReclaimsEarlierNewCopy() throws Exception { + File root = this.temporary.newFolder("snapshot-later-pending"); + File firstSource = new File(root, "first-source"); + File secondSource = new File(root, "second-source"); + File secondData = new File(root, "second"); + AtomicInteger sourceOpens = new AtomicInteger(); + RocksDBStdSessions first = cleanupOwner(new File(root, "first"), firstSource); + RocksDBStdSessions second = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + secondData.toString(), secondData.toString()) { + @Override + public String buildSnapshotPath(String prefix) { + return secondSource.toString(); + } + + @Override + OpenedRocksDB openSnapshot(String path) throws RocksDBException { + sourceOpens.incrementAndGet(); + return super.openSnapshot(path); + } + }; + try { + first.createSnapshot(firstSource.toString()); + second.createSnapshot(secondSource.toString()); + RocksDBSnapshotRestore.start(secondData.toString(), secondData.toString(), secondSource.toString()); + Path marker = new File(secondData + ".resume-pending").toPath(); + byte[] pendingBytes = Files.readAllBytes(marker); + RocksDBStore store = cleanupStore(first, second); + for (int attempt = 0; attempt < 3; attempt++) { + try { + store.resumeSnapshot("ignored", false); + fail("Later database is already recovering"); + } catch (BackendException expected) { + assertTrue(expected.getMessage().contains("reopen the database")); + } + assertEquals(0, sourceOpens.get()); + assertEquals(0, root.list((parent, name) -> name.startsWith("first_temp-") || + name.startsWith("second_temp-")).length); + assertArrayEquals(pendingBytes, Files.readAllBytes(marker)); + assertTrue(new File(firstSource, "CURRENT").isFile()); + assertTrue(new File(secondSource, "CURRENT").isFile()); + } + } finally { + first.forceCloseRocksDB(); + second.forceCloseRocksDB(); + } + } + + @Test + public void testUnusedCopiesDoNotAccumulateOnFirstPassFailure() throws Exception { + File root = this.temporary.newFolder("snapshot-copy-failure"); + File source = new File(root, "source"); + RuntimeException failure = new IllegalStateException("later database has no snapshot"); + RocksDBStdSessions first = cleanupOwner(new File(root, "first"), source); + RocksDBStdSessions second = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + new File(root, "second").toString(), + new File(root, "second").toString()) { + @Override + public String buildSnapshotPath(String prefix) { + throw failure; + } + }; + try { + first.createSnapshot(source.toString()); + RocksDBStore store = cleanupStore(first, second); + File foreign = new File(root, "first_temp-foreign"); + assertTrue(foreign.mkdir()); + for (int attempt = 0; attempt < 3; attempt++) { + try { + store.resumeSnapshot("ignored", false); + fail("Expected first-pass failure"); + } catch (IllegalStateException expected) { + assertSame(failure, expected); + } + assertArrayEquals(new String[]{"first_temp-foreign"}, + root.list((parent, name) -> name.startsWith("first_temp-"))); + assertTrue(source.isDirectory()); + assertTrue(first.databaseOpened()); + } + } finally { + first.forceCloseRocksDB(); + second.forceCloseRocksDB(); + } + } + + @Test + public void testRestoreFailurePreservesPendingButCleansUnstartedCopy() throws Exception { + File root = this.temporary.newFolder("snapshot-restore-failure"); + List copies = new ArrayList<>(); + List started = new ArrayList<>(); + RocksDBStdSessions[] owners = new RocksDBStdSessions[2]; + try { + for (int i = 0; i < owners.length; i++) { + File data = new File(root, "data" + i); + File source = new File(root, "source" + i); + owners[i] = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()) { + @Override + public String buildSnapshotPath(String prefix) { + return source.toString(); + } + + @Override + public String hardLinkSnapshot(String path) throws RocksDBException { + String copy = super.hardLinkSnapshot(path); + copies.add(copy); + return copy; + } + + @Override + public synchronized void resumeSnapshot(String path) { + started.add(path); + this.forceCloseRocksDB(); + try (RecoveryLock lock = RocksDBSnapshotRestore.lock(data.toString())) { + RocksDBSnapshotRestore.start(data.toString(), data.toString(), path); + } catch (IOException e) { + throw new AssertionError(e); + } + throw new IllegalStateException("stop after pending marker"); + } + }; + owners[i].createSnapshot(source.toString()); + } + try { + cleanupStore(owners).resumeSnapshot("ignored", false); + fail("Expected second-pass failure"); + } catch (IllegalStateException expected) { + assertEquals("stop after pending marker", expected.getMessage()); + } + assertEquals(2, copies.size()); + assertEquals(1, started.size()); + for (String copy : copies) { + assertEquals(started.contains(copy), new File(copy).exists()); + } + assertTrue(new File(root, "source0").exists()); + assertTrue(new File(root, "source1").exists()); + } finally { + for (RocksDBStdSessions owner : owners) { + if (owner != null) { + owner.forceCloseRocksDB(); + } + } + } + } + + @Test + public void testCleanupPreservesCopiesWithMalformedOrNonFileMarker() throws Exception { + File root = this.temporary.newFolder("snapshot-uncertain-marker"); + File data = new File(root, "data"); + File source = new File(root, "source"); + RocksDBStdSessions owner = cleanupOwner(data, source); + try { + owner.createSnapshot(source.toString()); + String copy = owner.hardLinkSnapshot(source.toString()); + Path marker = new File(data + ".resume-pending").toPath(); + Files.write(marker, "snapshot=broken\n".getBytes(StandardCharsets.UTF_8)); + owner.cleanupSnapshot(copy); + assertTrue(new File(copy).exists()); + Files.delete(marker); + Files.createDirectory(marker); + owner.cleanupSnapshot(copy); + assertTrue(new File(copy).exists()); + Files.delete(marker); + owner.cleanupSnapshot(copy); + assertFalse(new File(copy).exists()); + assertTrue(source.exists()); + } finally { + owner.forceCloseRocksDB(); + } + } + + @Test + public void testCleanupRetainsCopyWhenAnotherOwnerHoldsLock() throws Exception { + File root = this.temporary.newFolder("snapshot-cleanup-contention"); + File data = new File(root, "data"); + File source = new File(root, "source"); + RocksDBStdSessions owner = cleanupOwner(data, source); + try { + owner.createSnapshot(source.toString()); + String copy = owner.hardLinkSnapshot(source.toString()); + owner.forceCloseRocksDB(); + try (RecoveryLock competing = RocksDBSnapshotRestore.lock(data.toString())) { + try { + owner.cleanupSnapshot(copy); + fail("Cleanup must acquire recovery ownership"); + } catch (BackendException expected) { + assertTrue(new File(copy).exists()); + assertTrue(competing.isOpen()); + } + } + owner.cleanupSnapshot(copy); + assertFalse(new File(copy).exists()); + assertTrue(source.exists()); + } finally { + owner.forceCloseRocksDB(); + } + } + + @Test + public void testConsumingRestoreFailureNeverCleansUserCheckpoint() throws Exception { + File root = this.temporary.newFolder("snapshot-consuming-failure"); + File source = new File(root, "source"); + RocksDBStdSessions owner = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + new File(root, "data").toString(), + new File(root, "data").toString()) { + @Override + public String buildSnapshotPath(String prefix) { + return source.toString(); + } + + @Override + public void resumeSnapshot(String path) { + throw new IllegalStateException("before consuming source"); + } + }; + try { + owner.createSnapshot(source.toString()); + try { + cleanupStore(owner).resumeSnapshot("ignored", true); + fail("Expected restore failure"); + } catch (IllegalStateException expected) { + assertEquals("before consuming source", expected.getMessage()); + } + assertTrue(new File(source, "CURRENT").isFile()); + } finally { + owner.forceCloseRocksDB(); + } + } + + @Test + public void testCopyFailureBeforeReturnCleansOutputAndInternalStaging() throws Exception { + for (boolean closeFailure : new boolean[]{false, true}) { + File root = this.temporary.newFolder("snapshot-before-return-" + closeFailure); + File source = new File(root, "source"); + RuntimeException failure = new IllegalStateException("checkpoint creation or close failed"); + RocksDBStdSessions owner = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + new File(root, "data").toString(), + new File(root, "data").toString()) { + @Override + OpenedRocksDB openSnapshot(String path) throws RocksDBException { + OpenedRocksDB delegate = super.openSnapshot(path); + return new OpenedRocksDB(delegate.rocksdb(), new HashMap<>(), null) { + @Override + public void createCheckpoint(String target) { + if (closeFailure) { + delegate.createCheckpoint(target); + } else { + try { + Files.createDirectory(new File(target + "_temp").toPath()); + Files.write(new File(target + "_temp/partial").toPath(), new byte[]{1}); + } catch (IOException e) { + throw new AssertionError(e); + } + throw failure; + } + } + + @Override + public void close() { + delegate.close(); + if (closeFailure) { + throw failure; + } + } + }; + } + }; + try { + owner.createSnapshot(source.toString()); + try { + owner.hardLinkSnapshot(source.toString()); + fail("Expected checkpoint failure"); + } catch (IllegalStateException expected) { + assertSame(failure, expected); + } + assertEquals(0, root.list((parent, name) -> name.startsWith("data_temp-")).length); + assertTrue(new File(source, "CURRENT").isFile()); + } finally { + owner.forceCloseRocksDB(); + } + } + } + + @Test + public void testCleanupFailureDoesNotReplaceRestoreFailure() throws Exception { + File root = this.temporary.newFolder("snapshot-cleanup-error"); + File data = new File(root, "data"); + File source = new File(root, "source"); + RuntimeException original = new IllegalStateException("restore failed"); + RuntimeException cleanup = new IllegalStateException("cleanup failed"); + RocksDBStdSessions owner = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()) { + @Override + public String buildSnapshotPath(String prefix) { + return source.toString(); + } + + @Override + public void resumeSnapshot(String path) { + throw original; + } + + @Override + void cleanupSnapshot(String path) { + throw cleanup; + } + }; + try { + owner.createSnapshot(source.toString()); + try { + cleanupStore(owner).resumeSnapshot("ignored", false); + fail("Expected restore failure"); + } catch (IllegalStateException expected) { + assertSame(original, expected); + assertArrayEquals(new Throwable[]{cleanup}, expected.getSuppressed()); + } + } finally { + owner.forceCloseRocksDB(); + } + } + + private static RocksDBStdSessions cleanupOwner(File data, File source) throws RocksDBException { + return new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()) { + @Override + public String buildSnapshotPath(String prefix) { + return source.toString(); + } + }; + } + + private static RocksDBStore cleanupStore(RocksDBStdSessions... owners) { + RocksDBStore store = new RocksDBStore.RocksDBGraphStore(new RocksDBStoreProvider(), "db", "store"); + Whitebox.setInternalState(store, "sessions", owners[0]); + Map databases = new ConcurrentSkipListMap<>(); + for (int i = 0; i < owners.length; i++) { + // Match registerOpenedSessions: native ownership alone leaves the + // BackendSessionPool closed until its initial session is registered. + owners[i].session().open(); + databases.put(Integer.toString(i), owners[i]); + } + Whitebox.setInternalState(store, "dbs", databases); + assertTrue(store.opened()); + return store; + } + + @Test + public void testDataCopyFailureRetainsCheckpointAndRetries() throws Exception { + this.failureAndRetry("data-copy", "separate"); + } + + @Test + public void testWalCopyFailureRetainsCheckpointAndRetries() throws Exception { + this.failureAndRetry("copy", "separate"); + } + + @Test + public void testShortCopyIsNotPublished() throws Exception { + this.failureAndRetry("short-copy", "separate"); + } + + @Test + public void testSameLengthCorruptionIsNotPublished() throws Exception { + this.failureAndRetry("corrupt-copy", "separate"); + } + + @Test + public void testRetirementFailureRetainsMarkerAndRetries() throws Exception { + this.failureAndRetry("retire", "separate"); + } + + @Test + public void testPublishFailureRetainsMarkerAndRetries() throws Exception { + this.failureAndRetry("publish", "separate"); + } + + @Test + public void testParentWalRetirementFailurePreservesData() throws Exception { + this.failureAndRetry("retire", "parent"); + } + + @Test + public void testNestedWalPublishFailureRetries() throws Exception { + this.failureAndRetry("publish", "nested"); + } + + @Test + public void testSymlinkWalPublishFailurePreservesLink() throws Exception { + this.failureAndRetry("publish", "symlink"); + } + + @Test + public void testSymlinkInsideDataSurvivesInterruptedRestore() throws Exception { + this.failureAndRetry("data-copy", "nested-symlink"); + } + + @Test + public void testIndirectSymlinkInsideDataSurvivesInterruptedRestore() throws Exception { + this.failureAndRetry("data-copy", "ancestor-symlink"); + } + + @Test + public void testCorruptedDataCopyIsNotOpened() throws Exception { + this.failureAndRetry("corrupt-data-copy", "separate"); + } + + private static void fakeCheckpoint(File snapshot) throws IOException { + Files.write(new File(snapshot, "CURRENT").toPath(), "MANIFEST-000001\n".getBytes(StandardCharsets.UTF_8)); + Files.write(new File(snapshot, "MANIFEST-000001").toPath(), new byte[]{1, 2, 3}); + } + + private void failureAndRetry(String fault, String layout) throws Exception { + File root = this.temporary.newFolder(); + File data = new File(root, "data"); + File wal = new File(root, "wal"); + File snapshot = new File(root, "snapshot"); + if ("parent".equals(layout)) { + data = new File(wal, "data"); + snapshot = new File(wal, "checkpoints/snapshot"); + } else if ("nested".equals(layout)) { + wal = new File(data, "wal"); + } + FileUtils.forceMkdir(data); + if (layout.contains("symlink")) { + File target = new File(root, "target"); + FileUtils.forceMkdir(target); + File linkParent = "nested-symlink".equals(layout) || "ancestor-symlink".equals(layout) ? data : root; + wal = new File(linkParent, "wal-link"); + Files.createSymbolicLink(wal.toPath(), target.toPath()); + if ("ancestor-symlink".equals(layout)) { + wal = new File(wal, "child-wal"); + FileUtils.forceMkdir(wal); + } + } else { + FileUtils.forceMkdir(wal); + } + FileUtils.forceMkdir(snapshot); + fakeCheckpoint(snapshot); + byte[] tail = "checkpoint".getBytes(StandardCharsets.UTF_8); + FileUtils.writeByteArrayToFile(new File(snapshot, "000123.log"), tail); + FileUtils.writeByteArrayToFile(new File(snapshot, "keep.txt"), tail); + // Same-name, same-length old WAL must never be accepted as the checkpoint. + FileUtils.writeByteArrayToFile(new File(wal, "000123.log"), + "old-oldwal".getBytes(StandardCharsets.UTF_8)); + FileUtils.writeByteArrayToFile(new File(wal, "000124.log"), tail); + RocksDBSnapshotRestore restore = new RocksDBSnapshotRestore( + data.toString(), wal.toString(), snapshot.toString(), new FaultyFiles(fault)); + restore.begin(); + try { + restore.install(); + fail("Expected injected " + fault); + } catch (IOException expected) { + assertTrue(new File(data + ".resume-pending").isFile()); + assertArrayEquals(tail, Files.readAllBytes(new File(snapshot, "000123.log").toPath())); + } + // The public resume path can retry the SAME source even if its WAL alias + // disappeared between deleting data and copying the checkpoint. + RocksDBSnapshotRestore.start(data.toString(), wal.toString(), snapshot.toString()); + RocksDBSnapshotRestore retry = RocksDBSnapshotRestore.prepareOpen(data.toString(), wal.toString()); + assertNotNull(retry); + assertArrayEquals(tail, Files.readAllBytes(new File(wal, "000123.log").toPath())); + assertFalse(new File(wal, "000124.log").exists()); + assertFalse(new File(data, "000123.log").exists()); + assertTrue(new File(data, "keep.txt").isFile()); + assertTrue(new File(snapshot, "000123.log").isFile()); + if (layout.contains("symlink")) { + assertTrue(Files.isSymbolicLink("ancestor-symlink".equals(layout) ? + wal.getParentFile().toPath() : wal.toPath())); + } + retry.complete(); + assertFalse(new File(data + ".resume-pending").exists()); + } + + @Test + public void testMarkerDirectorySyncPrecedesInstallAndCheckpointCleanup() throws Exception { + this.checkMarkerDirectorySync(0); + } + + @Test + public void testPublicationDirectorySyncFailurePreservesLiveData() throws Exception { + this.checkMarkerDirectorySync(1); + } + + @Test + public void testRetryDirectorySyncFailurePreservesLiveData() throws Exception { + this.checkMarkerDirectorySync(2); + } + + @Test + public void testCompletionDirectorySyncFailurePreservesCheckpoint() throws Exception { + this.checkMarkerDirectorySync(3); + } + + private void checkMarkerDirectorySync(int failAt) throws Exception { + File root = this.temporary.newFolder("directory-sync-" + failAt); + File data = new File(root, "data"); + File snapshot = new File(root, "snapshot"); + FileUtils.forceMkdir(data); + FileUtils.forceMkdir(snapshot); + fakeCheckpoint(snapshot); + Path live = new File(data, "live-sentinel").toPath(); + byte[] original = new byte[]{42}; + Files.write(live, original); + Path marker = Path.of(data + ".resume-pending"); + AtomicInteger syncs = new AtomicInteger(); + RocksDBSnapshotRestore.FileOperations operations = new RocksDBSnapshotRestore.FileOperations() { + @Override + void forceDirectory(Path directory) throws IOException { + int call = syncs.incrementAndGet(); + assertEquals(marker.getParent().toRealPath(), directory.toRealPath()); + assertEquals(call < 3, Files.exists(marker)); + // The retry source survives both sides of the marker removal boundary. + assertTrue(snapshot.isDirectory()); + if (call == failAt) { + throw new IOException("injected directory sync failure " + call); + } + // Execute the real Linux directory fsync; this does not simulate a power loss. + super.forceDirectory(directory); + } + + @Override + void deleteDirectory(File directory) throws IOException { + assertEquals(2, syncs.get()); + super.deleteDirectory(directory); + } + }; + RocksDBSnapshotRestore restore = new RocksDBSnapshotRestore( + data.toString(), data.toString(), snapshot.toString(), operations); + try { + restore.begin(); + restore.install(); + restore.complete(); + assertEquals(0, failAt); + assertEquals(3, syncs.get()); + assertFalse(Files.exists(marker)); + assertFalse(snapshot.exists()); + } catch (IOException | BackendException expected) { + assertTrue(failAt > 0); + assertEquals(failAt, syncs.get()); + assertTrue(snapshot.isDirectory()); + if (failAt < 3) { + assertArrayEquals(original, Files.readAllBytes(live)); + assertTrue(Files.isRegularFile(marker)); + // A normal subsequent open retries the preserved checkpoint. + RocksDBSnapshotRestore retry = RocksDBSnapshotRestore.prepareOpen(data.toString(), data.toString()); + assertNotNull(retry); + retry.complete(); + } else { + // Installation already succeeded, but failed unlink fsync must not consume its source. + assertFalse(Files.exists(marker)); + assertTrue(new File(data, "CURRENT").isFile()); + } + } + } + + @Test + public void testWalNameCollisionFailsClosed() throws Exception { + File data = this.temporary.newFolder("data"); + File wal = this.temporary.newFolder("wal"); + File snapshot = this.temporary.newFolder("snapshot"); + fakeCheckpoint(snapshot); + FileUtils.writeByteArrayToFile(new File(snapshot, "000123.log"), new byte[]{1}); + FileUtils.forceMkdir(new File(wal, "000123.log")); + RocksDBSnapshotRestore restore = new RocksDBSnapshotRestore( + data.toString(), wal.toString(), snapshot.toString(), + new RocksDBSnapshotRestore.FileOperations()); + restore.begin(); + try { + RocksDBSnapshotRestore.prepareOpen(data.toString(), wal.toString()); + fail("WAL directory collision must fail closed"); + } catch (BackendException expected) { + assertTrue(new File(data + ".resume-pending").isFile()); + assertTrue(new File(snapshot, "000123.log").isFile()); + } + } + + @Test + public void testFreshSessionRecoversPendingSnapshotBeforeNativeOpen() throws Exception { + File data = this.temporary.newFolder("data"); + File wal = this.temporary.newFolder("wal"); + File snapshot = new File(this.temporary.newFolder("snapshot-root"), "rocks"); + HugeConfig config = FakeObjects.newConfig(); + RocksDBSessions original = new RocksDBStdSessions(config, "db", "store", data.toString(), wal.toString()); + byte[] key = new byte[]{1}; + try { + original.createTable("test"); + original.session().put("test", key, new byte[]{2}); + original.session().commit(); + original.createSnapshot(snapshot.toString()); + original.session().put("test", key, new byte[]{3}); + original.session().commit(); + } finally { + original.close(); + } + RocksDBSnapshotRestore restore = new RocksDBSnapshotRestore( + data.toString(), wal.toString(), snapshot.toString(), new FaultyFiles("data-copy")); + restore.begin(); + try { + restore.install(); + fail("Expected data copy failure"); + } catch (IOException expected) { + assertTrue(new File(data + ".resume-pending").exists()); + } + RocksDBSessions fresh = new RocksDBStdSessions(config, "db", "store", data.toString(), wal.toString(), + Collections.singletonList("test")); + try { + assertArrayEquals(new byte[]{2}, fresh.session().get("test", key)); + assertFalse(new File(data + ".resume-pending").exists()); + assertFalse(snapshot.exists()); + } finally { + fresh.close(); + } + } + + @Test + public void testBothOpenOverloadsRejectIncompleteMarker() throws Exception { + File data = this.temporary.newFolder("data"); + File marker = new File(data + ".resume-pending"); + Files.write(marker.toPath(), new byte[0]); + HugeConfig config = FakeObjects.newConfig(); + for (boolean withTables : new boolean[]{false, true}) { + try { + if (withTables) { + new RocksDBStdSessions(config, "db", "store", data.toString(), data.toString(), + Collections.emptyList()); + } else { + new RocksDBStdSessions(config, "db", "store", data.toString(), data.toString()); + } + fail("Native open must not bypass incomplete marker"); + } catch (BackendException expected) { + assertEquals(0, data.list().length); + assertTrue(marker.isFile()); + } + } + } + + @Test + public void testCompetingRecoveryCannotEnterWhileOwnerHoldsLock() throws Exception { + File data = this.temporary.newFolder("data"); + try (RecoveryLock owner = RocksDBSnapshotRestore.lock(data.toString())) { + CountDownLatch done = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + Thread contender = new Thread(() -> { + try { + for (boolean withTables : new boolean[]{false, true}) { + try { + if (withTables) { + new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString(), + Collections.emptyList()); + } else { + new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()); + } + fail("Competing open acquired recovery lock"); + } catch (RocksDBException expected) { + assertTrue(expected.getMessage().contains("No locks available")); + assertEquals(Status.Code.IOError, expected.getStatus().getCode()); + assertTrue(expected.getCause() instanceof BackendException); + assertNotNull(expected.getCause().getCause()); + } + } + } catch (Throwable e) { + failure.set(e); + } finally { + done.countDown(); + } + }); + contender.start(); + assertTrue(done.await(10, TimeUnit.SECONDS)); + if (failure.get() != null) { + throw new AssertionError(failure.get()); + } + assertEquals(0, data.list().length); + } + try (RecoveryLock retry = RocksDBSnapshotRestore.lock(data.toString())) { + assertTrue(retry.isOpen()); + } + } + + @Test + public void testGuardErrorTextCannotReuseCachedNativeOwner() throws Exception { + File data = this.temporary.newFolder("No locks available Column family not found"); + HugeConfig config = FakeObjects.newConfig(); + RocksDBSessions owner = new RocksDBStdSessions(config, "db", "store", + data.toString(), data.toString()); + RocksDBStore store = new RocksDBStore.RocksDBGraphStore(new RocksDBStoreProvider(), "db", "store"); + Map databases = Whitebox.getInternalState(store, "dbs"); + databases.put(data.toString(), owner); + Path guard = new File(data + ".resume-lock").toPath(); + Path held = new File(data + ".owned-lock").toPath(); + try { + owner.createTable("test"); + owner.session().put("test", new byte[]{1}, new byte[]{2}); + owner.session().commit(); + // Genuine contention can share the live owner's CFs. + RocksDBSessions copy = store.open(config, data.toString(), data.toString(), + Collections.singletonList("test")); + assertArrayEquals(new byte[]{2}, copy.session().get("test", new byte[]{1})); + copy.close(); + databases.put(data.toString(), owner); + + // Inject an IO failure, retaining the locked inode and live native owner. + Files.move(guard, held); + Files.createDirectory(guard); + try { + store.open(config, data.toString(), data.toString(), Collections.singletonList("test")); + fail("Path text must not classify a guard IO failure as contention or missing CF"); + } catch (ConnectionException expected) { + assertTrue(expected.getCause() instanceof RocksDBStdSessions.RecoveryLockException); + assertFalse(((RocksDBStdSessions.RecoveryLockException) expected.getCause()).isContention()); + assertTrue(expected.getCause().getCause() instanceof BackendException); + assertTrue(expected.getCause().getCause().getCause() instanceof IOException); + assertTrue(expected.getCause().getMessage().contains("No locks available")); + assertTrue(expected.getCause().getMessage().contains("Column family not found")); + assertTrue(owner.databaseOpened()); + assertArrayEquals(new byte[]{2}, owner.session().get("test", new byte[]{1})); + assertFalse(new File(data + ".resume-pending").exists()); + } + } finally { + if (Files.exists(held)) { + Files.delete(guard); + Files.move(held, guard); + } + owner.forceCloseRocksDB(); + } + } + + @Test + public void testLiveDatabasePreventsPendingRecovery() throws Exception { + File data = this.temporary.newFolder("data"); + File snapshot = new File(this.temporary.newFolder("snapshot-root"), "rocks"); + RocksDBSessions owner = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()); + try { + owner.createSnapshot(snapshot.toString()); + RocksDBSnapshotRestore restore = new RocksDBSnapshotRestore( + data.toString(), data.toString(), snapshot.toString(), + new RocksDBSnapshotRestore.FileOperations()); + restore.begin(); + byte[] manifest = Files.readAllBytes(new File(data, "CURRENT").toPath()); + try { + new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()); + fail("Must not restore under an open native owner"); + } catch (RocksDBException expected) { + assertTrue(expected.getMessage().contains("No locks available")); + assertEquals(Status.Code.IOError, expected.getStatus().getCode()); + assertArrayEquals(manifest, Files.readAllBytes(new File(data, "CURRENT").toPath())); + assertTrue(owner.databaseOpened()); + assertTrue(snapshot.isDirectory()); + } + } finally { + owner.forceCloseRocksDB(); + } + RocksDBSessions recovered = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()); + recovered.forceCloseRocksDB(); + assertFalse(new File(data + ".resume-pending").exists()); + } + + @Test + public void testRepeatedAdapterSnapshotResumePreservesConsumeSemantics() throws Exception { + File root = this.temporary.newFolder("adapter"); + // createCheckpoint's existing assertion requires a snapshot-prefixed parent. + File data = new File(root, "snapshot-data/store"); + File wal = new File(root, "wal"); + RocksDBSessions sessions = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), wal.toString()); + RocksDBStore store = new RocksDBStore.RocksDBGraphStore(new RocksDBStoreProvider(), "db", "store"); + Whitebox.setInternalState(store, "sessions", sessions); + Map databases = Whitebox.getInternalState(store, "dbs"); + databases.put(data.toString(), sessions); + byte[] key = new byte[]{1}; + try { + sessions.createTable("test"); + sessions.session().put("test", key, new byte[]{2}); + sessions.session().commit(); + store.createSnapshot("snapshot"); + String snapshot = sessions.buildSnapshotPath("snapshot"); + for (boolean consume : new boolean[]{false, false, true}) { + sessions.session().put("test", key, new byte[]{3}); + sessions.session().commit(); + store.resumeSnapshot("snapshot", consume); + assertArrayEquals(new byte[]{2}, sessions.session().get("test", key)); + assertEquals(!consume, new File(snapshot).exists()); + assertFalse(new File(snapshot + ".resume-lock").exists()); + assertEquals(!consume, new File(snapshot).getParentFile().exists()); + assertEquals(0, data.getParentFile().listFiles(file -> file.isDirectory() && + file.getName().startsWith("store_temp-")).length); + assertFalse(new File(data + ".resume-pending").exists()); + } + } finally { + sessions.close(); + } + } + + @Test + public void testMissingManifestRejectedBeforeDataMutation() throws Exception { + File data = this.temporary.newFolder("data"); + File snapshot = this.temporary.newFolder("snapshot"); + File sentinel = new File(data, "live"); + Files.write(sentinel.toPath(), new byte[]{42}); + try { + RocksDBSnapshotRestore.start(data.toString(), data.toString(), snapshot.toString()); + fail("Incomplete snapshot must not be installed"); + } catch (IOException expected) { + assertArrayEquals(new byte[]{42}, Files.readAllBytes(sentinel.toPath())); + assertFalse(new File(data + ".resume-pending").exists()); + } + } + + @Test + public void testNativeOpenFailureKeepsRecoverySourceAndMarker() throws Exception { + File data = this.temporary.newFolder("data"); + File snapshot = this.temporary.newFolder("snapshot"); + fakeCheckpoint(snapshot); + RocksDBSnapshotRestore.start(data.toString(), data.toString(), snapshot.toString()); + try { + new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", data.toString(), data.toString()); + fail("Invalid native manifest must not report success"); + } catch (org.rocksdb.RocksDBException expected) { + assertTrue(new File(data + ".resume-pending").isFile()); + assertArrayEquals(new byte[]{1, 2, 3}, + Files.readAllBytes(new File(snapshot, "MANIFEST-000001").toPath())); + } + } + + @Test + public void testMetadataChangesRejectBeforeRestoringMissingWalAlias() throws Exception { + for (String field : Arrays.asList("wal", "wal-link-target-0", "generation", "unexpected", "missing-wal")) { + File root = this.temporary.newFolder("metadata-" + field); + File data = new File(root, "data"); + File snapshot = new File(root, "snapshot"); + File target = new File(root, "wal-target"); + Files.createDirectories(data.toPath()); + Files.createDirectories(snapshot.toPath()); + Files.createDirectories(target.toPath()); + fakeCheckpoint(snapshot); + Path alias = new File(root, "wal-link").toPath(); + Files.createSymbolicLink(alias, target.toPath()); + Path sentinel = new File(data, "untouched").toPath(); + Files.write(sentinel, new byte[]{9}); + RocksDBSnapshotRestore.start(data.toString(), alias.toString(), snapshot.toString()); + Path marker = new File(data + ".resume-pending").toPath(); + Properties state = readRecoveryMetadata(marker); + if ("missing-wal".equals(field)) { + state.remove("wal"); + } else { + state.setProperty(field, "generation".equals(field) ? String.join("", Collections.nCopies(64, "a")) : + new File(root, "different-target").getCanonicalPath()); + } + writeRecoveryMetadata(marker, state); + byte[] damaged = Files.readAllBytes(marker); + Files.delete(alias); + try { + RocksDBSnapshotRestore.prepareOpen(data.toString(), alias.toString()); + fail("Changed metadata must fail before alias reconstruction: " + field); + } catch (BackendException expected) { + assertTrue(expected.getCause().getMessage().contains("metadata checksum")); + } + assertFalse(Files.exists(alias, java.nio.file.LinkOption.NOFOLLOW_LINKS)); + assertArrayEquals(new byte[]{9}, Files.readAllBytes(sentinel)); + assertArrayEquals(damaged, Files.readAllBytes(marker)); + assertTrue(snapshot.exists()); + } + } + + @Test + public void testValidMetadataCanRestoreMissingWalAlias() throws Exception { + File root = this.temporary.newFolder("metadata-valid-alias"); + File data = new File(root, "data"); + File snapshot = new File(root, "snapshot"); + File target = new File(root, "wal-target"); + Files.createDirectories(data.toPath()); + Files.createDirectories(snapshot.toPath()); + Files.createDirectories(target.toPath()); + fakeCheckpoint(snapshot); + Files.write(new File(snapshot, "000001.log").toPath(), new byte[]{5}); + Path alias = new File(root, "wal-link").toPath(); + Files.createSymbolicLink(alias, target.toPath()); + RocksDBSnapshotRestore.start(data.toString(), alias.toString(), snapshot.toString()); + Path marker = new File(data + ".resume-pending").toPath(); + // Re-serialization changes comments/order/escaping, not the property values. + writeRecoveryMetadata(marker, readRecoveryMetadata(marker)); + Files.delete(alias); + RocksDBSnapshotRestore.start(data.toString(), alias.toString(), snapshot.toString()); + RocksDBSnapshotRestore recovered = RocksDBSnapshotRestore.prepareOpen(data.toString(), alias.toString()); + assertNotNull(recovered); + assertTrue(Files.isSymbolicLink(alias)); + assertEquals(target.toPath().toRealPath(), alias.toRealPath()); + assertArrayEquals(new byte[]{5}, Files.readAllBytes(new File(target, "000001.log").toPath())); + assertTrue(snapshot.exists()); + assertTrue(Files.exists(marker)); + } + + @Test + public void testRecoveryKeepsFilesystemMeaningOfSymlinkThenParent() throws Exception { + File root = this.temporary.newFolder("metadata-symlink-parent"); + File data = new File(root, "data"); + File snapshot = new File(root, "snapshot"); + Path target = new File(root, "physical/child").toPath(); + Path realWal = target.getParent().resolve("logs"); + Path lexicalWal = new File(root, "logs").toPath(); + Files.createDirectories(data.toPath()); + Files.createDirectories(snapshot.toPath()); + Files.createDirectories(target); + Files.createDirectories(realWal); + Files.createDirectories(lexicalWal); + Path alias = new File(root, "link").toPath(); + Files.createSymbolicLink(alias, target); + Path configuredWal = alias.resolve("../logs"); + assertEquals(realWal.toRealPath(), configuredWal.toRealPath()); + assertFalse(configuredWal.toRealPath().equals(configuredWal.normalize().toRealPath())); + fakeCheckpoint(snapshot); + Files.write(new File(snapshot, "000001.log").toPath(), new byte[]{5}); + Files.write(realWal.resolve("000002.log"), new byte[]{6}); + Files.write(lexicalWal.resolve("000002.log"), new byte[]{7}); + RocksDBSnapshotRestore.start(data.toString(), configuredWal.toString(), snapshot.toString()); + RocksDBSnapshotRestore recovered = + RocksDBSnapshotRestore.prepareOpen(data.toString(), configuredWal.toString()); + assertNotNull(recovered); + assertArrayEquals(new byte[]{5}, Files.readAllBytes(realWal.resolve("000001.log"))); + assertFalse(Files.exists(realWal.resolve("000002.log"))); + assertArrayEquals(new byte[]{7}, Files.readAllBytes(lexicalWal.resolve("000002.log"))); + assertFalse(Files.exists(lexicalWal.resolve("000001.log"))); + } + + @Test + public void testExistingMarkerRequiresChecksumAndRetainsOperation() throws Exception { + File data = this.temporary.newFolder("metadata-existing-data"); + File snapshot = this.temporary.newFolder("metadata-existing-snapshot"); + fakeCheckpoint(snapshot); + RocksDBSnapshotRestore original = new RocksDBSnapshotRestore( + data.toString(), data.toString(), snapshot.toString(), new RocksDBSnapshotRestore.FileOperations()); + original.begin(); + Path marker = new File(data + ".resume-pending").toPath(); + byte[] valid = Files.readAllBytes(marker); + new RocksDBSnapshotRestore(data.toString(), data.toString(), snapshot.toString(), + new RocksDBSnapshotRestore.FileOperations()).begin(); + assertArrayEquals(valid, Files.readAllBytes(marker)); + Properties state = readRecoveryMetadata(marker); + state.remove("metadata-sha256"); + writeRecoveryMetadata(marker, state); + byte[] legacy = Files.readAllBytes(marker); + try { + new RocksDBSnapshotRestore(data.toString(), data.toString(), snapshot.toString(), + new RocksDBSnapshotRestore.FileOperations()).begin(); + fail("Existing legacy marker must not receive an invented checksum"); + } catch (IOException expected) { + assertTrue(expected.getMessage().contains("metadata checksum")); + } + try { + RocksDBSnapshotRestore.start(data.toString(), data.toString(), snapshot.toString()); + fail("Public retry must reject legacy marker"); + } catch (IOException expected) { + assertTrue(expected.getMessage().contains("metadata checksum")); + } + try { + RocksDBSnapshotRestore.prepareOpen(data.toString(), data.toString()); + fail("Opening must reject legacy marker"); + } catch (BackendException expected) { + assertTrue(expected.getCause().getMessage().contains("metadata checksum")); + } + assertArrayEquals(legacy, Files.readAllBytes(marker)); + assertTrue(snapshot.exists()); + } + + private static Properties readRecoveryMetadata(Path marker) throws IOException { + Properties state = new Properties(); + try (java.io.InputStream input = Files.newInputStream(marker)) { + state.load(input); + } + return state; + } + + private static void writeRecoveryMetadata(Path marker, Properties state) throws IOException { + try (java.io.OutputStream output = Files.newOutputStream(marker)) { + state.store(output, "re-serialized test metadata"); + } + } + + @Test + public void testInvalidOperationDoesNotReconstructWalAlias() throws Exception { + File data = this.temporary.newFolder("data"); + File snapshot = this.temporary.newFolder("snapshot"); + fakeCheckpoint(snapshot); + Path wal = new File(this.temporary.getRoot(), "wal-link").toPath(); + Files.createSymbolicLink(wal, this.temporary.newFolder("wal-target").toPath()); + RocksDBSnapshotRestore.start(data.toString(), wal.toString(), snapshot.toString()); + Path marker = new File(data + ".resume-pending").toPath(); + String metadata = Files.readString(marker); + Files.writeString(marker, metadata.replaceAll("operation=[^\n]+", "operation=invalid")); + Files.delete(wal); + try { + RocksDBSnapshotRestore.prepareOpen(data.toString(), wal.toString()); + fail("Invalid operation must fail before restoring any path"); + } catch (BackendException expected) { + assertFalse(Files.exists(wal, java.nio.file.LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.exists(marker)); + } + } + + @Test + public void testReplacedCheckpointGenerationIsRejectedBeforeTargetChanges() throws Exception { + File data = this.temporary.newFolder("data"); + File snapshot = this.temporary.newFolder("snapshot"); + fakeCheckpoint(snapshot); + Files.write(new File(data, "untouched").toPath(), new byte[]{9}); + RocksDBSnapshotRestore.start(data.toString(), data.toString(), snapshot.toString()); + File saved = new File(this.temporary.getRoot(), "saved-generation"); + Files.move(snapshot.toPath(), saved.toPath()); + FileUtils.copyDirectory(saved, snapshot); + Files.write(new File(snapshot, "MANIFEST-000001").toPath(), new byte[]{4, 5, 6}); + try { + RocksDBSnapshotRestore.prepareOpen(data.toString(), data.toString()); + fail("A replacement at the same path must not become the pending source"); + } catch (BackendException expected) { + assertArrayEquals(new byte[]{9}, Files.readAllBytes(new File(data, "untouched").toPath())); + assertTrue(new File(data + ".resume-pending").exists()); + assertTrue(saved.exists()); + } + } + + @Test + public void testPartialMarkerWriteLeavesNativeDataAndCheckpointRetryable() throws Exception { + File root = this.temporary.newFolder("partial-marker"); + File data = new File(root, "data"); + File wal = new File(root, "wal"); + File snapshot = new File(root, "snapshot"); + RocksDBStdSessions original = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), wal.toString()); + try { + original.createTable("test"); + original.session().put("test", new byte[]{1}, new byte[]{2}); + original.session().put("test", new byte[]{6}, new byte[]{7}); + original.session().commit(); + AtomicReference shared = Whitebox.getInternalState(original, "rocksdb"); + try (Checkpoint checkpoint = Checkpoint.create(shared.get().rocksdb())) { + checkpoint.createCheckpoint(snapshot.toString()); + } + original.session().put("test", new byte[]{1}, new byte[]{3}); + original.session().commit(); + } finally { + original.close(); + } + Map before = new HashMap<>(); + for (File directory : Arrays.asList(data, wal, snapshot)) { + try (java.util.stream.Stream paths = Files.walk(directory.toPath())) { + for (Path path : paths.filter(Files::isRegularFile).collect(java.util.stream.Collectors.toList())) { + before.put(path, Files.readAllBytes(path)); + } + } + } + runRecoveryProcess(data, wal, snapshot, "marker-partial", 0); + assertFalse(new File(data + ".resume-pending").exists()); + assertEquals(0, root.listFiles(file -> file.getName().startsWith("data.resume-pending.staging-")).length); + for (Map.Entry entry : before.entrySet()) { + assertArrayEquals(entry.getKey().toString(), entry.getValue(), Files.readAllBytes(entry.getKey())); + } + // With the IO fault gone, a fresh JVM opens the live value and retries the same checkpoint. + runRecoveryProcess(data, wal, snapshot, "marker-retry", 0); + runRecoveryProcess(data, wal, snapshot, "finish", 0); + } + + @Test + public void testKilledRestoreRetriesSameGenerationAndReclaimsOnlyOwnedStaging() throws Exception { + for (String phase : Arrays.asList("stage", "retire", "publish", "reopen")) { + File root = this.temporary.newFolder(phase); + File data = new File(root, "data"); + File wal = new File(root, "wal"); + File snapshot = new File(root, "snapshot"); + HugeConfig config = FakeObjects.newConfig(); + RocksDBStdSessions original = new RocksDBStdSessions(config, "db", "store", + data.toString(), wal.toString()); + try { + original.createTable("test"); + original.session().put("test", new byte[]{1}, new byte[]{2}); + original.session().commit(); + AtomicReference shared = Whitebox.getInternalState(original, "rocksdb"); + try (Checkpoint checkpoint = Checkpoint.create(shared.get().rocksdb())) { + // A native checkpoint flushes SSTs; append a captured real WAL tail below. + checkpoint.createCheckpoint(snapshot.toString()); + } + original.session().put("test", new byte[]{6}, new byte[]{7}); + original.session().commit(); + shared.get().rocksdb().flushWal(true); + File[] tails = wal.listFiles(file -> file.getName().matches("[0-9]+\\.log")); + assertNotNull(tails); + assertTrue("The native database must produce a real WAL tail", tails.length > 0); + for (File tail : tails) { + FileUtils.copyFile(tail, new File(snapshot, tail.getName())); + } + original.session().put("test", new byte[]{1}, new byte[]{3}); + original.session().commit(); + } finally { + original.close(); + } + File foreign = new File(data + ".resume-staging-foreign"); + FileUtils.forceMkdir(foreign); + Files.write(new File(foreign, "keep").toPath(), new byte[]{7}); + runRecoveryProcess(data, wal, snapshot, phase, 31); + assertTrue(new File(data + ".resume-pending").exists()); + // Interrupt the SAME operation again; no new staging namespace is allocated. + runRecoveryProcess(data, wal, snapshot, "reopen", 31); + runRecoveryProcess(data, wal, snapshot, "finish", 0); + assertFalse(new File(data + ".resume-pending").exists()); + assertFalse(snapshot.exists()); + assertArrayEquals(new byte[]{7}, Files.readAllBytes(new File(foreign, "keep").toPath())); + assertEquals(1, root.listFiles(file -> file.getName().startsWith("data.resume-staging-")).length); + } + } + + private static void runRecoveryProcess(File data, File wal, File snapshot, String phase, + int expectedExit) throws Exception { + String classpath = System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")); + File output = new File(data.getParentFile(), "child-" + phase + "-" + System.nanoTime() + ".log"); + Process child = new ProcessBuilder(new File(System.getProperty("java.home"), "bin/java").toString(), + "-cp", classpath, RocksDBSnapshotRestoreTest.class.getName(), + data.toString(), wal.toString(), snapshot.toString(), phase) + .redirectErrorStream(true).redirectOutput(output).start(); + try { + assertTrue("Recovery child timed out: " + output, child.waitFor(30, TimeUnit.SECONDS)); + assertEquals(Files.readString(output.toPath()), expectedExit, child.exitValue()); + } finally { + child.destroyForcibly(); + } + } + + private static void recoverChild(String[] args) throws Exception { + String phase = args[3]; + if ("marker-partial".equals(phase)) { + try (RecoveryLock held = RocksDBSnapshotRestore.lock(args[0])) { + RocksDBSnapshotRestore restore = new RocksDBSnapshotRestore(args[0], args[1], args[2], + new RocksDBSnapshotRestore.FileOperations() { + @Override + OutputStream markerOutput(Path staged) throws IOException { + return new FilterOutputStream(super.markerOutput(staged)) { + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + // Commit real bytes, then fail while Properties.store is still flushing. + this.out.write(bytes, offset, Math.min(32, length)); + this.out.flush(); + assertTrue(Files.size(staged) > 0); + throw new IOException("injected partial marker write"); + } + }; + } + }); + try { + restore.begin(); + fail("Partial marker IO must fail before installing data"); + } catch (IOException expected) { + assertEquals("injected partial marker write", expected.getMessage()); + } + } + return; + } + if ("marker-retry".equals(phase)) { + RocksDBStdSessions fresh = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + args[0], args[1], Collections.singletonList("test")); + try { + assertArrayEquals(new byte[]{3}, fresh.session().get("test", new byte[]{1})); + fresh.resumeSnapshot(args[2]); + assertArrayEquals(new byte[]{2}, fresh.session().get("test", new byte[]{1})); + assertArrayEquals(new byte[]{7}, fresh.session().get("test", new byte[]{6})); + } finally { + fresh.close(); + } + return; + } + if ("finish".equals(phase)) { + RocksDBStdSessions fresh = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + args[0], args[1], Collections.singletonList("test")); + try { + assertArrayEquals(new byte[]{2}, fresh.session().get("test", new byte[]{1})); + assertArrayEquals(new byte[]{7}, fresh.session().get("test", new byte[]{6})); + fresh.session().put("test", new byte[]{4}, new byte[]{5}); + fresh.session().commit(); + assertArrayEquals(new byte[]{5}, fresh.session().get("test", new byte[]{4})); + } finally { + fresh.close(); + } + return; + } + try (RecoveryLock held = RocksDBSnapshotRestore.lock(args[0])) { + RocksDBSnapshotRestore.start(args[0], args[1], args[2]); + if ("reopen".equals(phase)) { + RocksDBSnapshotRestore.prepareOpen(args[0], args[1]); + } else { + RocksDBSnapshotRestore restore = new RocksDBSnapshotRestore(args[0], args[1], args[2], + new RocksDBSnapshotRestore.FileOperations() { + @Override + void copyFile(File source, File target) throws IOException { + super.copyFile(source, target); + if ("stage".equals(phase)) { + Runtime.getRuntime().halt(31); + } + } + + @Override + void move(Path source, Path target) throws IOException { + super.move(source, target); + if (("retire".equals(phase) && target.toString().endsWith(".aside")) || + ("publish".equals(phase) && target.toString().endsWith(".log"))) { + Runtime.getRuntime().halt(31); + } + } + }); + restore.begin(); + restore.install(); + throw new AssertionError("Did not reach requested kill boundary: " + phase); + } + // Halt bypasses finally and native reopening: this is a process interruption. + Runtime.getRuntime().halt(31); + } + } + + @Test + public void testRecoveryLockExcludesAnotherProcess() throws Exception { + File data = this.temporary.newFolder("data"); + String classpath = System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")); + try (RecoveryLock owner = RocksDBSnapshotRestore.lock(data.toString())) { + Process child = new ProcessBuilder(new File(System.getProperty("java.home"), "bin/java").toString(), + "-cp", classpath, getClass().getName(), data.toString()) + .redirectErrorStream(true).start(); + try { + assertTrue("Competing process did not finish", child.waitFor(10, TimeUnit.SECONDS)); + String output = new String(child.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + assertEquals(output, 23, child.exitValue()); + assertEquals(0, data.list().length); + } finally { + child.destroyForcibly(); + } + } + } + + @Test + public void testExternalOwnerRejectsBothPublicOpenPaths() throws Exception { + File data = this.temporary.newFolder("external-owner"); + File ready = new File(this.temporary.getRoot(), "owner-ready"); + String classpath = System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")); + Process child = new ProcessBuilder(new File(System.getProperty("java.home"), "bin/java").toString(), + "-cp", classpath, getClass().getName(), data.toString(), + ready.toString()).redirectErrorStream(true).start(); + try { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while (!ready.exists() && child.isAlive() && System.nanoTime() < deadline) { + Thread.sleep(10); + } + assertTrue("External owner did not acquire lock", ready.exists()); + for (boolean withTables : new boolean[]{false, true}) { + try { + if (withTables) { + new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString(), Collections.emptyList()); + } else { + new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()); + } + fail("Public open must reject a competing process before native recovery"); + } catch (RocksDBException expected) { + assertTrue(expected instanceof RocksDBStdSessions.RecoveryLockException); + assertTrue(((RocksDBStdSessions.RecoveryLockException) expected).isContention()); + assertEquals(Status.Code.IOError, expected.getStatus().getCode()); + assertTrue(expected.getMessage().contains("No locks available")); + assertEquals(0, data.list().length); + } + } + child.getOutputStream().write('\n'); + child.getOutputStream().flush(); + assertTrue("External owner did not close", child.waitFor(10, TimeUnit.SECONDS)); + assertEquals(0, child.exitValue()); + } finally { + child.destroyForcibly(); + } + try (RecoveryLock retry = RocksDBSnapshotRestore.lock(data.toString())) { + assertTrue(retry.isOpen()); + } + } + + public static void main(String[] args) { + if (args.length == 4) { + try { + recoverChild(args); + return; + } catch (Exception e) { + throw new AssertionError(e); + } + } + try (RecoveryLock channel = RocksDBSnapshotRestore.lock(args[0])) { + if (args.length == 2) { + Files.write(new File(args[1]).toPath(), new byte[]{1}); + System.in.read(); + } else { + System.out.println("acquired"); + } + } catch (BackendException expected) { + System.exit(23); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + @Test + public void testCachedOwnerCopyCannotOverlapNativeReload() throws Exception { + File data = this.temporary.newFolder("copy-reload"); + CountDownLatch checked = new CountDownLatch(1); + CountDownLatch allowCopy = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + AtomicReference copied = new AtomicReference<>(); + RocksDBStdSessions owner = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()) { + @Override + public boolean databaseOpened() { + boolean opened = super.databaseOpened(); + checked.countDown(); + try { + assertTrue("Copy check was not released", allowCopy.await(10, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } + return opened; + } + + @Override + public RocksDBSessions copy(HugeConfig config, String database, String store) { + assertTrue("Native owner closed between its open check and copy", super.databaseOpened()); + return super.copy(config, database, store); + } + }; + RocksDBStore store = new RocksDBStore.RocksDBGraphStore(new RocksDBStoreProvider(), "db", "store"); + Map databases = Whitebox.getInternalState(store, "dbs"); + databases.put(data.toString(), owner); + Thread open = new Thread(() -> { + try { + copied.set(store.open(FakeObjects.newConfig(), data.toString(), data.toString(), + Collections.singletonList("test"))); + } catch (Throwable e) { + failure.compareAndSet(null, e); + } + }, "cached-owner-copy"); + Thread reload = new Thread(() -> { + try { + owner.reloadRocksDB(); + } catch (Throwable e) { + failure.compareAndSet(null, e); + } + }, "cached-owner-reload"); + try { + owner.createTable("test"); + owner.session().put("test", new byte[]{1}, new byte[]{2}); + owner.session().commit(); + open.start(); + assertTrue("Open did not reach the cached owner check", checked.await(10, TimeUnit.SECONDS)); + reload.start(); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while (reload.getState() != Thread.State.BLOCKED && reload.isAlive() && + System.nanoTime() < deadline) { + Thread.sleep(10); + } + assertEquals("Native reload must wait for the owner check and copy", Thread.State.BLOCKED, + reload.getState()); + allowCopy.countDown(); + open.join(10000); + reload.join(10000); + assertFalse(open.isAlive()); + assertFalse(reload.isAlive()); + if (failure.get() != null) { + throw new AssertionError(failure.get()); + } + assertNotNull(copied.get()); + assertArrayEquals(new byte[]{2}, owner.session().get("test", new byte[]{1})); + } finally { + allowCopy.countDown(); + open.join(10000); + reload.join(10000); + owner.forceCloseRocksDB(); + } + } + + @Test + public void testNativeCloseToRestoreTransitionRetainsSameLock() throws Exception { + File data = this.temporary.newFolder("data"); + File snapshot = new File(this.temporary.newFolder("snapshot-root"), "rocks"); + RocksDBSessions sessions = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()); + RecoveryLock transferred = null; + try { + sessions.createTable("test"); + sessions.session().put("test", new byte[]{1}, new byte[]{2}); + sessions.session().commit(); + sessions.createSnapshot(snapshot.toString()); + AtomicReference shared = Whitebox.getInternalState(sessions, "rocksdb"); + OpenedRocksDB old = shared.get(); + transferred = old.closeForRestore(); + RecoveryLock held = transferred; + assertFalse(old.isOwningHandle()); + assertTrue(held.isOpen()); + RocksDBStore store = new RocksDBStore.RocksDBGraphStore(new RocksDBStoreProvider(), "db", "store"); + Map databases = Whitebox.getInternalState(store, "dbs"); + databases.put(data.toString(), sessions); + try { + store.open(FakeObjects.newConfig(), data.toString(), data.toString(), + Collections.singletonList("test")); + fail("Cached owner must not be copied while native close/restore is in progress"); + } catch (ConnectionException expected) { + assertFalse(sessions.databaseOpened()); + assertTrue(held.isOpen()); + } + // Deterministic observation of the exact native-closed/marker-not-yet-created window. + try { + new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()); + fail("Native close must not release recovery ownership"); + } catch (RocksDBException expected) { + assertTrue(expected.getMessage().contains("No locks available")); + assertEquals(Status.Code.IOError, expected.getStatus().getCode()); + assertFalse(new File(data + ".resume-pending").exists()); + } + // Native LOCK is gone here: the recovery lease alone must still exclude another JVM, + // including after the two same-JVM contender paths above have failed. + assertExternalLock(data.toString(), 23); + RocksDBSnapshotRestore.start(data.toString(), data.toString(), snapshot.toString()); + old.close(); + assertTrue("Closing the detached old handle must retain the transferred lock", held.isOpen()); + OpenedRocksDB replacement = Whitebox.invokeStatic( + RocksDBStdSessions.class, + new Class[]{HugeConfig.class, List.class, String.class, String.class, RecoveryLock.class}, + "openRocksDB", FakeObjects.newConfig(), Collections.emptyList(), + data.toString(), data.toString(), held); + shared.set(replacement); + assertSame(held, Whitebox.getInternalState(replacement, "recoveryLock")); + assertArrayEquals(new byte[]{2}, sessions.session().get("test", new byte[]{1})); + } finally { + try { + sessions.close(); + } finally { + RocksDBSnapshotRestore.unlock(transferred); + } + } + try (RecoveryLock available = RocksDBSnapshotRestore.lock(data.toString())) { + assertTrue(available.isOpen()); + } + } + + @Test + public void testSameJvmAliasesDoNotReleaseIndependentProcessLock() throws Exception { + this.checkSameJvmAliases(false); + } + + @Test + public void testMissingFileKeyUsesPhysicalIdentityWithoutOpeningAnotherDescriptor() throws Exception { + this.checkSameJvmAliases(true); + } + + private void checkSameJvmAliases(boolean withoutFileKey) throws Exception { + File parent = this.temporary.newFolder(); + File data = new File(parent, "data"); + Files.createDirectory(data.toPath()); + Path symlinkParent = new File(this.temporary.getRoot(), "symlink-" + parent.getName()).toPath(); + Files.createSymbolicLink(symlinkParent, parent.toPath()); + File hardlinkData = this.temporary.newFolder(); + List aliases = Arrays.asList(new File(parent, "./data").toString(), + symlinkParent.resolve("data").toString(), + hardlinkData.toString()); + try (RecoveryLock owner = RocksDBSnapshotRestore.lock(data.toString())) { + if (withoutFileKey) { + // The JDK permits null keys; keep the actual file/channel and exercise isSameFile. + Whitebox.setInternalState(owner, "identity", null); + } + Files.createLink(new File(hardlinkData + ".resume-lock").toPath(), + new File(data + ".resume-lock").toPath()); + for (String alias : aliases) { + assertSameJvmContended(alias); + assertTrue(owner.isOpen()); + assertExternalLock(alias, 23); + } + assertEquals(0, data.list().length); + assertEquals(0, hardlinkData.list().length); + } + for (String alias : aliases) { + assertExternalLock(alias, 0); + } + } + + /** Explicit external Docker/kernel fixture: bind the same parent at both paths. */ + @Test + public void testParentBindAliasRetainsIndependentProcessExclusion() throws Exception { + String original = System.getProperty("hugegraph.test.recovery.parent.original"); + String alias = System.getProperty("hugegraph.test.recovery.parent.alias"); + org.junit.Assume.assumeTrue("requires the explicit parent-bind kernel fixture", + original != null && alias != null); + Path data = new File(original, "data").toPath(); + Files.createDirectories(data); + String aliasData = new File(alias, "data").toString(); + try (RecoveryLock owner = RocksDBSnapshotRestore.lock(data.toString())) { + assertSameJvmContended(aliasData); + assertTrue(owner.isOpen()); + assertExternalLock(aliasData, 23); + } + assertExternalLock(aliasData, 0); + } + + private static void assertSameJvmContended(String data) { + try (RecoveryLock contender = RocksDBSnapshotRestore.lock(data)) { + fail("The physical file already has a same-JVM owner: " + data); + } catch (BackendException expected) { + assertTrue(expected.getCause() instanceof OverlappingFileLockException); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + private static void assertExternalLock(String data, int expectedExit) throws Exception { + String classpath = System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")); + Process child = new ProcessBuilder(new File(System.getProperty("java.home"), "bin/java").toString(), + "-cp", classpath, RocksDBSnapshotRestoreTest.class.getName(), data) + .redirectErrorStream(true).start(); + try { + assertTrue("Independent JVM did not finish", child.waitFor(10, TimeUnit.SECONDS)); + String output = new String(child.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + assertEquals(output, expectedExit, child.exitValue()); + } finally { + child.destroyForcibly(); + assertTrue("Independent JVM did not stop", child.waitFor(10, TimeUnit.SECONDS)); + } + } + + @Test + public void testCopiesShareReloadedNativeOwnerAndLock() throws Exception { + this.checkSharedReplacement(false); + } + + @Test + public void testCopiesShareRestoredNativeOwnerAndLock() throws Exception { + this.checkSharedReplacement(true); + } + + private void checkSharedReplacement(boolean restore) throws Exception { + for (boolean replaceThroughCopy : new boolean[]{false, true}) { + for (boolean closeReplacementFirst : new boolean[]{false, true}) { + File data = this.temporary.newFolder(); + File snapshot = new File(this.temporary.newFolder("snapshot-" + data.getName()), "rocks"); + HugeConfig config = FakeObjects.newConfig(); + RocksDBSessions owner = new RocksDBStdSessions(config, "db", "store", + data.toString(), data.toString()); + RocksDBSessions copy = null; + try { + owner.createTable("test"); + owner.session().put("test", new byte[]{1}, new byte[]{2}); + owner.session().commit(); + copy = owner.copy(config, "db", "copy"); + assertArrayEquals(new byte[]{2}, copy.session().get("test", new byte[]{1})); + AtomicReference shared = Whitebox.getInternalState(owner, "rocksdb"); + assertSame(shared, Whitebox.getInternalState(copy, "rocksdb")); + OpenedRocksDB old = shared.get(); + RecoveryLock held = Whitebox.getInternalState(old, "recoveryLock"); + RocksDBSessions replacing = replaceThroughCopy ? copy : owner; + RocksDBSessions other = replaceThroughCopy ? owner : copy; + if (restore) { + owner.createSnapshot(snapshot.toString()); + owner.session().put("test", new byte[]{1}, new byte[]{3}); + owner.session().commit(); + replacing.resumeSnapshot(snapshot.toString()); + } else { + replacing.reloadRocksDB(); + } + OpenedRocksDB replacement = shared.get(); + assertFalse(old.isOwningHandle()); + assertSame(held, Whitebox.getInternalState(replacement, "recoveryLock")); + assertArrayEquals(new byte[]{2}, owner.session().get("test", new byte[]{1})); + assertArrayEquals(new byte[]{2}, copy.session().get("test", new byte[]{1})); + old.close(); + assertTrue("The old native wrapper must relinquish its lock", held.isOpen()); + + RocksDBSessions first = closeReplacementFirst ? replacing : other; + RocksDBSessions last = closeReplacementFirst ? other : replacing; + first.close(); + assertTrue(last.databaseOpened()); + assertTrue(replacement.isOwningHandle()); + assertTrue(held.isOpen()); + assertArrayEquals(new byte[]{2}, last.session().get("test", new byte[]{1})); + // A pending opener must fail before replacing files under the live last copy. + last.createSnapshot(snapshot.toString()); + RocksDBSnapshotRestore.start(data.toString(), data.toString(), snapshot.toString()); + byte[] current = Files.readAllBytes(new File(data, "CURRENT").toPath()); + try { + new RocksDBStdSessions(config, "db", "contender", data.toString(), data.toString()); + fail("The last shared native owner must retain recovery exclusion"); + } catch (RocksDBStdSessions.RecoveryLockException expected) { + assertTrue(expected.isContention()); + assertArrayEquals(current, Files.readAllBytes(new File(data, "CURRENT").toPath())); + assertTrue(new File(data + ".resume-pending").exists()); + assertTrue(snapshot.isDirectory()); + } + last.close(); + assertFalse(replacement.isOwningHandle()); + assertFalse(held.isOpen()); + RocksDBSessions fresh = new RocksDBStdSessions(config, "db", "fresh", + data.toString(), data.toString(), + Collections.emptyList()); + try { + assertArrayEquals(new byte[]{2}, fresh.session().get("test", new byte[]{1})); + assertFalse(new File(data + ".resume-pending").exists()); + assertFalse(snapshot.exists()); + } finally { + fresh.forceCloseRocksDB(); + } + } finally { + owner.forceCloseRocksDB(); + if (copy != null) { + copy.forceCloseRocksDB(); + } + } + } + } + } + + @Test + public void testForceCloseReleasesNativeAndRecoveryOwnership() throws Exception { + File data = this.temporary.newFolder("data"); + RocksDBSessions sessions = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()); + sessions.forceCloseRocksDB(); + assertFalse(sessions.databaseOpened()); + RocksDBSessions fresh = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + data.toString(), data.toString()); + fresh.forceCloseRocksDB(); + } + + @Test + public void testRootAndLinuxMountPointRejectedBeforeCreatingRecoveryLock() { + try { + RocksDBSnapshotRestore.lock(File.listRoots()[0].toString()); + fail("Filesystem root must not be a store directory"); + } catch (BackendException expected) { + assertTrue(expected.getCause().getMessage().contains("filesystem root")); + } + if (System.getProperty("os.name").startsWith("Linux")) { + Path lock = new File("/proc.resume-lock").toPath(); + assertFalse(Files.exists(lock)); + try { + RocksDBSnapshotRestore.lock("/proc"); + fail("Linux mount point must not be a store directory"); + } catch (BackendException expected) { + assertTrue(expected.getCause().getMessage().contains("mount point")); + } + assertFalse(Files.exists(lock)); + } + } + + @Test + public void testDescriptorCloseRetainsAliasesWithoutBlockingOtherDatabases() throws Exception { + File data = this.temporary.newFolder("closing-data"); + File unrelated = this.temporary.newFolder("unrelated-data"); + RecoveryLock owner = RocksDBSnapshotRestore.lock(data.toString()); + Path alias = this.temporary.newFolder("alias-data").toPath(); + Files.createLink(new File(alias + ".resume-lock").toPath(), new File(data + ".resume-lock").toPath()); + ClosingChannel channel = new ClosingChannel(Whitebox.getInternalState(owner, "channel"), null, true); + Whitebox.setInternalState(owner, "channel", channel); + FutureTask close = new FutureTask<>(() -> { owner.close(); return null; }); + Thread closer = new Thread(close, "descriptor-close-test"); + FutureTask probe = new FutureTask<>(() -> { + assertSameJvmContended(alias.toString()); + try (RecoveryLock other = RocksDBSnapshotRestore.lock(unrelated.toString())) { + assertTrue(other.isOpen()); + } + return null; + }); + Thread prober = new Thread(probe, "unrelated-open-test"); + try { + closer.start(); + assertTrue(channel.entered.await(10, TimeUnit.SECONDS)); + assertFalse("JDK clears isOpen before the descriptor is closed", channel.isOpen()); + prober.start(); + probe.get(10, TimeUnit.SECONDS); + assertExternalLock(alias.toString(), 23); + } finally { + channel.release.countDown(); + closer.join(10000L); + prober.join(10000L); + owner.close(); + } + close.get(10, TimeUnit.SECONDS); + owner.close(); + assertEquals(1, channel.closes); + assertExternalLock(alias.toString(), 0); + } + + @Test + public void testDescriptorIOExceptionCannotReleaseReservationOnRepeatedClose() throws Exception { + this.checkDescriptorCloseFailure(new IOException("injected descriptor close")); + } + + @Test + public void testDescriptorRuntimeExceptionCannotReleaseReservationOnRepeatedClose() throws Exception { + this.checkDescriptorCloseFailure(new IllegalStateException("injected descriptor close")); + } + + private void checkDescriptorCloseFailure(Exception failure) throws Exception { + File data = this.temporary.newFolder(); + RecoveryLock owner = RocksDBSnapshotRestore.lock(data.toString()); + ClosingChannel channel = new ClosingChannel(Whitebox.getInternalState(owner, "channel"), failure, false); + Whitebox.setInternalState(owner, "channel", channel); + Path alias = this.temporary.newFolder().toPath(); + Files.createLink(new File(alias + ".resume-lock").toPath(), new File(data + ".resume-lock").toPath()); + try { + for (int i = 0; i < 2; i++) { + try { + owner.close(); + fail("Failed close must remain failed"); + } catch (IOException | RuntimeException expected) { + assertSame(failure, expected); + } + assertSameJvmContended(alias.toString()); + assertExternalLock(alias.toString(), 23); + } + assertEquals(1, channel.closes); + RuntimeException primary = new IllegalStateException("open failed"); + RocksDBSnapshotRestore.unlock(owner, primary); + assertEquals(1, primary.getSuppressed().length); + Throwable suppressed = primary.getSuppressed()[0]; + assertSame(failure, failure instanceof IOException ? suppressed.getCause() : suppressed); + } finally { + // The injected failure intentionally left the real descriptor open. + // Only the fixture, after real close, may remove its failed reservation. + channel.delegate.close(); + Set owners = Whitebox.getInternalState(RecoveryLock.class, "OWNERS"); + synchronized (owners) { + owners.remove(owner); + } + } + assertExternalLock(alias.toString(), 0); + } + + @Test + public void testNativeDisposalBlocksCloseAndRetainsLeaseThroughTransfer() throws Exception { + File data = this.temporary.newFolder(); + RecoveryLock owner = RocksDBSnapshotRestore.lock(data.toString()); + ClosingRocksDB rocksdb = new ClosingRocksDB(data.toString(), null, true); + OpenedRocksDB opened = new OpenedRocksDB(rocksdb, new HashMap<>(), null, owner); + FutureTask transfer = new FutureTask<>(opened::closeForRestore); + FutureTask close = new FutureTask<>(() -> { opened.close(); return null; }); + Thread transferring = new Thread(transfer, "native-transfer-test"); + Thread closing = new Thread(close, "native-close-test"); + try { + transferring.start(); + assertTrue(rocksdb.entered.await(10, TimeUnit.SECONDS)); + assertFalse("JNI ownership flips before native disposal finishes", opened.isOwningHandle()); + assertSameJvmContended(data.toString()); + assertExternalLock(data.toString(), 23); + closing.start(); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while (closing.getState() != Thread.State.BLOCKED && System.nanoTime() < deadline) { + Thread.sleep(10L); + } + assertEquals("Concurrent close must reach the owner monitor", Thread.State.BLOCKED, closing.getState()); + try { + close.get(100, TimeUnit.MILLISECONDS); + fail("Concurrent close must wait for native disposal and lease transfer"); + } catch (TimeoutException expected) { + assertTrue(owner.isOpen()); + } + rocksdb.release.countDown(); + assertSame(owner, transfer.get(10, TimeUnit.SECONDS)); + close.get(10, TimeUnit.SECONDS); + assertTrue(owner.isOpen()); + assertExternalLock(data.toString(), 23); + } finally { + rocksdb.release.countDown(); + transferring.join(10000L); + closing.join(10000L); + opened.close(); + owner.close(); + } + } + + @Test + public void testNativeDisposalFailureCannotReleaseOrTransferLease() throws Exception { + File data = this.temporary.newFolder(); + RuntimeException failure = new IllegalStateException("injected native disposal failure"); + RecoveryLock owner = RocksDBSnapshotRestore.lock(data.toString()); + ClosingRocksDB rocksdb = new ClosingRocksDB(data.toString(), failure, false); + OpenedRocksDB opened = new OpenedRocksDB(rocksdb, new HashMap<>(), null, owner); + try { + for (Runnable operation : Arrays.asList(opened::close, opened::close, opened::closeForRestore)) { + try { + operation.run(); + fail("Native close failure must remain failed"); + } catch (RuntimeException expected) { + assertSame(failure, expected); + } + assertFalse(opened.isOwningHandle()); + assertTrue(owner.isOpen()); + assertSameJvmContended(data.toString()); + assertExternalLock(data.toString(), 23); + } + assertEquals(1, rocksdb.disposals); + RuntimeException primary = new IllegalStateException("marker completion failed"); + Whitebox.invokeStatic(RocksDBStdSessions.class, + new Class[]{OpenedRocksDB.class, RecoveryLock.class, Throwable.class}, + "closeFailedOpen", opened, owner, primary); + assertSame(failure, primary.getSuppressed()[0]); + assertTrue(owner.isOpen()); + } finally { + rocksdb.finishDisposal(); + owner.close(); + } + } + + private static void awaitClose(CountDownLatch release) throws IOException { + try { + if (!release.await(10, TimeUnit.SECONDS)) { + throw new IOException("Timed out waiting for injected close"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e); + } + } + + private static class ClosingRocksDB extends RocksDB { + + private final RuntimeException failure; + private final CountDownLatch entered = new CountDownLatch(1); + private final CountDownLatch release; + private int disposals; + + ClosingRocksDB(String data, RuntimeException failure, boolean block) throws RocksDBException { + super(openHandle(data)); + this.failure = failure; + this.release = new CountDownLatch(block ? 1 : 0); + } + + private static long openHandle(String data) throws RocksDBException { + RocksDB.loadLibrary(); + try (Options options = new Options().setCreateIfMissing(true)) { + RocksDB original = RocksDB.open(options, data); + Whitebox.invoke(RocksDB.class, "disOwnNativeHandle", original); + return original.getNativeHandle(); + } + } + + @Override + protected void disposeInternal(long handle) { + this.disposals++; + this.entered.countDown(); + try { + awaitClose(this.release); + } catch (IOException e) { + throw new IllegalStateException(e); + } + if (this.failure != null) { + throw this.failure; + } + super.disposeInternal(handle); + } + + void finishDisposal() { + super.disposeInternal(this.nativeHandle_); + } + } + + private static class ClosingChannel extends FileChannel { + + private final FileChannel delegate; + private final Exception failure; + private final CountDownLatch entered = new CountDownLatch(1); + private final CountDownLatch release; + private int closes; + + ClosingChannel(FileChannel delegate, Exception failure, boolean block) { + this.delegate = delegate; + this.failure = failure; + this.release = new CountDownLatch(block ? 1 : 0); + } + + @Override + protected void implCloseChannel() throws IOException { + this.closes++; + this.entered.countDown(); + awaitClose(this.release); + if (this.failure instanceof IOException) { + throw (IOException) this.failure; + } + if (this.failure instanceof RuntimeException) { + throw (RuntimeException) this.failure; + } + this.delegate.close(); + } + + @Override + public int read(ByteBuffer dst) throws IOException { + return this.delegate.read(dst); + } + + @Override + public long read(ByteBuffer[] dst, int offset, int length) throws IOException { + return this.delegate.read(dst, offset, length); + } + + @Override + public int write(ByteBuffer src) throws IOException { + return this.delegate.write(src); + } + + @Override + public long write(ByteBuffer[] src, int offset, int length) throws IOException { + return this.delegate.write(src, offset, length); + } + + @Override + public long position() throws IOException { + return this.delegate.position(); + } + + @Override + public FileChannel position(long position) throws IOException { + return this.delegate.position(position); + } + + @Override + public long size() throws IOException { + return this.delegate.size(); + } + + @Override + public FileChannel truncate(long size) throws IOException { + return this.delegate.truncate(size); + } + + @Override + public void force(boolean metadata) throws IOException { + this.delegate.force(metadata); + } + + @Override + public long transferTo(long position, long count, WritableByteChannel target) throws IOException { + return this.delegate.transferTo(position, count, target); + } + + @Override + public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException { + return this.delegate.transferFrom(src, position, count); + } + + @Override + public int read(ByteBuffer dst, long position) throws IOException { + return this.delegate.read(dst, position); + } + + @Override + public int write(ByteBuffer src, long position) throws IOException { + return this.delegate.write(src, position); + } + + @Override + public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException { + return this.delegate.map(mode, position, size); + } + + @Override + public FileLock lock(long position, long size, boolean shared) throws IOException { + return this.delegate.lock(position, size, shared); + } + + @Override + public FileLock tryLock(long position, long size, boolean shared) throws IOException { + return this.delegate.tryLock(position, size, shared); + } + } + + private static class FaultyFiles extends RocksDBSnapshotRestore.FileOperations { + + private final String fault; + + FaultyFiles(String fault) { + this.fault = fault; + } + + @Override + void copyDirectory(File source, File target) throws IOException { + if ("data-copy".equals(this.fault)) { + throw new IOException("injected data copy failure"); + } + super.copyDirectory(source, target); + if ("corrupt-data-copy".equals(this.fault)) { + Files.write(new File(target, "MANIFEST-000001").toPath(), new byte[]{3, 2, 1}); + } + } + + @Override + void copyFile(File source, File target) throws IOException { + if ("copy".equals(this.fault)) { + throw new IOException("injected copy failure"); + } + super.copyFile(source, target); + if ("short-copy".equals(this.fault)) { + Files.write(target.toPath(), new byte[]{0}); + } else if ("corrupt-copy".equals(this.fault)) { + Files.write(target.toPath(), new byte[(int) target.length()]); + } + } + + @Override + void move(Path source, Path target) throws IOException { + if (("retire".equals(this.fault) && target.toString().endsWith(".aside")) || + ("publish".equals(this.fault) && target.toString().endsWith(".log"))) { + throw new IOException("injected " + this.fault + " failure"); + } + super.move(source, target); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java index 65940d3e3a..62325f0ae9 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java @@ -17,12 +17,44 @@ package org.apache.hugegraph.backend.tx; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.HugeGraphParams; +import org.apache.hugegraph.analyzer.Analyzer; +import org.apache.hugegraph.backend.serializer.AbstractSerializer; +import org.apache.hugegraph.backend.store.BackendEntry; +import org.apache.hugegraph.backend.store.BackendStore; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.HugeVertex; +import org.apache.hugegraph.structure.HugeEdge; +import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.structure.builder.IndexBuilder; import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.unit.FakeObjects; import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.IdStrategy; import org.apache.hugegraph.type.define.IndexType; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.EdgeLabelType; +import org.apache.hugegraph.type.define.Frequency; +import org.apache.hugegraph.type.define.WriteType; import org.junit.Test; +import org.mockito.Mockito; public class GraphIndexTransactionTest { @@ -46,4 +78,181 @@ public void testKeepBackendIndexOrderOnlyForOrderedHstoreRangeQuery() { Assert.assertTrue(GraphIndexTransaction.keepBackendIndexOrder( true, IndexType.RANGE_INT, query)); } + + @Test + public void testEngineTransactionAndStoreBuilderShareExpansionForEveryIndexKind() { + FakeObjects objects = new FakeObjects(); + HugeGraph graph = objects.graph(); + PropertyKey text = objects.newPropertyKey(IdGenerator.of(1), "text", DataType.TEXT); + PropertyKey number = objects.newPropertyKey(IdGenerator.of(2), "number", DataType.INT); + VertexLabel vertexLabel = objects.newVertexLabel(IdGenerator.of(4), "vertex", + IdStrategy.CUSTOMIZE_NUMBER, text.id(), number.id()); + HugeVertex vertex = new HugeVertex(graph, IdGenerator.of(1), vertexLabel); + vertex.addProperty(text, "alpha beta"); + vertex.addProperty(number, 7); + vertex.expiredTime(123L); + Analyzer analyzer = value -> Set.of("alpha", "beta"); + int value = 10; + for (IndexType type : List.of(IndexType.SECONDARY, IndexType.SEARCH, IndexType.SHARD, + IndexType.UNIQUE, IndexType.RANGE_INT, IndexType.RANGE_FLOAT, + IndexType.RANGE_LONG, IndexType.RANGE_DOUBLE)) { + PropertyKey field = type.isRange() ? number : text; + IndexLabel label = objects.newIndexLabel(IdGenerator.of(value++), type.name(), + HugeType.VERTEX_LABEL, vertexLabel.id(), type, field.id()); + List expected = new IndexBuilder(graph, analyzer).buildIndex(vertex.element(), label); + List actual = captureEngineExpansion(objects, vertex, label, analyzer); + Assert.assertEquals(type.name(), expected.size(), actual.size()); + for (int i = 0; i < expected.size(); i++) { + Assert.assertEquals(type.name(), expected.get(i).fieldValues(), actual.get(i).fieldValues()); + Assert.assertEquals(vertex.id(), actual.get(i).elementId()); + Assert.assertEquals(123L, actual.get(i).expiredTime()); + } + if (type == IndexType.SEARCH) { + Set values = new HashSet<>(); + actual.forEach(index -> values.add(index.fieldValues())); + Assert.assertEquals(Set.of("alpha", "beta", "alpha beta"), values); + } + } + } + + @Test + public void testEngineNullableCompositePrefixAndCollectionExpansionRemainUnloaded() { + FakeObjects objects = new FakeObjects(); + PropertyKey first = objects.newPropertyKey(IdGenerator.of(1), "first", DataType.TEXT); + PropertyKey missing = objects.newPropertyKey(IdGenerator.of(2), "missing", DataType.TEXT); + PropertyKey last = objects.newPropertyKey(IdGenerator.of(3), "last", DataType.TEXT); + VertexLabel label = objects.newVertexLabel(IdGenerator.of(4), "vertex", IdStrategy.CUSTOMIZE_NUMBER, + first.id(), missing.id(), last.id()); + label.nullableKeys(missing.id()); + HugeVertex vertex = new HugeVertex(objects.graph(), IdGenerator.of(1), label); + vertex.addProperty(first, "first"); + vertex.addProperty(last, "last"); + vertex.propNotLoaded(); + IndexLabel index = objects.newIndexLabel(IdGenerator.of(5), "composite", HugeType.VERTEX_LABEL, + label.id(), IndexType.SECONDARY, + first.id(), missing.id(), last.id()); + List actual = captureEngineExpansion(objects, vertex, index, text -> Set.of(text)); + Assert.assertEquals(1, actual.size()); + Assert.assertEquals("first", actual.get(0).fieldValues()); + Assert.assertFalse(vertex.isPropLoaded()); + + PropertyKey list = objects.newPropertyKey(IdGenerator.of(6), "list", DataType.INT, Cardinality.LIST); + label.properties(list.id()); + vertex.addProperty(list, List.of(7, -1, 7)); + IndexLabel collection = objects.newIndexLabel(IdGenerator.of(7), "collection", HugeType.VERTEX_LABEL, + label.id(), IndexType.SECONDARY, list.id()); + actual = captureEngineExpansion(objects, vertex, collection, text -> Set.of(text)); + Assert.assertEquals(3, actual.size()); + Assert.assertEquals(actual.get(0).fieldValues(), actual.get(2).fieldValues()); + } + + @Test + public void testEngineAndStoreSelectInheritedEdgeIndexesThroughSharedHelper() { + FakeObjects objects = new FakeObjects(); + PropertyKey inherited = objects.newPropertyKey(IdGenerator.of(1), "inherited", DataType.TEXT); + PropertyKey own = objects.newPropertyKey(IdGenerator.of(2), "own", DataType.TEXT); + EdgeLabel parent = objects.newEdgeLabel(IdGenerator.of(3), "parent", Frequency.MULTIPLE, + IdGenerator.of(5), IdGenerator.of(5), inherited.id()); + EdgeLabel child = objects.newEdgeLabel(IdGenerator.of(4), "child", Frequency.MULTIPLE, + IdGenerator.of(5), IdGenerator.of(5), inherited.id(), own.id()); + child.edgeLabelType(EdgeLabelType.SUB); + child.fatherId(parent.id()); + parent.enableLabelIndex(false); + child.enableLabelIndex(false); + IndexLabel parentIndex = objects.newIndexLabel(IdGenerator.of(11), "parentIndex", HugeType.EDGE_LABEL, + parent.id(), IndexType.SECONDARY, inherited.id()); + IndexLabel childIndex = objects.newIndexLabel(IdGenerator.of(12), "childIndex", HugeType.EDGE_LABEL, + child.id(), IndexType.SECONDARY, own.id()); + parent.indexLabels(parentIndex.id()); + child.indexLabels(childIndex.id()); + HugeEdge edge = new HugeEdge(objects.graph(), new EdgeId(IdGenerator.of(1), Directions.OUT, + parent.id(), child.id(), "", IdGenerator.of(2)), child); + edge.addProperty(inherited, "parent value"); + edge.addProperty(own, "child value"); + Analyzer analyzer = text -> Set.of(text); + List store = new IndexBuilder(objects.graph(), analyzer).buildEdgeIndex(edge.element()); + List engine = captureEngineIndexes(objects, List.of(parentIndex, childIndex), analyzer, + tx -> tx.updateEdgeIndex(edge, true)); + Assert.assertEquals(2, engine.size()); + Assert.assertEquals(childIndex.id(), engine.get(0).indexLabelId()); + Assert.assertEquals(parentIndex.id(), engine.get(1).indexLabelId()); + for (int i = 0; i < engine.size(); i++) { + Assert.assertEquals(store.get(i).indexLabelId(), engine.get(i).indexLabelId()); + Assert.assertEquals(store.get(i).fieldValues(), engine.get(i).fieldValues()); + } + } + + @Test + public void testEngineOlapAdapterSelectsTransactionCandidatesAndPreservesSingletonGuard() { + FakeObjects objects = new FakeObjects(); + PropertyKey property = objects.newPropertyKey(IdGenerator.of(1), "olap", DataType.INT); + property.writeType(WriteType.OLAP_COMMON); + PropertyKey unrelated = objects.newPropertyKey(IdGenerator.of(2), "other", DataType.INT); + HugeVertex vertex = new HugeVertex(objects.graph(), IdGenerator.of(1), VertexLabel.OLAP_VL); + vertex.addProperty(property, 42); + IndexLabel selected = objects.newIndexLabel(IdGenerator.of(11), "selected", HugeType.VERTEX_LABEL, + VertexLabel.OLAP_VL.id(), IndexType.RANGE_INT, property.id()); + IndexLabel other = objects.newIndexLabel(IdGenerator.of(12), "other", HugeType.VERTEX_LABEL, + VertexLabel.OLAP_VL.id(), IndexType.RANGE_INT, unrelated.id()); + List actual = captureEngineIndexes(objects, List.of(other, selected), text -> Set.of(text), + tx -> tx.updateVertexIndex(vertex, true)); + Assert.assertEquals(1, actual.size()); + Assert.assertEquals(selected.id(), actual.get(0).indexLabelId()); + Assert.assertEquals(42, actual.get(0).fieldValues()); + for (HugeVertex malformed : List.of(new HugeVertex(objects.graph(), IdGenerator.of(2), VertexLabel.OLAP_VL), + new HugeVertex(objects.graph(), IdGenerator.of(3), VertexLabel.OLAP_VL))) { + if (malformed.id().equals(IdGenerator.of(3))) { + malformed.addProperty(property, 42); + malformed.addProperty(unrelated, 43); + } + try { + captureEngineIndexes(objects, List.of(selected), text -> Set.of(text), + tx -> tx.updateVertexIndex(malformed, true)); + Assert.fail("Malformed olap vertex must fail the shared singleton guard"); + } catch (IllegalArgumentException expected) { + Assert.assertTrue(expected.getMessage().contains("Expect only 1 property")); + } + } + } + + private static List captureEngineExpansion(FakeObjects objects, HugeVertex vertex, + IndexLabel label, Analyzer analyzer) { + return captureEngineIndexes(objects, List.of(label), analyzer, + tx -> tx.updateIndex(label.id(), vertex, true)); + } + + private static List captureEngineIndexes(FakeObjects objects, List labels, + Analyzer analyzer, Consumer action) { + HugeGraphParams params = Mockito.mock(HugeGraphParams.class); + ISchemaTransaction schema = Mockito.mock(ISchemaTransaction.class); + AbstractSerializer serializer = Mockito.mock(AbstractSerializer.class); + BackendStore store = Mockito.mock(BackendStore.class); + Mockito.when(params.graph()).thenReturn(objects.graph()); + Mockito.doReturn(objects.graph().configuration()).when(params).configuration(); + Mockito.when(params.serializer()).thenReturn(serializer); + Mockito.when(params.schemaTransaction()).thenReturn(schema); + Mockito.when(schema.getVertexLabel(Mockito.any(Id.class))) + .thenAnswer(call -> objects.graph().vertexLabel((Id) call.getArgument(0))); + Mockito.when(schema.getEdgeLabel(Mockito.any(Id.class))) + .thenAnswer(call -> objects.graph().edgeLabel((Id) call.getArgument(0))); + Mockito.when(schema.getIndexLabels()).thenReturn(labels); + Mockito.when(params.analyzer()).thenReturn(analyzer); + for (IndexLabel label : labels) { + Mockito.when(schema.getIndexLabel(label.id())).thenReturn(label); + } + List indexes = new ArrayList<>(); + Mockito.when(serializer.writeIndex(Mockito.any(Index.class))).thenAnswer(call -> { + indexes.add(call.getArgument(0)); + return Mockito.mock(BackendEntry.class); + }); + GraphIndexTransaction tx = new GraphIndexTransaction(params, store) { + @Override + public void doEliminate(BackendEntry entry) { + // Capture pure expansion without touching a real backend. + } + }; + action.accept(tx); + return indexes; + } + } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphTransactionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphTransactionTest.java index 3cb79e3687..cc67b851f0 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphTransactionTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphTransactionTest.java @@ -17,8 +17,11 @@ package org.apache.hugegraph.backend.tx; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.apache.hugegraph.HugeFactory; @@ -26,20 +29,24 @@ import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.backend.cache.Cache; import org.apache.hugegraph.backend.cache.CachedGraphTransaction; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery.OptimizedType; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdQuery; import org.apache.hugegraph.backend.query.QueryResultContext; import org.apache.hugegraph.backend.tx.GraphIndexTransaction.RemoveLeftIndexJob; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.job.EphemeralJob; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery.OptimizedType; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.BaseEdge; +import org.apache.hugegraph.structure.HugeEdge; +import org.apache.hugegraph.structure.HugeElement; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.type.define.SchemaStatus; import org.apache.hugegraph.unit.FakeObjects; import org.apache.tinkerpop.gremlin.structure.T; @@ -49,6 +56,84 @@ public class GraphTransactionTest { + @Test + public void testRangeIndexCleanupWithoutMatchingLabel() throws Exception { + try (FilterFixture fixture = new FilterFixture()) { + ConditionQuery query = Mockito.spy(new ConditionQuery(HugeType.VERTEX)); + query.eq(HugeKeys.LABEL, fixture.vertex.schemaLabel().id()); + query.eq(HugeKeys.LABEL, IdGenerator.of(Long.MAX_VALUE)); + Id field = fixture.graph.propertyKey("name").id(); + ConditionQuery.LeftIndex left = new ConditionQuery.LeftIndex( + Collections.singleton("stale"), field); + Mockito.doReturn(Collections.singleton(left)).when(query) + .getLeftIndexOfElement(fixture.vertex.id()); + Constructor constructor = + RemoveLeftIndexJob.class.getDeclaredConstructor( + ConditionQuery.class, HugeElement.class); + constructor.setAccessible(true); + RemoveLeftIndexJob job = constructor.newInstance(query, fixture.vertex); + Whitebox.setInternalState(job, "tx", fixture.transaction.indexTransaction()); + Method process = RemoveLeftIndexJob.class.getDeclaredMethod( + "processRangeIndexLeft", ConditionQuery.class, HugeElement.class); + process.setAccessible(true); + Assert.assertEquals(0L, process.invoke(job, query, fixture.vertex)); + Mockito.verify(query).removeElementLeftIndex(fixture.vertex.id()); + } + } + + @Test + public void testResolvedEdgeLabelsInBatchResultFilter() throws Exception { + try (FilterFixture fixture = new FilterFixture()) { + Method filter = GraphTransaction.class.getDeclaredMethod( + "rightResultFromIndexQuery", QueryResultContext.class, HugeElement.class); + filter.setAccessible(true); + HugeEdge edge = Mockito.mock(HugeEdge.class); + BaseEdge baseEdge = Mockito.mock(BaseEdge.class); + Mockito.when(edge.element()).thenReturn(baseEdge); + Mockito.when(edge.type()).thenReturn(HugeType.EDGE); + Id first = IdGenerator.of(1L); + Id second = IdGenerator.of(2L); + ConditionQuery query = Mockito.spy(new ConditionQuery(HugeType.EDGE)); + query.optimized(OptimizedType.INDEX); + query.query(Condition.in(HugeKeys.LABEL, Arrays.asList(first, second))); + Mockito.doReturn(false).when(query).test(Mockito.eq(baseEdge), Mockito.isNull()); + + // Multi-label batches must not take the single-label fast path. + Assert.assertEquals(false, filter.invoke(fixture.transaction, + new QueryResultContext(query), edge)); + Mockito.verify(query).test(baseEdge, null); + + // An intersection that resolves to one label can use the fast path. + query.query(Condition.eq(HugeKeys.LABEL, first)); + Assert.assertEquals(true, filter.invoke(fixture.transaction, + new QueryResultContext(query), edge)); + Mockito.verify(query).test(baseEdge, null); + + // An empty intersection must be filtered, not accepted as one label. + query.query(Condition.eq(HugeKeys.LABEL, second)); + Assert.assertEquals(false, filter.invoke(fixture.transaction, + new QueryResultContext(query), edge)); + Mockito.verify(query, Mockito.times(2)).test(baseEdge, null); + } + } + + @Test + public void testOlapDeletionDoesNotScanBaseVertexEdges() { + GraphTransaction transaction = Mockito.mock(GraphTransaction.class, Mockito.CALLS_REAL_METHODS); + HugeVertex vertex = Mockito.mock(HugeVertex.class); + Mockito.when(vertex.schemaLabel()).thenReturn(VertexLabel.OLAP_VL); + Id id = IdGenerator.of(1L); + Mockito.when(vertex.id()).thenReturn(id); + + // Preserve OLAP's original rejection before any ordinary edge scan. + IllegalStateException error = Assert.assertThrows(IllegalStateException.class, () -> { + transaction.prepareDeletions(Collections.singletonMap(id, vertex), Collections.emptyMap()); + }); + Assert.assertTrue(error.getMessage().startsWith("Graph is null of schema")); + Mockito.verify(transaction, Mockito.never()).queryEdgesFromBackend(Mockito.any()); + Mockito.verify(transaction, Mockito.never()).doRemove(Mockito.any()); + } + @Test public void testBatchDecisionsRemainFixedAfterOriginChanges() { ConditionQuery root = new ConditionQuery(HugeType.VERTEX); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java index aa4ac8cce7..66680e70cc 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/AuthTest.java @@ -27,9 +27,10 @@ import javax.security.sasl.AuthenticationException; import org.apache.commons.collections.CollectionUtils; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; +import org.apache.hugegraph.auth.AuthConstant; import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.HugeAccess; import org.apache.hugegraph.auth.HugeBelong; @@ -41,11 +42,14 @@ import org.apache.hugegraph.auth.HugeUser; import org.apache.hugegraph.auth.RolePermission; import org.apache.hugegraph.auth.StandardAuthManager; +import org.apache.hugegraph.auth.TokenGenerator; import org.apache.hugegraph.auth.UserWithRole; import org.apache.hugegraph.backend.cache.Cache; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.exception.NotFoundException; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.util.JsonUtil; @@ -139,8 +143,7 @@ public void clearAll() { } //FIXME: support project in hstore - boolean isHstore = Objects.equals("hstore", System.getProperty("backend")) || - (System.getProperty("backend") == null); + boolean isHstore = Objects.equals("hstore", graph.backend()); if (!isHstore) { for (HugeProject project : authManager.listAllProject(-1)) { @@ -1667,6 +1670,25 @@ public void testValidateUserByToken() throws AuthenticationException { Assert.assertNull(userWithRole.role()); } + @Test + public void testRejectInvalidAndExpiredTokens() { + AuthManager authManager = graph().authManager(); + TokenGenerator generator = Whitebox.getInternalState(authManager, + "tokenGenerator"); + Map claims = ImmutableMap.of(AuthConstant.TOKEN_USER_NAME, + "test"); + String expired = generator.create(claims, -60000L); + String wrongKey = new TokenGenerator("abcdefghijklmnopqrstuvwxyz012345") + .create(claims, 60000L); + + for (String token : ImmutableList.of(expired, wrongKey, "123.ansfaf")) { + UserWithRole rejected = authManager.validateUser(token); + Assert.assertEquals("", rejected.username()); + Assert.assertNull(rejected.userId()); + Assert.assertNull(rejected.role()); + } + } + @Test public void testLogout() throws AuthenticationException { AuthManager authManager = graph().authManager(); @@ -1757,6 +1779,90 @@ public void testDelProject() { }); } + @Test + public void testProjectCleanupRepeatsWithoutBackendSystemProperty() { + Assume.assumeTrue("skip this test for hstore", !Objects.equals("hstore", graph().backend())); + String previous = System.getProperty("backend"); + try { + System.clearProperty("backend"); + for (int attempt = 0; attempt < 2; attempt++) { + this.testDeleteProjectAfterRequestCleanup(); + this.clearAll(); + Assert.assertEquals(0, graph().authManager().listAllProject(-1).size()); + } + } finally { + if (previous == null) { + System.clearProperty("backend"); + } else { + System.setProperty("backend", previous); + } + } + } + + @Test + public void testDeleteProjectAfterRequestCleanup() { + Assume.assumeTrue("skip this test for hstore", !Objects.equals("hstore", graph().backend())); + AuthManager authManager = graph().authManager(); + HugeProject project = makeProject("cleanup_project", ""); + Id projectId = authManager.createProject(project); + HugeProject saved = authManager.getProject(projectId); + Id retainedId = authManager.createProject(makeProject("retained_project", "")); + HugeProject retained = authManager.getProject(retainedId); + Assert.assertEquals(3, authManager.listAccessByTarget(saved.targetId(), -1).size()); + Id userId = authManager.createUser(makeUser("membership_user", "pass")); + authManager.createBelong(makeBelong(userId, saved.adminGroupId())); + Id retainedBelongId = authManager.createBelong(makeBelong(userId, retained.adminGroupId())); + + // Match FINISHED cleanup between REST creation and deletion requests. + HugeFactory.closeCurrentThreadTransactions(); + authManager.deleteProject(projectId); + HugeFactory.closeCurrentThreadTransactions(); + + Assert.assertEquals(0, authManager.listAccessByGroup(saved.adminGroupId(), -1).size()); + Assert.assertEquals(0, authManager.listAccessByGroup(saved.opGroupId(), -1).size()); + Assert.assertEquals(0, authManager.listAccessByTarget(saved.targetId(), -1).size()); + Assert.assertEquals(3, authManager.listAccessByTarget(retained.targetId(), -1).size()); + Assert.assertEquals(0, authManager.listBelongByGroup(saved.adminGroupId(), -1).size()); + Assert.assertEquals(1, authManager.listBelongByUser(userId, -1).size()); + Assert.assertEquals(retainedBelongId, authManager.getBelong(retainedBelongId).id()); + Assert.assertEquals(userId, authManager.getUser(userId).id()); + Assert.assertEquals(projectId, authManager.createProject(makeProject("cleanup_project", ""))); + Assert.assertEquals(0, authManager.listBelongByGroup(saved.adminGroupId(), -1).size()); + } + + @Test + public void testDeleteUserWithRelationsAfterRequestCleanup() { + Assume.assumeTrue("skip this test for hstore", !Objects.equals("hstore", graph().backend())); + AuthManager authManager = graph().authManager(); + Id deletedUser = authManager.createUser(makeUser("deleted_member", "pass")); + Id retainedUser = authManager.createUser(makeUser("retained_member", "pass")); + Id group = authManager.createGroup(makeGroup("shared_group")); + authManager.createBelong(makeBelong(deletedUser, group)); + Id retainedBelong = authManager.createBelong(makeBelong(retainedUser, group)); + + HugeFactory.closeCurrentThreadTransactions(); + authManager.deleteUser(deletedUser); + HugeFactory.closeCurrentThreadTransactions(); + + Assert.assertThrows(NotFoundException.class, () -> authManager.getUser(deletedUser)); + Assert.assertEquals(0, authManager.listBelongByUser(deletedUser, -1).size()); + Assert.assertEquals(1, authManager.listBelongByGroup(group, -1).size()); + Assert.assertEquals(retainedBelong, authManager.getBelong(retainedBelong).id()); + Assert.assertEquals(retainedUser, authManager.getUser(retainedUser).id()); + } + + @Test + public void testDeleteUserWithoutRelationsAfterRequestCleanup() { + Assume.assumeTrue("skip this test for hstore", !Objects.equals("hstore", graph().backend())); + AuthManager authManager = graph().authManager(); + Id userId = authManager.createUser(makeUser("isolated_user", "pass")); + HugeFactory.closeCurrentThreadTransactions(); + authManager.deleteUser(userId); + HugeFactory.closeCurrentThreadTransactions(); + Assert.assertThrows(NotFoundException.class, () -> authManager.getUser(userId)); + Assert.assertEquals(0, authManager.listBelongByUser(userId, -1).size()); + } + @Test public void testUpdateProject() { Assume.assumeTrue("skip this test for hstore", diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/BaseCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/BaseCoreTest.java index 701065fe44..ab84a393d6 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/BaseCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/BaseCoreTest.java @@ -24,7 +24,7 @@ import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.backend.cache.CacheManager; import org.apache.hugegraph.backend.store.BackendFeatures; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.util.Log; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java index f18d656c08..6eb8b29733 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java @@ -18,11 +18,14 @@ package org.apache.hugegraph.core; import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.auth.BackendLeaseCleanupTest; +import org.apache.hugegraph.auth.TransactionLifecycleTest; import org.apache.hugegraph.constant.ServiceConstant; import org.apache.hugegraph.dist.RegisterUtil; import org.apache.hugegraph.masterelection.GlobalMasterInfo; import org.apache.hugegraph.meta.MetaManager; import org.apache.hugegraph.meta.PdMetaDriver; +import org.apache.hugegraph.task.StandardTaskSchedulerTxTest; import org.apache.hugegraph.task.TaskAndResultSchedulerTest; import org.apache.hugegraph.testutil.Utils; import org.apache.hugegraph.util.Log; @@ -34,13 +37,19 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ + BackendLeaseCleanupTest.class, + OltpTraverserCursorTest.class, + OltpTraverserRocksDBTest.class, PropertyKeyCoreTest.class, VertexLabelCoreTest.class, EdgeLabelCoreTest.class, IndexLabelCoreTest.class, VertexCoreTest.class, + SourceIndexCoverageTest.class, EdgeCoreTest.class, CountStrategyCoreTest.class, + IdPredicateCoreTest.class, + TinkerPop37StepsCoreTest.class, PrimaryKeyStrategyCoreTest.class, ParentAndSubEdgeCoreTest.class, PropertyCoreTest.VertexPropertyCoreTest.class, @@ -48,7 +57,9 @@ RestoreCoreTest.class, TaskCoreTest.class, TaskAndResultSchedulerTest.class, + StandardTaskSchedulerTxTest.class, AuthTest.class, + TransactionLifecycleTest.class, MultiGraphsTest.class, RamTableTest.class }) diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java index f009180508..273688a5c4 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java @@ -17,27 +17,100 @@ package org.apache.hugegraph.core; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.query.Aggregate; +import org.apache.hugegraph.query.Aggregate.AggregateFunc; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.traversal.optimize.ConditionP; +import org.apache.hugegraph.traversal.optimize.HugeCountStep; +import org.apache.hugegraph.traversal.optimize.HugeCountStepStrategy; +import org.apache.hugegraph.traversal.optimize.HugeCountStrategy; import org.apache.hugegraph.traversal.optimize.HugeGraphStep; +import org.apache.hugegraph.traversal.optimize.HugeGraphStepStrategy; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.tinkerpop.gremlin.process.traversal.NotP; import org.apache.tinkerpop.gremlin.process.traversal.P; import org.apache.tinkerpop.gremlin.process.traversal.Step; +import org.apache.tinkerpop.gremlin.process.traversal.TextP; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.OrderGlobalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer; import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator; import org.junit.Test; public class CountStrategyCoreTest extends BaseCoreTest { + @Test + public void testNamedLimitCountPreservesExplicitBound() { + GraphTraversalSource g = graph().traversal(); + Assert.assertEquals(Collections.singletonList(3L), + g.inject(1, 2, 3).limit(3).count().is(P.gt(0L)).toList()); + Assert.assertEquals(Collections.singletonList(3L), + g.withoutStrategies(HugeCountStrategy.class) + .inject(1, 2, 3).limit(GValue.of("size", 3L)) + .count().is(P.gt(0L)).toList()); + Assert.assertEquals(Collections.singletonList(3L), + g.inject(1, 2, 3).limit(GValue.of("size", 3L)) + .count().is(P.gt(0L)).toList()); + Assert.assertEquals(Collections.singletonList(3L), + g.inject(1, 2, 3).limit(GValue.of("size", 3L)) + .count().is(P.not(P.eq(0L))).toList()); + } + + @Test + public void testNamedRangeCountPreservesExplicitBounds() { + GraphTraversalSource g = graph().traversal(); + Assert.assertEquals(Collections.singletonList(3L), + g.inject(1, 2, 3, 4).range(1, 4) + .count().is(P.gt(0L)).toList()); + Assert.assertEquals(Collections.singletonList(3L), + g.withoutStrategies(HugeCountStrategy.class) + .inject(1, 2, 3, 4) + .range(GValue.of("lo", 1L), GValue.of("hi", 4L)) + .count().is(P.gt(0L)).toList()); + Assert.assertEquals(Collections.singletonList(3L), + g.inject(1, 2, 3, 4) + .range(GValue.of("lo", 1L), GValue.of("hi", 4L)) + .count().is(P.gt(0L)).toList()); + Assert.assertEquals(Collections.singletonList(3L), + g.inject(1, 2, 3, 4) + .range(GValue.of("lo", 1L), GValue.of("hi", 4L)) + .count().is(P.not(P.eq(0L))).toList()); + } + private void initSchema() { SchemaManager schema = graph().schema(); schema.propertyKey("name").asText().create(); @@ -101,7 +174,8 @@ private static boolean hasRemainingHasStep(GraphTraversal traversal, if (!(step instanceof HasStep)) { continue; } - HasContainerHolder holder = (HasContainerHolder) step; + HasContainerHolder holder = + (HasContainerHolder) step; for (HasContainer has : holder.getHasContainers()) { if (key.equals(has.getKey())) { return true; @@ -111,6 +185,38 @@ private static boolean hasRemainingHasStep(GraphTraversal traversal, return false; } + private void assertNegatedBooleanPredicate(long expected, + P predicate) { + GraphTraversal traversal = graph().traversal().V() + .has("vp2", + P.not(predicate)) + .count(); + traversal.asAdmin().applyStrategies(); + + Assert.assertTrue(hasRemainingHasStep(traversal, "vp2")); + Assert.assertEquals(expected, traversal.next().longValue()); + } + + private static void assertUncommittedRangeUnsupported( + GraphTraversal traversal) { + Assert.assertThrows(IllegalArgumentException.class, traversal::next, + e -> { + Assert.assertContains("offset/limit", e.getMessage()); + Assert.assertContains("uncommitted records", e.getMessage()); + }); + } + + private static void assertNegatedCountHighRange(long expected, + P predicate) { + GraphTraversal traversal = __.count().is(P.not(predicate)); + HugeCountStrategy.instance().apply(traversal.asAdmin()); + + Step firstStep = traversal.asAdmin().getStartStep(); + Assert.assertInstanceOf(RangeGlobalStep.class, firstStep); + Assert.assertEquals(expected, + ((RangeGlobalStep) firstStep).getHighRange()); + } + private void initTextRangeSchema(boolean withEdge) { SchemaManager schema = graph().schema(); schema.propertyKey("vp4").asText().create(); @@ -134,6 +240,111 @@ private void initConnectiveRangeNoIndexSchema() { .nullableKeys("ep4").link("vl1", "vl1").create(); } + private void initNegatedDoubleSchema() { + SchemaManager schema = graph().schema(); + schema.propertyKey("score").asDouble().create(); + schema.vertexLabel("sample").properties("score").create(); + schema.indexLabel("sampleByScore").onV("sample") + .by("score").range().create(); + } + + private static void assertUnoptimizedCount(long expected, + GraphTraversal traversal) { + traversal.asAdmin().applyStrategies(); + + Assert.assertInstanceOf(CountGlobalStep.class, traversal.asAdmin().getEndStep()); + Assert.assertFalse(traversal.asAdmin().getSteps().stream() + .anyMatch(step -> step instanceof HugeCountStep)); + Assert.assertEquals(Collections.singletonList(expected), traversal.toList()); + } + + private void assertRepeatedGraphCounts(long vertices) { + assertUnoptimizedCount(vertices * vertices, + graph().traversal().V().V().count()); + assertUnoptimizedCount(2L * vertices, + graph().traversal().inject(1, 2).V().count()); + // Repeated inputs can be bulked, but each must still contribute a scan. + assertUnoptimizedCount(2L * vertices, + graph().traversal().inject(1, 1).barrier().V().count()); + + GraphTraversal root = graph().traversal().V().count(); + root.asAdmin().applyStrategies(); + Assert.assertInstanceOf(HugeCountStep.class, root.asAdmin().getStartStep()); + Assert.assertEquals(Collections.singletonList(vertices), root.toList()); + } + + @Test + public void testRepeatedGraphCountOnEmptyGraph() { + this.initSchema(); + + this.assertRepeatedGraphCounts(0L); + } + + @Test + public void testRepeatedGraphCountOnSingleVertex() { + this.initSchema(); + graph().addVertex(T.label, "person", "name", "marko"); + commitTx(); + + this.assertRepeatedGraphCounts(1L); + } + + @Test + public void testRepeatedGraphCountOnMultipleVertices() { + this.initSchema(); + this.initGraph(); + + this.assertRepeatedGraphCounts(3L); + } + + @Test + public void testRepeatedGraphCountWithIds() { + this.initSchema(); + this.initGraph(); + Object id = graph().traversal().V().next().id(); + + assertUnoptimizedCount(3L, graph().traversal().V().V(id).count()); + assertUnoptimizedCount(2L, graph().traversal().inject(1, 2).V(id).count()); + assertUnoptimizedCount(1L, graph().traversal().V(id).V(id).count()); + } + + @Test + public void testCountAfterOrderByMissingProperty() { + this.initSchema(); + this.initGraph(); + + long count = graph().traversal().V().order().by("none").count().next(); + + Assert.assertEquals(0L, count); + } + + @Test + public void testCountAfterOrderByFilteringTraversal() { + this.initSchema(); + this.initGraph(); + + long count = graph().traversal().V().order() + .by(__.hasLabel("person").values("name")) + .count().next(); + + Assert.assertEquals(2L, count); + } + + @Test + public void testCountAfterOrderByPresentProperty() { + this.initSchema(); + this.initGraph(); + + GraphTraversal traversal = graph().traversal().V() + .order().by("name").count(); + traversal.asAdmin().applyStrategies(); + + Assert.assertFalse(traversal.asAdmin().getEndStep() instanceof HugeCountStep); + Assert.assertTrue(traversal.asAdmin().getSteps().stream() + .anyMatch(step -> step instanceof OrderGlobalStep)); + Assert.assertEquals(3L, traversal.next().longValue()); + } + @Test public void testWhereCountLtNegativeIsAlwaysFalse() { this.initSchema(); @@ -246,6 +457,305 @@ public void testWhereCountNegatedNestedConnectivePredicate() { Assert.assertEquals(1L, count); } + @Test + public void testCountNegatedCollectionPredicatesKeepFinalResults() { + this.assertCountPredicatesKeepFinalResults(List.of( + P.not(P.without(2L, 3L)), + P.not(P.within(2L, 3L)), + P.not(P.without(2L, 3L)).or(P.eq(0L)), + P.not(P.within(2L, 3L)).or(P.eq(0L)), + P.not(P.without(2L, 3L)).and(P.gt(0L)), + P.not(P.within(2L, 3L)).and(P.gt(0L)), + P.not(P.not(P.within(2L, 3L))).or(P.eq(0L)), + P.not(P.not(P.without(2L, 3L))).or(P.eq(0L)), + new NotP<>(new NotP<>(P.within(2L, 3L))).or(P.eq(0L)), + new NotP<>(new NotP<>(P.without(2L, 3L))).or(P.eq(0L)), + new NotP<>(new NotP<>(P.eq(2L))).or(P.eq(0L)), + P.not(P.without(2L, 3L)).or(P.eq(0L)).and(P.gte(0L)), + P.not(P.within(2L, 3L).or(P.eq(0L))))); + } + + @Test + public void testCountUnknownNumericPredicateKeepsFinalResults() { + P custom = new P<>((count, ignored) -> count == 2L, 0L); + P customFalse = new P<>((count, ignored) -> count == 1L, 0L); + this.assertCountPredicatesKeepFinalResults(List.of( + custom, custom.or(P.eq(0L)), custom.and(P.gt(0L)), + customFalse.or(P.eq(0L)), P.not(custom).or(P.eq(0L)))); + } + + @Test + public void testCountWithoutNegativeValuesKeepsEmptyCount() { + this.initSchema(); + commitTx(); + assertNegativeWithoutCounts(graph().traversal(), 0L); + } + + @Test + public void testCountWithoutNegativeValuesKeepsNonemptyAndNestedCount() { + this.initSchema(); + Vertex source = graph().addVertex(T.label, "person", "name", "source"); + for (int i = 0; i < 5; i++) { + Vertex target = graph().addVertex(T.label, "person", "name", "target" + i); + source.addEdge("knows", target); + } + commitTx(); + GraphTraversalSource g = graph().traversal(); + assertNegativeWithoutCounts(g, 6L); + for (P predicate : List.of(P.without(-1L), P.without(-2L, -1L))) { + Assert.assertEquals(Collections.singletonList(source.id()), + g.V().filter(__.out("knows").count().is(predicate.clone()) + .is(P.gt(0L))).id().toList()); + } + } + + private static void assertNegativeWithoutCounts(GraphTraversalSource g, long count) { + GraphTraversalSource control = g.withoutStrategies(HugeCountStrategy.class); + for (P predicate : List.of(P.without(-1L), P.without(-2L, -1L))) { + Assert.assertEquals(Collections.singletonList(count), + control.V().count().is(predicate.clone()).toList()); + Assert.assertEquals(Collections.singletonList(count), + g.V().count().is(predicate.clone()).toList()); + Assert.assertEquals(count > 0L ? Collections.singletonList(count) : Collections.emptyList(), + g.V().count().is(predicate.clone()).is(P.gt(0L)).toList()); + } + } + + @Test + public void testCountWithoutUsesBoundAboveAllExcludedCounts() { + this.initSchema(); + Vertex source = graph().addVertex(T.label, "person", "name", "source"); + for (int i = 0; i < 4; i++) { + Vertex target = graph().addVertex(T.label, "person", "name", "target" + i); + source.addEdge("knows", target); + } + commitTx(); + GraphTraversalSource g = graph().traversal(); + GraphTraversalSource control = g.withoutStrategies(HugeCountStrategy.class); + for (P predicate : List.of(P.without(2L, 3L), + P.without(2L, 3L).or(P.eq(0L)), + P.without(2L, 3L).and(P.gt(0L)))) { + Assert.assertEquals(Collections.singletonList(source.id()), + control.V(source.id()).where(__.out("knows").count() + .is(predicate.clone())).id().toList()); + Assert.assertEquals(Collections.singletonList(source.id()), + g.V(source.id()).where(__.out("knows").count() + .is(predicate.clone())).id().toList()); + Assert.assertEquals(Collections.singletonList(4L), + g.V(source.id()).out("knows").count() + .is(predicate.clone()).toList()); + } + } + + @Test + public void testCountLossyNumericBoundsKeepCompleteCount() { + List bounds = List.of(new BigDecimal("1E-400"), + BigInteger.ONE, new AtomicLong(1L), 9007199254740993L, + 16777216F, 9007199254740992D); + GraphTraversalSource g = graph().traversal(); + GraphTraversalSource control = g.withoutStrategies(HugeCountStrategy.class); + for (Number bound : bounds) { + for (P predicate : List.of(P.gte(bound), P.lt(bound), + P.gte(bound).or(P.eq(0)))) { + List expected = predicate.test(2L) ? + Collections.singletonList(2L) : + Collections.emptyList(); + Assert.assertEquals(expected, + control.inject(1, 2).count().is(predicate.clone()).toList()); + Assert.assertEquals(expected, + g.inject(1, 2).count().is(predicate.clone()).toList()); + GraphTraversal traversal = __.count().is(predicate.clone()); + HugeCountStrategy.instance().apply(traversal.asAdmin()); + Assert.assertInstanceOf(CountGlobalStep.class, + traversal.asAdmin().getStartStep()); + } + } + } + + private void assertCountPredicatesKeepFinalResults(List> predicates) { + this.initSchema(); + Vertex source = graph().addVertex(T.label, "person", "name", "source"); + Vertex first = graph().addVertex(T.label, "person", "name", "first"); + Vertex second = graph().addVertex(T.label, "person", "name", "second"); + source.addEdge("knows", first); + source.addEdge("knows", second); + commitTx(); + + GraphTraversalSource g = graph().traversal(); + GraphTraversalSource control = g.withoutStrategies(HugeCountStrategy.class); + for (P predicate : predicates) { + List expectedIds = predicate.test(2L) ? + Collections.singletonList(source.id()) : + Collections.emptyList(); + List expectedCounts = predicate.test(2L) ? + Collections.singletonList(2L) : + Collections.emptyList(); + Assert.assertEquals(expectedIds, + control.V(source.id()).where(__.out("knows").count() + .is(predicate.clone())).id().toList()); + Assert.assertEquals(expectedIds, + g.V(source.id()).where(__.out("knows").count() + .is(predicate.clone())).id().toList()); + Assert.assertEquals(expectedCounts, + control.V(source.id()).out("knows").count() + .is(predicate.clone()).toList()); + Assert.assertEquals(expectedCounts, + g.V(source.id()).out("knows").count() + .is(predicate.clone()).toList()); + } + } + + @Test + public void testWhereCountNegatedScalarPredicatesKeepSemantics() { + this.initSchema(); + Vertex source = graph().addVertex(T.label, "person", "name", "source"); + Vertex first = graph().addVertex(T.label, "person", "name", "first"); + Vertex second = graph().addVertex(T.label, "person", "name", "second"); + source.addEdge("knows", first); + source.addEdge("knows", second); + commitTx(); + + long notEqZero = graph().traversal().V(source.id()) + .where(__.out("knows").count() + .is(P.not(P.eq(0L)))) + .count().next(); + long notNeqOne = graph().traversal().V(source.id()) + .where(__.out("knows").count() + .is(P.not(P.neq(1L)))) + .count().next(); + long notLtTwo = graph().traversal().V(source.id()) + .where(__.out("knows").count() + .is(P.not(P.lt(2L)))) + .count().next(); + long notLteOne = graph().traversal().V(source.id()) + .where(__.out("knows").count() + .is(P.not(P.lte(1L)))) + .count().next(); + long notGtOne = graph().traversal().V(source.id()) + .where(__.out("knows").count() + .is(P.not(P.gt(1L)))) + .count().next(); + long notGteThree = graph().traversal().V(source.id()) + .where(__.out("knows").count() + .is(P.not(P.gte(3L)))) + .count().next(); + + Assert.assertEquals(1L, notEqZero); + Assert.assertEquals(0L, notNeqOne); + Assert.assertEquals(1L, notLtTwo); + Assert.assertEquals(1L, notLteOne); + Assert.assertEquals(0L, notGtOne); + Assert.assertEquals(1L, notGteThree); + } + + @Test + public void testNegatedScalarPredicatesUseComplementedHighRange() { + assertNegatedCountHighRange(3L, P.eq(2L)); + assertNegatedCountHighRange(3L, P.neq(2L)); + assertNegatedCountHighRange(2L, P.lt(2L)); + assertNegatedCountHighRange(3L, P.lte(2L)); + assertNegatedCountHighRange(3L, P.gt(2L)); + assertNegatedCountHighRange(2L, P.gte(2L)); + } + + @Test + public void testNegatedTextPredicateStaysLocal() { + this.initTextRangeSchema(false); + graph().schema().indexLabel("vl1ByVp4").onV("vl1") + .by("vp4").secondary().create(); + graph().addVertex(T.label, "vl1", "vp4", "marko", "age", 29); + graph().addVertex(T.label, "vl1", "vp4", "josh", "age", 32); + commitTx(); + + GraphTraversal traversal = graph().traversal().V() + .hasLabel("vl1") + .has("vp4", + TextP.containing("ar") + .negate()) + .count(); + applyAndGetGraphStep(traversal); + + Assert.assertTrue(hasRemainingHasStep(traversal, "vp4")); + Assert.assertEquals(1L, traversal.next().longValue()); + } + + @Test + public void testNegatedNaNPredicatesKeepGremlinSemantics() { + this.initNegatedDoubleSchema(); + graph().addVertex(T.label, "sample", "score", 1.0D); + graph().addVertex(T.label, "sample", "score", Double.NaN); + commitTx(); + + long notLtNaN = graph().traversal().V() + .hasLabel("sample") + .has("score", P.not(P.lt(Double.NaN))) + .count().next(); + long notEqNaN = graph().traversal().V() + .hasLabel("sample") + .has("score", P.not(P.eq(Double.NaN))) + .count().next(); + + Assert.assertEquals(2L, notLtNaN); + Assert.assertEquals(2L, notEqNaN); + } + + @Test + public void testOptimizedGraphCountCanBeResetAndReused() { + this.initSchema(); + this.initGraph(); + + GraphTraversal traversal = graph().traversal().V().count(); + + Assert.assertEquals(3L, traversal.next()); + + traversal.asAdmin().reset(); + + Assert.assertEquals(3L, traversal.next()); + } + + @Test + public void testOptimizedGraphCountEqualityIgnoresExecutionState() { + this.initSchema(); + this.initGraph(); + + GraphTraversal first = graph().traversal().V().count(); + GraphTraversal second = graph().traversal().V().count(); + first.asAdmin().applyStrategies(); + second.asAdmin().applyStrategies(); + + Step firstStep = first.asAdmin().getEndStep(); + Step secondStep = second.asAdmin().getEndStep(); + Assert.assertInstanceOf(HugeCountStep.class, firstStep); + Assert.assertInstanceOf(HugeCountStep.class, secondStep); + Assert.assertEquals(firstStep, secondStep); + + int hashCode = firstStep.hashCode(); + Set> steps = new HashSet<>(); + steps.add(firstStep); + + Assert.assertEquals(3L, first.next()); + + Assert.assertEquals(hashCode, firstStep.hashCode()); + Assert.assertEquals(firstStep, secondStep); + Assert.assertTrue(steps.contains(firstStep)); + } + + @Test + public void testOptimizedGraphCountIncludesUncommittedRecords() { + this.initSchema(); + graph().schema().indexLabel("personByName") + .onV("person").by("name").create(); + + graph().addVertex(T.label, "person", "name", "marko"); + + long count = graph().traversal().V() + .hasLabel("person") + .has("name", "marko") + .count().next(); + + Assert.assertEquals(1L, count); + } + @Test public void testWhereCountFlatAndContradictionEmpty() { this.initSchema(); @@ -338,6 +848,666 @@ public void testWhereCountFlatConnectiveStillGetsRangeBound() { Assert.assertEquals(1L, count); } + @Test + public void testVertexLimitCountRejectsUncommittedAddition() { + this.initSchema(); + graph().addVertex(T.label, "person", "name", "marko"); + + assertUncommittedRangeUnsupported( + graph().traversal().V().limit(1L).count()); + } + + @Test + public void testVertexRangeCountRejectsUncommittedDeletion() { + this.initSchema(); + graph().schema().indexLabel("personByName") + .onV("person").by("name").create(); + this.initGraph(); + Vertex marko = graph().traversal().V() + .hasLabel("person") + .has("name", "marko") + .next(); + marko.remove(); + + assertUncommittedRangeUnsupported( + graph().traversal().V().range(1L, 3L).count()); + } + + @Test + public void testSystemCountsIgnoreUncommittedVertices() { + this.initSchema(); + for (HugeType type : new HugeType[]{HugeType.TASK, HugeType.SERVER}) { + Query query = new Query(type); + query.aggregate(new Aggregate(AggregateFunc.COUNT, null)); + long before = graph().queryNumber(query).longValue(); + graph().addVertex(T.label, "person", "name", "uncommitted"); + Assert.assertEquals(before, graph().queryNumber(query).longValue()); + graph().tx().rollback(); + } + } + + @Test + public void testQueryNumberKeepsOriginalAggregate() { + this.initSchema(); + graph().addVertex(T.label, "person", "name", "marko"); + + Query query = new Query(HugeType.VERTEX); + Aggregate aggregate = new Aggregate(AggregateFunc.COUNT, null); + query.aggregate(aggregate); + + Assert.assertEquals(1L, graph().queryNumber(query).longValue()); + Assert.assertSame(aggregate, query.aggregate()); + } + + @Test + public void testUncommittedVertexCountClosesIteratorOnFailure() { + CountCloseableIterator vertices = + new CountCloseableIterator<>(true); + AtomicBoolean dirty = new AtomicBoolean(true); + GraphTransaction transaction = + this.newCountTransaction(vertices, null, dirty); + + try { + Query query = countQuery(HugeType.VERTEX); + Assert.assertThrows(IllegalStateException.class, + () -> transaction.queryNumber(query)); + Assert.assertTrue(vertices.closed()); + } finally { + dirty.set(false); + transaction.close(); + } + } + + @Test + public void testUncommittedEdgeCountClosesIteratorOnFailure() { + CountCloseableIterator edges = + new CountCloseableIterator<>(true); + AtomicBoolean dirty = new AtomicBoolean(true); + GraphTransaction transaction = + this.newCountTransaction(null, edges, dirty); + + try { + Query query = countQuery(HugeType.EDGE); + Assert.assertThrows(IllegalStateException.class, + () -> transaction.queryNumber(query)); + Assert.assertTrue(edges.closed()); + } finally { + dirty.set(false); + transaction.close(); + } + } + + @Test + public void testCommittedVertexCountClosesIterator() { + this.assertCommittedVertexCountClosesIterator(false); + } + + @Test + public void testCommittedVertexCountClosesIteratorOnFailure() { + this.assertCommittedVertexCountClosesIterator(true); + } + + private void assertCommittedVertexCountClosesIterator(boolean fail) { + SchemaManager schema = graph().schema(); + schema.propertyKey("name").asText().create(); + schema.vertexLabel("person").properties("name") + .primaryKeys("name").create(); + + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.eq(HugeKeys.LABEL, graph().vertexLabel("person").id()); + query.query(Condition.eq(graph().propertyKey("name").id(), "marko")); + query.aggregate(new Aggregate(AggregateFunc.COUNT, null)); + + CountCloseableIterator vertices = new CountCloseableIterator<>(fail); + GraphTransaction transaction = this.newCountTransaction( + vertices, null, new AtomicBoolean(false)); + try { + // Primary-key optimization produces an IdQuery that must scan to count. + if (fail) { + Assert.assertThrows(IllegalStateException.class, + () -> transaction.queryNumber(query)); + } else { + Assert.assertEquals(1L, transaction.queryNumber(query).longValue()); + } + Assert.assertTrue(vertices.closed()); + } finally { + transaction.close(); + } + } + + @Test + public void testLabelPredicatesWithNullHostileCollections() { + this.initSchema(); + Vertex person = graph().addVertex(T.label, "person", "name", "marko"); + Vertex software = graph().addVertex(T.label, "software", "name", "market"); + commitTx(); + + List> values = List.of(List.of("person"), Set.of("person")); + for (Collection labels : values) { + Assert.assertEquals(Collections.singletonList(person), + graph().traversal().V().hasLabel(P.within(labels)).toList()); + Assert.assertEquals(1L, graph().traversal().V().hasLabel(P.within(labels)) + .count().next().longValue()); + GraphTraversal without = graph().traversal().V() + .hasLabel(P.without(labels)).has("name", TextP.containing("ar")); + without.asAdmin().applyStrategies(); + Assert.assertTrue(hasRemainingHasStep(without, T.label.getAccessor())); + Assert.assertEquals(Collections.singletonList(software), without.toList()); + } + } + + @Test + public void testLabelPredicatesWithMutableNullValuesStayLocal() { + this.initSchema(); + Vertex person = graph().addVertex(T.label, "person", "name", "marko"); + Vertex software = graph().addVertex(T.label, "software", "name", "market"); + commitTx(); + + Collection list = new ArrayList<>(List.of("person")); + list.add(null); + Collection set = new HashSet<>(list); + for (Collection labels : List.of(list, set)) { + GraphTraversal within = graph().traversal().V() + .hasLabel(P.within(labels)); + within.asAdmin().applyStrategies(); + Assert.assertTrue(hasRemainingHasStep(within, T.label.getAccessor())); + Assert.assertEquals(Collections.singletonList(person), within.toList()); + GraphTraversal without = graph().traversal().V() + .hasLabel(P.without(labels)); + without.asAdmin().applyStrategies(); + Assert.assertTrue(hasRemainingHasStep(without, T.label.getAccessor())); + Assert.assertEquals(Collections.singletonList(software), without.toList()); + } + } + + @Test + public void testMixedTextNativeIdFilterKeepsAllConditionsLocal() { + this.initSchema(); + Vertex josh = graph().addVertex(T.label, "person", "name", "josh"); + Vertex marko = graph().addVertex(T.label, "person", "name", "marko"); + commitTx(); + + GraphTraversalSource[] sources = { + graph().traversal(), + graph().traversal().withoutStrategies(HugeGraphStepStrategy.class, + HugeCountStepStrategy.class) + }; + for (GraphTraversalSource source : sources) { + assertMixedTextNativeIdFilter(source, marko.id(), "ar", + Collections.singletonList(marko)); + assertMixedTextNativeIdFilter(source, josh.id(), "ar", + Collections.emptyList()); + assertMixedTextNativeIdFilter(source, marko.id(), "osh", + Collections.emptyList()); + Assert.assertEquals(Collections.singletonList(marko), + source.V().hasId(marko.id()).toList()); + Assert.assertEquals(1L, source.V().hasId(P.eq(marko.id())) + .count().next().longValue()); + } + } + + private static void assertMixedTextNativeIdFilter(GraphTraversalSource source, + Object id, String text, + List expected) { + GraphTraversal traversal = nativeIdTextQuery(source, id, text); + traversal.asAdmin().applyStrategies(); + assertMixedTextNativeIdPlan(traversal); + Assert.assertEquals(expected, traversal.toList()); + + GraphTraversal count = nativeIdTextQuery(source, id, text).count(); + assertUnoptimizedCount(expected.size(), count); + assertMixedTextNativeIdPlan(count); + Assert.assertEquals(expected, nativeIdTextQuery(source, id, text) + .limit(1).toList()); + Assert.assertEquals(Collections.emptyList(), + nativeIdTextQuery(source, id, text).range(1, 2).toList()); + } + + private static GraphTraversal nativeIdTextQuery( + GraphTraversalSource source, Object id, String text) { + return source.V().hasId(ConditionP.eq(id)) + .has("name", TextP.containing(text)); + } + + private static void assertMixedTextNativeIdPlan(GraphTraversal traversal) { + Assert.assertTrue(hasRemainingHasStep(traversal, T.id.getAccessor())); + Assert.assertTrue(hasRemainingHasStep(traversal, "name")); + Assert.assertEquals(0, ((GraphStep) traversal.asAdmin() + .getStartStep()).getIds().length); + } + + @Test + public void testMixedTextEdgeFilterKeepsAllConditionsLocal() { + this.initSchema(); + graph().schema().propertyKey("weight").asInt().create(); + graph().schema().edgeLabel("rated").link("person", "person") + .properties("name", "weight").create(); + Vertex source = graph().addVertex(T.label, "person", "name", "source"); + Vertex target = graph().addVertex(T.label, "person", "name", "target"); + source.addEdge("rated", target, "name", "marko", "weight", 1); + Vertex other = graph().addVertex(T.label, "person", "name", "other"); + Edge expected = source.addEdge("rated", other, + "name", "marko", "weight", 2); + commitTx(); + + List edges = graph().traversal().V(source.id()).outE("rated") + .has("name", TextP.containing("ar")) + .has("weight", 2).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals(expected.id(), edges.get(0).id()); + Assert.assertEquals(1L, graph().traversal().V(source.id()).outE("rated") + .has("name", TextP.containing("ar")) + .has("weight", 2).count().next().longValue()); + } + + @Test + public void testUpdatedThenRemovedVertexIsAbsentFromCounts() { + this.initSchema(); + Vertex vertex = graph().addVertex(T.label, "person", "name", "before"); + commitTx(); + Object id = vertex.id(); + vertex = graph().traversal().V(id).next(); + vertex.property("name", "after"); + vertex.remove(); + + this.assertRemovedVertexCounts(id); + graph().tx().rollback(); + Assert.assertEquals(1L, graph().traversal().V(id).count().next().longValue()); + Assert.assertEquals(1L, graph().traversal().V().hasLabel("person") + .count().next().longValue()); + Assert.assertEquals("before", graph().traversal().V(id).values("name").next()); + } + + private void assertRemovedVertexCounts(Object id) { + Assert.assertEquals(0L, graph().traversal().V(id).count().next().longValue()); + Assert.assertEquals(0L, graph().traversal().V().hasLabel("person") + .count().next().longValue()); + Assert.assertEquals(0L, graph().traversal().V().count().next().longValue()); + Assert.assertTrue(graph().traversal().V().toList().isEmpty()); + Assert.assertEquals(0L, graph().traversal().V() + .has("name", TextP.containing("after")) + .count().next().longValue()); + } + + @Test + public void testUpdatedThenRemovedEdgeIsAbsentFromCounts() { + this.assertUpdatedThenRemovedEdgeCounts(false); + } + + @Test + public void testUpdatedThenRemovedSelfLoopIsAbsentFromCounts() { + this.assertUpdatedThenRemovedEdgeCounts(true); + } + + private void assertUpdatedThenRemovedEdgeCounts(boolean selfLoop) { + this.initSchema(); + graph().schema().edgeLabel("rated").link("person", "person") + .properties("name").create(); + Vertex source = graph().addVertex(T.label, "person", "name", "source"); + Vertex target = selfLoop ? source : + graph().addVertex(T.label, "person", "name", "target"); + Edge edge = source.addEdge("rated", target, "name", "before"); + commitTx(); + Object id = edge.id(); + edge = graph().traversal().V(target.id()).inE("rated").next(); + edge.property("name", "after"); + edge.remove(); + + this.assertRemovedEdgeCounts(id, source, target); + graph().tx().rollback(); + Assert.assertEquals(1L, graph().traversal().E(id).count().next().longValue()); + Assert.assertEquals(1L, graph().traversal().E().hasLabel("rated") + .count().next().longValue()); + Assert.assertEquals(selfLoop ? 2L : 1L, + graph().traversal().V(source.id()).bothE("rated") + .count().next().longValue()); + Assert.assertEquals("before", graph().traversal().E(id).values("name").next()); + } + + private void assertRemovedEdgeCounts(Object id, Vertex source, Vertex target) { + Assert.assertEquals(0L, graph().traversal().E(id).count().next().longValue()); + Assert.assertEquals(0L, graph().traversal().V(source.id()).bothE("rated") + .count().next().longValue()); + Assert.assertTrue(graph().traversal().V(source.id()).bothE("rated") + .toList().isEmpty()); + Assert.assertEquals(0L, graph().traversal().V(source.id()).outE("rated") + .count().next().longValue()); + Assert.assertEquals(0L, graph().traversal().V(target.id()).inE("rated") + .count().next().longValue()); + Assert.assertEquals(0L, graph().traversal().E().hasLabel("rated") + .count().next().longValue()); + Assert.assertEquals(0L, graph().traversal().E().count().next().longValue()); + Assert.assertTrue(graph().traversal().E().toList().isEmpty()); + } + + @Test + public void testDirtyVertexIndexCountRemainsUnsupported() { + this.initSchema(); + graph().schema().indexLabel("personByName").onV("person") + .by("name").secondary().create(); + Vertex vertex = graph().addVertex(T.label, "person", "name", "before"); + commitTx(); + vertex = graph().traversal().V(vertex.id()).next(); + vertex.property("name", "after"); + + Assert.assertEquals(1L, graph().traversal().V(vertex.id()) + .count().next().longValue()); + Assert.assertEquals(1L, graph().traversal().V().count().next().longValue()); + assertDirtyIndexCountUnsupported(graph().traversal().V() + .hasLabel("person").has("name", "before").count()); + this.assertDirtyIndexLabelCount(graph().traversal().V() + .hasLabel("person").count()); + } + + @Test + public void testDirtyEdgeIndexCountRemainsUnsupported() { + this.initSchema(); + graph().schema().edgeLabel("rated").link("person", "person") + .properties("name").create(); + graph().schema().indexLabel("ratedByName").onE("rated") + .by("name").secondary().create(); + Vertex source = graph().addVertex(T.label, "person", "name", "source"); + Vertex target = graph().addVertex(T.label, "person", "name", "target"); + Edge edge = source.addEdge("rated", target, "name", "before"); + commitTx(); + edge = graph().traversal().E(edge.id()).next(); + edge.property("name", "after"); + + Assert.assertEquals(1L, graph().traversal().E(edge.id()) + .count().next().longValue()); + Assert.assertEquals(1L, graph().traversal().E().count().next().longValue()); + assertDirtyIndexCountUnsupported(graph().traversal().E() + .hasLabel("rated").has("name", "before").count()); + this.assertDirtyIndexLabelCount(graph().traversal().E() + .hasLabel("rated").count()); + } + + private void assertDirtyIndexLabelCount(GraphTraversal traversal) { + if (storeFeatures().supportsQueryByLabel()) { + Assert.assertEquals(1L, traversal.next().longValue()); + } else { + assertDirtyIndexCountUnsupported(traversal); + } + } + + private static void assertDirtyIndexCountUnsupported(GraphTraversal traversal) { + Assert.assertThrows(HugeException.class, traversal::next, e -> { + Assert.assertContains("Can't do index query when there are " + + "changes in transaction", e.getMessage()); + }); + } + + @Test + public void testRemovedVertexCanBeReaddedWithSameId() { + this.initSchema(); + graph().schema().vertexLabel("reused").properties("name") + .useCustomizeStringId().create(); + Vertex vertex = graph().addVertex(T.label, "reused", T.id, "same", + "name", "before"); + commitTx(); + vertex = graph().traversal().V(vertex.id()).next(); + vertex.property("name", "changed"); + vertex.remove(); + Vertex replacement = graph().addVertex(T.label, "reused", T.id, "same", + "name", "again"); + + Assert.assertEquals(1L, graph().traversal().V(replacement.id()) + .count().next().longValue()); + Assert.assertEquals(1L, graph().traversal().V().hasLabel("reused") + .count().next().longValue()); + Assert.assertEquals(Collections.singletonList(replacement), + graph().traversal().V().toList()); + Assert.assertEquals("again", graph().traversal().V(replacement.id()) + .values("name").next()); + } + + @Test + public void testReaddedVertexShadowsUpdatedVertexBeforeLabelMatching() { + SchemaManager schema = graph().schema(); + schema.propertyKey("name").asText().create(); + schema.vertexLabel("person").properties("name") + .useCustomizeStringId().create(); + schema.vertexLabel("software").properties("name") + .useCustomizeStringId().create(); + Vertex vertex = graph().addVertex(T.label, "person", T.id, "same", + "name", "before"); + commitTx(); + Object id = vertex.id(); + vertex = graph().traversal().V(id).next(); + vertex.property("name", "after"); + vertex.remove(); + Vertex replacement = graph().addVertex(T.label, "software", T.id, "same", + "name", "again"); + + Assert.assertEquals(0L, graph().traversal().V(id).hasLabel("person") + .count().next().longValue()); + Assert.assertTrue(graph().traversal().V(id).hasLabel("person") + .toList().isEmpty()); + Assert.assertEquals(0L, graph().traversal().V().hasLabel("person") + .count().next().longValue()); + Assert.assertTrue(graph().traversal().V().hasLabel("person") + .toList().isEmpty()); + Assert.assertEquals(1L, graph().traversal().V(id).hasLabel("software") + .count().next().longValue()); + Assert.assertEquals(1L, graph().traversal().V().hasLabel("software") + .count().next().longValue()); + Assert.assertEquals(Collections.singletonList(replacement), + graph().traversal().V().hasLabel("software").toList()); + Assert.assertEquals(Collections.singletonList(replacement), + graph().traversal().V(id).toList()); + Assert.assertEquals(Collections.singletonList(replacement), + graph().traversal().V().toList()); + Assert.assertEquals(1L, graph().traversal().V().count().next().longValue()); + Assert.assertEquals("again", graph().traversal().V(id).values("name").next()); + + graph().tx().rollback(); + Assert.assertEquals(1L, graph().traversal().V().hasLabel("person") + .count().next().longValue()); + Assert.assertEquals(0L, graph().traversal().V().hasLabel("software") + .count().next().longValue()); + Assert.assertEquals("before", graph().traversal().V(id).values("name").next()); + } + + @Test + public void testRemovedEdgeCanBeReaddedWithSameId() { + this.initSchema(); + graph().schema().edgeLabel("rated").link("person", "person") + .properties("name").create(); + Vertex source = graph().addVertex(T.label, "person", "name", "source"); + Vertex target = graph().addVertex(T.label, "person", "name", "target"); + Edge edge = source.addEdge("rated", target, "name", "before"); + commitTx(); + edge = graph().traversal().E(edge.id()).next(); + edge.property("name", "changed"); + edge.remove(); + Edge replacement = source.addEdge("rated", target, "name", "again"); + Assert.assertEquals(edge.id(), replacement.id()); + Assert.assertEquals(1L, graph().traversal().E(replacement.id()) + .count().next().longValue()); + Assert.assertEquals(1L, graph().traversal().E().hasLabel("rated") + .count().next().longValue()); + Assert.assertEquals(Collections.singletonList(replacement), + graph().traversal().E().toList()); + Assert.assertEquals("again", graph().traversal().E(replacement.id()) + .values("name").next()); + Assert.assertTrue(graph().traversal().E().hasLabel("rated") + .has("name", TextP.containing("changed")) + .toList().isEmpty()); + Assert.assertEquals(0L, graph().traversal().E().hasLabel("rated") + .has("name", TextP.containing("changed")) + .count().next().longValue()); + Assert.assertEquals(0L, graph().traversal().E(replacement.id()) + .has("name", TextP.containing("changed")) + .count().next().longValue()); + Assert.assertEquals(Collections.singletonList(replacement), + graph().traversal().E().hasLabel("rated") + .has("name", TextP.containing("again")).toList()); + Assert.assertEquals(1L, graph().traversal().E().hasLabel("rated") + .has("name", TextP.containing("again")) + .count().next().longValue()); + + graph().tx().rollback(); + Assert.assertEquals(1L, graph().traversal().E(replacement.id()) + .count().next().longValue()); + Assert.assertEquals("before", graph().traversal().E(replacement.id()) + .values("name").next()); + } + + @Test + public void testAddedSelfLoopCountKeepsBothDirections() { + this.initSchema(); + Vertex vertex = graph().addVertex(T.label, "person", "name", "loop"); + vertex.addEdge("knows", vertex); + this.assertSelfLoopCounts(vertex); + commitTx(); + this.assertSelfLoopCounts(vertex); + } + + @Test + public void testUpdatedSelfLoopCountKeepsBothDirections() { + this.initSchema(); + graph().schema().edgeLabel("rated").link("person", "person") + .properties("name").create(); + Vertex vertex = graph().addVertex(T.label, "person", "name", "loop"); + Edge loop = vertex.addEdge("rated", vertex, "name", "before"); + commitTx(); + loop = graph().traversal().E(loop.id()).next(); + loop.property("name", "after"); + this.assertSelfLoopCounts(vertex); + commitTx(); + this.assertSelfLoopCounts(vertex); + loop.remove(); + Assert.assertEquals(0L, graph().traversal().V(vertex.id()) + .bothE().count().next().longValue()); + } + + private void assertSelfLoopCounts(Vertex vertex) { + Assert.assertEquals(2L, graph().traversal().V(vertex.id()) + .bothE().count().next().longValue()); + Assert.assertEquals(2, graph().traversal().V(vertex.id()) + .bothE().toList().size()); + Assert.assertEquals(1L, graph().traversal().V(vertex.id()) + .outE().count().next().longValue()); + Assert.assertEquals(1L, graph().traversal().V(vertex.id()) + .inE().count().next().longValue()); + Assert.assertEquals(1L, graph().traversal().E().count().next().longValue()); + } + + @Test + public void testOptimizedEdgeCountIncludesUncommittedRecords() { + this.initSchema(); + graph().schema().indexLabel("personByName") + .onV("person").by("name").create(); + this.initGraph(); + + Vertex josh = graph().traversal().V() + .hasLabel("person").has("name", "josh").next(); + Vertex marko = graph().traversal().V() + .hasLabel("person").has("name", "marko").next(); + josh.addEdge("knows", marko); + + long count = graph().traversal().E().hasLabel("knows").count().next(); + + Assert.assertEquals(2L, count); + } + + private static Query countQuery(HugeType type) { + Query query = new Query(type); + query.aggregate(new Aggregate(AggregateFunc.COUNT, null)); + return query; + } + + private GraphTransaction newCountTransaction( + Iterator vertices, Iterator edges, + AtomicBoolean dirty) { + return new GraphTransaction(params(), params().loadGraphStore()) { + + @Override + public boolean hasUpdate() { + return dirty.get(); + } + + @Override + public Iterator queryVertices(Query query) { + return vertices; + } + + @Override + public Iterator queryEdges(Query query) { + return edges; + } + }; + } + + private static final class CountCloseableIterator + implements CloseableIterator { + + private final boolean fail; + private boolean consumed; + private boolean closed; + + private CountCloseableIterator(boolean fail) { + this.fail = fail; + } + + @Override + public boolean hasNext() { + if (this.fail) { + throw new IllegalStateException("Injected iterator failure"); + } + return !this.consumed; + } + + @Override + public T next() { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + this.consumed = true; + return null; + } + + @Override + public void close() { + this.closed = true; + } + + public boolean closed() { + return this.closed; + } + } + + @Test + public void testEdgeRangeCountRejectsUncommittedAddition() { + this.initSchema(); + graph().schema().indexLabel("personByName") + .onV("person").by("name").create(); + this.initGraph(); + Vertex josh = graph().traversal().V() + .hasLabel("person") + .has("name", "josh") + .next(); + Vertex marko = graph().traversal().V() + .hasLabel("person") + .has("name", "marko") + .next(); + josh.addEdge("knows", marko); + + assertUncommittedRangeUnsupported( + graph().traversal().E().range(1L, 3L).count()); + } + + @Test + public void testEdgeLimitCountRejectsUncommittedDeletion() { + this.initSchema(); + this.initGraph(); + Edge edge = graph().traversal().E().hasLabel("knows").next(); + edge.remove(); + + assertUncommittedRangeUnsupported( + graph().traversal().E().limit(1L).count()); + } + @Test public void testRepeatAfterTextRangeFilterWithEmptyResult() { this.initTextRangeSchema(true); @@ -382,6 +1552,30 @@ public void testTextRangeFilterKeepsMixedGraphHasStep() { Assert.assertEquals(direct, viaMatch); } + @Test + public void testTextRangeFilterKeepsIndexedGraphHasStepLocal() { + this.initTextRangeSchema(false); + graph().schema().indexLabel("vl1ByAge").onV("vl1") + .by("age").secondary().create(); + + graph().addVertex(T.label, "vl1", "vp4", "a", "age", 1); + graph().addVertex(T.label, "vl1", "vp4", "b", "age", 2); + commitTx(); + + GraphTraversal traversal = graph().traversal().V() + .hasLabel("vl1") + .has("vp4", P.lt("")) + .has("age", 1) + .count(); + HugeGraphStep graphStep = applyAndGetGraphStep(traversal); + + Assert.assertTrue(graphStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasRemainingHasStep(traversal, T.label.getAccessor())); + Assert.assertTrue(hasRemainingHasStep(traversal, "vp4")); + Assert.assertTrue(hasRemainingHasStep(traversal, "age")); + Assert.assertEquals(0L, traversal.next().longValue()); + } + @Test public void testTextRangeFilterKeepsMixedVertexHasStep() { this.initTextRangeSchema(true); @@ -587,6 +1781,53 @@ public void testMatchWithIndexedRangeConditionStillExtractsHas() { Assert.assertEquals(1L, traversal.next()); } + @Test + public void testMatchWithNegatedBooleanPredicateKeepsHas() { + this.initMatchNoIndexSchema(); + graph().schema().indexLabel("vl1ByVp2").onV("vl1") + .by("vp2").secondary().create(); + this.initMatchNoIndexGraph(); + + GraphTraversal traversal = graph().traversal().V() + .has("vp2", + P.not(P.eq(true))) + .match(__.as("s") + .has("vp2") + .as("m")) + .select("m") + .count(); + + HugeGraphStep graphStep = applyAndGetGraphStep(traversal); + Assert.assertEquals(0, graphStep.getHasContainers().size()); + Assert.assertTrue(hasRemainingHasStep(traversal, "vp2")); + Assert.assertEquals(1L, traversal.next()); + } + + @Test + public void testNegatedBooleanComparisonsKeepGremlinSemantics() { + this.initMatchNoIndexSchema(); + graph().schema().indexLabel("vl1ByVp2").onV("vl1") + .by("vp2").secondary().create(); + this.initMatchNoIndexGraph(); + + this.assertNegatedBooleanPredicate(1L, P.eq(true)); + this.assertNegatedBooleanPredicate(1L, P.eq(false)); + this.assertNegatedBooleanPredicate(1L, P.neq(true)); + this.assertNegatedBooleanPredicate(1L, P.neq(false)); + this.assertNegatedBooleanPredicate(1L, P.lt(true)); + this.assertNegatedBooleanPredicate(2L, P.lt(false)); + this.assertNegatedBooleanPredicate(0L, P.lte(true)); + this.assertNegatedBooleanPredicate(1L, P.lte(false)); + this.assertNegatedBooleanPredicate(2L, P.gt(true)); + this.assertNegatedBooleanPredicate(1L, P.gt(false)); + this.assertNegatedBooleanPredicate(1L, P.gte(true)); + this.assertNegatedBooleanPredicate(0L, P.gte(false)); + this.assertNegatedBooleanPredicate(1L, + P.eq(true).and(P.gte(false))); + this.assertNegatedBooleanPredicate(0L, + P.eq(true).or(P.lt(true))); + } + @Test public void testMatchWithNoIndexConditionKeepsExtractingNextHas() { this.initMatchNoIndexSchema(); @@ -721,6 +1962,32 @@ public void testMatchWithIndexedNumericNeqConditionKeepsHas() { Assert.assertEquals(0L, traversal.next()); } + @Test + public void testMatchWithNegatedNumericRangeConditionKeepsHas() { + this.initMatchNoIndexSchema(); + graph().schema().indexLabel("vl0ByVp3").onV("vl0") + .by("vp3").range().create(); + graph().schema().indexLabel("vl1ByVp2").onV("vl1") + .by("vp2").secondary().create(); + this.initMatchNoIndexGraph(); + + GraphTraversal traversal = graph().traversal().V() + .has("vp3", P.not(P.lte( + 4592737712018141718L))) + .has("vp2", true) + .match(__.as("s") + .has("vp2") + .as("m")) + .select("m") + .count(); + + HugeGraphStep graphStep = applyAndGetGraphStep(traversal); + Assert.assertEquals(0, graphStep.getHasContainers().size()); + Assert.assertTrue(hasRemainingHasStep(traversal, "vp3")); + Assert.assertTrue(hasRemainingHasStep(traversal, "vp2")); + Assert.assertEquals(0L, traversal.next()); + } + @Test public void testMatchWithSystemRangeConditionMatchesDirectTraversal() { this.initMatchNoIndexSchema(); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java index 9ea6b0e919..aa414df0ae 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java @@ -30,24 +30,24 @@ import java.util.concurrent.ExecutionException; import java.util.function.Function; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.BackendException; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.page.PageInfo; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.backend.store.BackendTable; -import org.apache.hugegraph.backend.store.Shard; +import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.exception.LimitExceedException; import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.Userdata; +import org.apache.hugegraph.struct.schema.Userdata; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.testutil.Assert; @@ -213,6 +213,48 @@ protected void initStrikeIndex() { .by("place", "tool", "reason").create(); } + @Test + public void testDeleteEdgeWithMissingSchemaLabel() throws InterruptedException, ExecutionException { + HugeGraph graph = graph(); + Vertex author = graph.addVertex(T.label, "author", "id", 1, "name", "James", + "age", 62, "lived", "Canadian"); + Vertex book = graph.addVertex(T.label, "book", "name", "Java"); + Edge edge = author.addEdge("authored", book, "score", 3); + graph.tx().commit(); + + // Preserve the backend edge while removing its schema metadata. + params().schemaTransaction().removeSchema(graph.edgeLabel("authored")); + params().graphEventHub().notify(Events.CACHE, "clear", null).get(); + Edge orphan = graph.edges(edge.id()).next(); + Assert.assertEquals("~undefined", orphan.label()); + + orphan.remove(); + graph.tx().commit(); + Assert.assertFalse(graph.edges(edge.id()).hasNext()); + Assert.assertFalse(graph.vertices(author.id()).next().edges(Direction.OUT).hasNext()); + Assert.assertFalse(graph.vertices(book.id()).next().edges(Direction.IN).hasNext()); + } + + @Test + public void testDeleteVertexAdjacentToEdgeWithMissingSchemaLabel() throws InterruptedException, ExecutionException { + HugeGraph graph = graph(); + Vertex author = graph.addVertex(T.label, "author", "id", 1, "name", "James", + "age", 62, "lived", "Canadian"); + Vertex book = graph.addVertex(T.label, "book", "name", "Java"); + Edge edge = author.addEdge("authored", book, "score", 3); + graph.tx().commit(); + + params().schemaTransaction().removeSchema(graph.edgeLabel("authored")); + params().graphEventHub().notify(Events.CACHE, "clear", null).get(); + Assert.assertEquals("~undefined", graph.edges(edge.id()).next().label()); + + graph.vertices(author.id()).next().remove(); + graph.tx().commit(); + Assert.assertFalse(graph.vertices(author.id()).hasNext()); + Assert.assertFalse(graph.edges(edge.id()).hasNext()); + Assert.assertFalse(graph.vertices(book.id()).next().edges(Direction.IN).hasNext()); + } + @Test public void testAddEdge() { HugeGraph graph = graph(); @@ -755,7 +797,7 @@ public void testAddEdgeWithSecondaryIndexAndTtl() { "date", "2019-12-23 12:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("place", "library of school"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge, edges.next()); @@ -767,7 +809,7 @@ public void testAddEdgeWithSecondaryIndexAndTtl() { // Ignore } - edges = graph().traversal().E().has("place", "library of school"); + edges = graph().traversal().E().hasLabel("read").has("place", "library of school"); Assert.assertFalse(edges.hasNext()); } @@ -797,27 +839,27 @@ public void testAddEdgeWithRangeIndexAndTtl() { "date", "2019-12-23 16:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("date", "2019-12-23 14:00:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge3, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge5, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge1, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -829,22 +871,22 @@ public void testAddEdgeWithRangeIndexAndTtl() { // Ignore } - edges = graph().traversal().E().has("date", "2019-12-23 14:00:00"); + edges = graph().traversal().E().hasLabel("read").has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); } @@ -878,31 +920,31 @@ public void testAddEdgeWithShardIndexAndTtl() { "date", "2019-12-23 14:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E().has("place", "home"); + Iterator edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge6, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge3, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge5, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge1, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -914,26 +956,26 @@ public void testAddEdgeWithShardIndexAndTtl() { // Ignore } - edges = graph().traversal().E().has("place", "home"); + edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); } @@ -951,7 +993,7 @@ public void testAddEdgeWithSearchIndexAndTtl() { "date", "2019-12-23 12:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("place", Text.contains("library")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge, edges.next()); @@ -963,7 +1005,7 @@ public void testAddEdgeWithSearchIndexAndTtl() { // Ignore } - edges = graph().traversal().E().has("place", Text.contains("library")); + edges = graph().traversal().E().hasLabel("read").has("place", Text.contains("library")); Assert.assertFalse(edges.hasNext()); } @@ -1010,7 +1052,7 @@ public void testOverrideEdgeWithSecondaryIndexAndTtl() { "date", "2019-12-23 12:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("place", "library of school"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge, edges.next()); @@ -1028,10 +1070,10 @@ public void testOverrideEdgeWithSecondaryIndexAndTtl() { graph().tx().commit(); // Due to overridden edges are expired, query will lead to async delete - edges = graph().traversal().E().has("place", "library of school"); + edges = graph().traversal().E().hasLabel("read").has("place", "library of school"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "home"); + edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge, edges.next()); graph().tx().commit(); @@ -1043,9 +1085,9 @@ public void testOverrideEdgeWithSecondaryIndexAndTtl() { } // All edges are expired after 6s - edges = graph().traversal().E().has("place", "library of school"); + edges = graph().traversal().E().hasLabel("read").has("place", "library of school"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "home"); + edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertFalse(edges.hasNext()); } @@ -1075,27 +1117,27 @@ public void testOverrideEdgeWithRangeIndexAndTtl() { "date", "2019-12-23 16:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("date", "2019-12-23 14:00:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge3, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge5, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge1, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -1123,29 +1165,29 @@ public void testOverrideEdgeWithRangeIndexAndTtl() { "date", "2019-12-23 16:01:00"); graph().tx().commit(); // Due to overridden edges are expired, query will lead to async delete - edges = graph().traversal().E().has("date", "2019-12-23 14:00:00"); + edges = graph().traversal().E().hasLabel("read").has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("date", "2019-12-23 14:01:00"); + edges = graph().traversal().E().hasLabel("read").has("date", "2019-12-23 14:01:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge8, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge10, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge6, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -1158,25 +1200,25 @@ public void testOverrideEdgeWithRangeIndexAndTtl() { } // All edges are expired after 6s - edges = graph().traversal().E().has("date", "2019-12-23 14:00:00"); + edges = graph().traversal().E().hasLabel("read").has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("date", "2019-12-23 14:01:00"); + edges = graph().traversal().E().hasLabel("read").has("date", "2019-12-23 14:01:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:01:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:01:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:01:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:01:00")); Assert.assertFalse(edges.hasNext()); } @@ -1210,31 +1252,31 @@ public void testOverrideEdgeWithShardIndexAndTtl() { "date", "2019-12-23 14:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E().has("place", "home"); + Iterator edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge6, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge3, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge5, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge1, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -1265,38 +1307,38 @@ public void testOverrideEdgeWithShardIndexAndTtl() { graph().tx().commit(); // Due to overridden edges are expired, query will lead to async delete - edges = graph().traversal().E().has("place", "home"); + edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library"); + edges = graph().traversal().E().hasLabel("read").has("place", "library"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge12, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:01:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge9, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge11, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge7, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -1309,33 +1351,33 @@ public void testOverrideEdgeWithShardIndexAndTtl() { } // All edges are expired after 6s - edges = graph().traversal().E().has("place", "home"); + edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library"); + edges = graph().traversal().E().hasLabel("read").has("place", "library"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:01:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); } @@ -1353,7 +1395,7 @@ public void testOverrideEdgeWithSearchIndexAndTtl() { "date", "2019-12-23 12:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("place", Text.contains("school")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge, edges.next()); @@ -1370,10 +1412,10 @@ public void testOverrideEdgeWithSearchIndexAndTtl() { graph().tx().commit(); // Due to overridden edges are expired, query will lead to async delete - edges = graph().traversal().E().has("place", Text.contains("school")); + edges = graph().traversal().E().hasLabel("read").has("place", Text.contains("school")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", Text.contains("city")); + edges = graph().traversal().E().hasLabel("read").has("place", Text.contains("city")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge, edges.next()); graph().tx().commit(); @@ -1385,7 +1427,7 @@ public void testOverrideEdgeWithSearchIndexAndTtl() { } // All edges are expired after 6s - edges = graph().traversal().E().has("place", Text.contains("library")); + edges = graph().traversal().E().hasLabel("read").has("place", Text.contains("library")); Assert.assertFalse(edges.hasNext()); } @@ -1445,7 +1487,7 @@ public void testQueryEdgeWithSecondaryIndexAndTtlInTx() { baby.addEdge("read", java2, "place", "library of school", "date", "2019-12-23 12:00:00"); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("place", "library of school"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -1458,7 +1500,7 @@ public void testQueryEdgeWithSecondaryIndexAndTtlInTx() { } // All edges are expired after 3s - edges = graph().traversal().E().has("place", "library of school"); + edges = graph().traversal().E().hasLabel("read").has("place", "library of school"); Assert.assertFalse(edges.hasNext()); } @@ -1503,27 +1545,27 @@ public void testQueryEdgeWithRangeIndexAndTtlInTx() { baby.addEdge("read", java10, "place", "library of school", "date", "2019-12-23 16:00:00"); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("date", "2019-12-23 14:00:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(4, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(4, IteratorUtils.count(edges)); @@ -1536,22 +1578,22 @@ public void testQueryEdgeWithRangeIndexAndTtlInTx() { } // All edges are expired after 3s - edges = graph().traversal().E().has("date", "2019-12-23 14:00:00"); + edges = graph().traversal().E().hasLabel("read").has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); } @@ -1603,31 +1645,31 @@ public void testQueryEdgeWithShardIndexAndTtlInTx() { baby.addEdge("read", java12, "place", "home", "date", "2019-12-23 14:00:00"); - Iterator edges = graph().traversal().E().has("place", "home"); + Iterator edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(4, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(4, IteratorUtils.count(edges)); @@ -1640,26 +1682,26 @@ public void testQueryEdgeWithShardIndexAndTtlInTx() { } // All edges are expired after 3s - edges = graph().traversal().E().has("place", "home"); + edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); } @@ -1682,7 +1724,7 @@ public void testQueryEdgeWithSearchIndexAndTtlInTx() { baby.addEdge("read", java2, "place", "library of school", "date", "2019-12-23 12:00:00"); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("place", Text.contains("library")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -1695,7 +1737,7 @@ public void testQueryEdgeWithSearchIndexAndTtlInTx() { } // All edges are expired after 3s - edges = graph().traversal().E().has("place", Text.contains("library")); + edges = graph().traversal().E().hasLabel("read").has("place", Text.contains("library")); Assert.assertFalse(edges.hasNext()); } @@ -3176,9 +3218,7 @@ public void testQueryAdjacentVerticesOfEdges() { // Fill edge properties Assert.assertEquals(2, edge.getProperties().size()); - Whitebox.setInternalState(edge, "propLoaded", false); - Whitebox.setInternalState(edge, "properties", - CollectionFactory.newIntObjectMap()); + edge.resetProperties(); Assert.assertEquals(0, edge.getProperties().size()); Assert.assertEquals(2, edge.getFilledProperties().size()); Assert.assertEquals(2, edge.getProperties().size()); @@ -3559,6 +3599,265 @@ public void testQueryOutEdgesOfVertexBySortkeyAndProps() { Assert.assertEquals(0, edges.size()); } + @Test + public void testQueryOutEdgesBySingleResolvedLabelAndSortKey() { + HugeGraph graph = graph(); + Vertex reader = initEdgeLabelQueryEdges(); + + List edges = graph.traversal().V(reader.id()) + .outE("reviewed") + .has(T.label, P.within("reviewed", + "recommended")) + .has("time", "2026-1-1") + .toList(); + + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("reviewed", edges.get(0).label()); + Assert.assertEquals("2026-1-1", edges.get(0).value("time")); + } + + @Test + public void testQueryOutEdgesByMultiLabelsAndSortKey() { + HugeGraph graph = graph(); + Vertex reader = initEdgeLabelQueryEdges(); + + List edges = graph.traversal().V(reader.id()) + .outE("reviewed", "recommended") + .has("time", "2026-1-1") + .toList(); + + Set labels = new HashSet<>(); + for (Edge edge : edges) { + labels.add(edge.label()); + Assert.assertEquals("2026-1-1", edge.value("time")); + } + Assert.assertEquals(2, edges.size()); + Assert.assertEquals(ImmutableSet.of("reviewed", "recommended"), + labels); + } + + @Test + public void testQueryEdgesByNonEqLabel() { + HugeGraph graph = graph(); + init18Edges(); + + List edges = graph.traversal().E() + .has(T.label, P.neq("created")) + .toList(); + Assert.assertEquals(16, edges.size()); + for (Edge edge : edges) { + Assert.assertNotEquals("created", edge.label()); + } + } + + @Test + public void testPageOutEdgesWithNegativeLabelBeforeLimit() { + this.assertOutEdgeNegativeLabelPages(true); + } + + @Test + public void testPageOutEdgesWithNegativeLabelAfterLimit() { + this.assertOutEdgeNegativeLabelPages(false); + } + + private void assertOutEdgeNegativeLabelPages(boolean filterBeforeLimit) { + Assume.assumeTrue("Not support paging", storeFeatures().supportsQueryByPage()); + HugeGraph graph = graph(); + graph.schema().vertexLabel("pageVertex").useCustomizeNumberId().create(); + graph.schema().edgeLabel("pageExcluded").link("pageVertex", "pageVertex").create(); + graph.schema().edgeLabel("pageIncluded").link("pageVertex", "pageVertex").create(); + Vertex source = graph.addVertex(T.label, "pageVertex", T.id, 1); + Set expected = new HashSet<>(); + for (int i = 0; i < 9; i++) { + Vertex target = graph.addVertex(T.label, "pageVertex", T.id, i + 2); + Edge edge = source.addEdge(i < 6 ? "pageExcluded" : "pageIncluded", target); + if (i >= 6) { + expected.add(edge.id()); + } + } + this.commitTx(); + + Set actual = new HashSet<>(); + Set cursors = new HashSet<>(); + String page = ""; + int pages = 0; + boolean emptyPageWithCursor = false; + do { + GraphTraversal traversal = graph.traversal().V(source.id()) + .outE().has("~page", page); + if (filterBeforeLimit) { + traversal.hasLabel(P.neq("pageExcluded")).limit(1); + } else { + traversal.limit(1).hasLabel(P.neq("pageExcluded")); + } + traversal.asAdmin().applyStrategies(); + Assert.assertNotNull(TraversalUtil.firstPageStep(traversal)); + List edges = traversal.toList(); + Assert.assertTrue(edges.size() <= 1); + for (Edge edge : edges) { + Assert.assertEquals("pageIncluded", edge.label()); + Assert.assertTrue("Duplicate edge across pages", actual.add(edge.id())); + } + page = TraversalUtil.page(traversal); + if (page != null && !page.isEmpty()) { + Assert.assertTrue("Repeated continuation cursor", cursors.add(page)); + emptyPageWithCursor |= edges.isEmpty(); + } + Assert.assertTrue("Paging did not terminate", ++pages <= 10); + } while (page != null && !page.isEmpty()); + Assert.assertEquals(expected, actual); + // An empty filtered page must not terminate iteration prematurely. + Assert.assertTrue(emptyPageWithCursor); + } + + @Test + public void testLocalEdgeIdAndLabelRepresentations() { + HugeGraph graph = graph(); + graph.schema().vertexLabel("localEV").useCustomizeNumberId().create(); + graph.schema().edgeLabel("localE").link("localEV", "localEV").create(); + Vertex source = graph.addVertex(T.label, "localEV", T.id, 123); + Vertex target = graph.addVertex(T.label, "localEV", T.id, 456); + Edge edge = source.addEdge("localE", target); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertEquals(ImmutableList.of(edge), g.E().hasId(edge).toList()); + Assert.assertEquals(ImmutableList.of(edge), + g.E().hasId(edge).limit(10).hasLabel(P.neq("other")).toList()); + Assert.assertEquals(ImmutableList.of(edge), + g.V(source.id()).outE().hasId(P.within(edge)).limit(10) + .hasLabel(P.neq("other")).toList()); + Id label = graph.edgeLabel("localE").id(); + for (Object value : ImmutableList.of("localE", label, label.asLong())) { + Assert.assertEquals(ImmutableList.of(edge), g.E().has(T.label, value).toList()); + Assert.assertEquals(ImmutableList.of(edge), + g.E().has(T.label, value).limit(10) + .hasLabel(P.neq("other")).toList()); + Assert.assertEquals(ImmutableList.of(edge), + g.V(source.id()).outE().barrier().has(T.label, P.within(value)).limit(10) + .hasLabel(P.neq("other")).toList()); + } + } + + @Test + public void testQueryEdgesByNonEqLabelAndIndexedPropertyAcrossBarrier() { + HugeGraph graph = graph(); + initEdgeLabelQueryEdges(); + + GraphTraversalSource g = graph.traversal(); + + List edges = g.E().has(T.label, P.neq("reviewed")) + .barrier().has("score", 2).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + + edges = g.E().has("score", 2).barrier() + .has(T.label, P.neq("reviewed")).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + } + + @Test + public void testQueryEdgesByNonEqLabelAndIndexedPropertyAcrossRange() { + HugeGraph graph = graph(); + initEdgeLabelQueryEdges(); + + GraphTraversalSource g = graph.traversal(); + + List edges = g.E().has("score", 2).skip(0) + .has(T.label, P.neq("reviewed")).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + + edges = g.E().has("score", 2).limit(1000) + .has(T.label, P.neq("reviewed")).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + } + + @Test + public void testQueryEdgesByNonEqLabelAndIndexedPropertyAcrossMixedKeyOr() { + HugeGraph graph = graph(); + initEdgeLabelQueryEdges(); + + GraphTraversalSource g = graph.traversal(); + + List edges = g.E().has("score", 2) + .or(__.has(T.label, P.neq("reviewed")), + __.has("time", "2026-1-1")) + .toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + } + + @Test + public void testQueryEdgesByNonEqLabelAndIndexedPropertyAcrossSideEffect() { + HugeGraph graph = graph(); + initEdgeLabelQueryEdges(); + + GraphTraversalSource g = graph.traversal(); + + List edges = g.E().has("score", 2).aggregate("x") + .has(T.label, P.neq("reviewed")).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + + edges = g.E().has("score", 2).coin(1.0D) + .has(T.label, P.neq("reviewed")).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + } + + @Test + public void testQueryEdgesByMixedConnectiveLabel() { + HugeGraph graph = graph(); + init18Edges(); + + GraphTraversalSource g = graph.traversal(); + + List edges = g.E() + .has(T.label, P.eq("created") + .or(P.neq("authored"))) + .toList(); + Assert.assertEquals(15, edges.size()); + for (Edge edge : edges) { + Assert.assertNotEquals("authored", edge.label()); + } + + edges = g.E() + .has(T.label, P.eq("created") + .and(P.neq("authored"))) + .toList(); + Assert.assertEquals(2, edges.size()); + for (Edge edge : edges) { + Assert.assertEquals("created", edge.label()); + } + } + + @Test + public void testQueryEdgesByMultipleNegativeLabelContainers() { + HugeGraph graph = graph(); + init18Edges(); + + GraphTraversalSource g = graph.traversal(); + + List edges = g.E().hasLabel("created") + .has(T.label, P.neq("authored")) + .toList(); + Assert.assertEquals(2, edges.size()); + for (Edge edge : edges) { + Assert.assertEquals("created", edge.label()); + } + + edges = g.E().has(T.label, P.neq("created")) + .has(T.label, P.neq("authored")) + .toList(); + Assert.assertEquals(13, edges.size()); + for (Edge edge : edges) { + Assert.assertNotEquals("created", edge.label()); + Assert.assertNotEquals("authored", edge.label()); + } + } + @Test public void testQueryOutEdgesOfVertexBySortkeyWithRange() { // FIXME: The legacy HStore guard and related coverage debt are tracked in @@ -4729,10 +5028,10 @@ public void testQueryByLongPropOfOverrideEdge() { louise.addEdge("strike", sean, "id", 1, "timestamp", current + 1, "place", "park", "tool", "shovel", "reason", "jeer", "arrested", false); - List edges = graph.traversal().E().has("timestamp", current) + List edges = graph.traversal().E().hasLabel("strike").has("timestamp", current) .toList(); Assert.assertEquals(0, edges.size()); - edges = graph.traversal().E().has("timestamp", current + 1).toList(); + edges = graph.traversal().E().hasLabel("strike").has("timestamp", current + 1).toList(); Assert.assertEquals(1, edges.size()); } @@ -4758,36 +5057,36 @@ public void testQueryByNegativeLongProperty() { graph.tx().commit(); - List edges = graph.traversal().E() + List edges = graph.traversal().E().hasLabel("transfer") .has("timestamp", -100L).toList(); Assert.assertEquals(1, edges.size()); Assert.assertEquals(IdGenerator.of(2), edges.get(0).value("id")); - edges = graph.traversal().E() + edges = graph.traversal().E().hasLabel("transfer") .has("timestamp", P.between(-101L, 0L)) .toList(); Assert.assertEquals(1, edges.size()); Assert.assertEquals(IdGenerator.of(2), edges.get(0).value("id")); - edges = graph.traversal().E().has("timestamp", P.gt(-101L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.gt(-101L)).toList(); Assert.assertEquals(2, edges.size()); - edges = graph.traversal().E().has("timestamp", P.gte(-100L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.gte(-100L)).toList(); Assert.assertEquals(2, edges.size()); - edges = graph.traversal().E().has("timestamp", P.gt(-100L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.gt(-100L)).toList(); Assert.assertEquals(1, edges.size()); Assert.assertEquals(IdGenerator.of(1), edges.get(0).value("id")); - edges = graph.traversal().E().has("timestamp", P.gt(-99L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.gt(-99L)).toList(); Assert.assertEquals(1, edges.size()); Assert.assertEquals(IdGenerator.of(1), edges.get(0).value("id")); - edges = graph.traversal().E().has("timestamp", P.lt(-100L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.lt(-100L)).toList(); Assert.assertEquals(0, edges.size()); - edges = graph.traversal().E().has("timestamp", P.lte(-100L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.lte(-100L)).toList(); Assert.assertEquals(1, edges.size()); - edges = graph.traversal().E().has("timestamp", P.lt(0L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.lt(0L)).toList(); Assert.assertEquals(1, edges.size()); } @@ -4807,17 +5106,17 @@ public void testQueryByRepeatedRangePredicates() { "timestamp", -4L, "message", "test"); graph.tx().commit(); - List edges = graph.traversal().E() + List edges = graph.traversal().E().hasLabel("transfer") .has("timestamp", -4L) .has("timestamp", P.lte(4L)).toList(); Assert.assertEquals(1, edges.size()); - long count = graph.traversal().E() + long count = graph.traversal().E().hasLabel("transfer") .has("timestamp", -4L) .has("timestamp", P.lte(4L)).count().next(); Assert.assertEquals(1L, count); - count = graph.traversal().E() + count = graph.traversal().E().hasLabel("transfer") .has("timestamp", P.lte(4L)) .has("timestamp", -4L).count().next(); Assert.assertEquals(1L, count); @@ -7081,10 +7380,10 @@ public void testQueryEdgeByStringPropOfOverrideEdge() { louise.addEdge("strike", sean, "id", 1, "timestamp", current, "place", "street", "tool", "shovel", "reason", "jeer", "arrested", false); - List edges = graph.traversal().E().has("place", "park") + List edges = graph.traversal().E().hasLabel("strike").has("place", "park") .toList(); Assert.assertEquals(0, edges.size()); - edges = graph.traversal().E().has("place", "street").toList(); + edges = graph.traversal().E().hasLabel("strike").has("place", "street").toList(); Assert.assertEquals(1, edges.size()); } @@ -7103,28 +7402,28 @@ public void testQueryEdgeBeforeAfterUpdateMultiPropertyWithIndex() { "tool", "shovel", "reason", "jeer", "arrested", false); - List vl = graph.traversal().E().has("tool", "shovel").toList(); + List vl = graph.traversal().E().hasLabel("strike").has("tool", "shovel").toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals(1, (int) vl.get(0).value("id")); - vl = graph.traversal().E().has("timestamp", current).toList(); + vl = graph.traversal().E().hasLabel("strike").has("timestamp", current).toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals(1, (int) vl.get(0).value("id")); - vl = graph.traversal().E().has("tool", "knife").toList(); + vl = graph.traversal().E().hasLabel("strike").has("tool", "knife").toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().E().has("timestamp", 666L).toList(); + vl = graph.traversal().E().hasLabel("strike").has("timestamp", 666L).toList(); Assert.assertEquals(0, vl.size()); edge.property("tool", "knife"); edge.property("timestamp", 666L); - vl = graph.traversal().E().has("tool", "shovel").toList(); + vl = graph.traversal().E().hasLabel("strike").has("tool", "shovel").toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().E().has("timestamp", current).toList(); + vl = graph.traversal().E().hasLabel("strike").has("timestamp", current).toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().E().has("tool", "knife").toList(); + vl = graph.traversal().E().hasLabel("strike").has("tool", "knife").toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals(1, (int) vl.get(0).value("id")); - vl = graph.traversal().E().has("timestamp", 666L).toList(); + vl = graph.traversal().E().hasLabel("strike").has("timestamp", 666L).toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals(1, (int) vl.get(0).value("id")); } @@ -7175,18 +7474,18 @@ public void testQueryEdgeBeforeAfterUpdatePropertyWithRangeIndex() { "tool", "shovel", "reason", "jeer", "arrested", false); - List el = graph.traversal().E().has("timestamp", current) + List el = graph.traversal().E().hasLabel("strike").has("timestamp", current) .toList(); Assert.assertEquals(1, el.size()); Assert.assertEquals(1, (int) el.get(0).value("id")); - el = graph.traversal().E().has("timestamp", 666L).toList(); + el = graph.traversal().E().hasLabel("strike").has("timestamp", 666L).toList(); Assert.assertEquals(0, el.size()); edge.property("timestamp", 666L); - el = graph.traversal().E().has("timestamp", current).toList(); + el = graph.traversal().E().hasLabel("strike").has("timestamp", current).toList(); Assert.assertEquals(0, el.size()); - el = graph.traversal().E().has("timestamp", 666L).toList(); + el = graph.traversal().E().hasLabel("strike").has("timestamp", 666L).toList(); Assert.assertEquals(1, el.size()); Assert.assertEquals(1, (int) el.get(0).value("id")); } @@ -7205,11 +7504,11 @@ public void testQueryEdgeWithNullablePropertyInCompositeIndex() { "timestamp", current, "place", "park", "tool", "shovel", "arrested", false); - List el = graph.traversal().E().has("place", "park") + List el = graph.traversal().E().hasLabel("strike").has("place", "park") .toList(); Assert.assertEquals(1, el.size()); Assert.assertEquals(1, (int) el.get(0).value("id")); - el = graph.traversal().E().has("place", "park") + el = graph.traversal().E().hasLabel("strike").has("place", "park") .has("tool", "shovel").toList(); Assert.assertEquals(1, el.size()); Assert.assertEquals(1, (int) el.get(0).value("id")); @@ -7747,6 +8046,42 @@ private void init18Edges(boolean commit) { } } + private Vertex initEdgeLabelQueryEdges() { + HugeGraph graph = graph(); + SchemaManager schema = graph.schema(); + + schema.edgeLabel("reviewed").properties("time", "score") + .multiTimes().sortKeys("time") + .link("person", "book") + .enableLabelIndex(false) + .create(); + schema.edgeLabel("recommended").properties("time", "score") + .multiTimes().sortKeys("time") + .link("person", "book") + .enableLabelIndex(false) + .create(); + schema.indexLabel("reviewedByScore").onE("reviewed") + .secondary().by("score").create(); + + Vertex reader = graph.addVertex(T.label, "person", + "name", "edge-label-reader", + "city", "Beijing", + "age", 29); + Vertex book1 = graph.addVertex(T.label, "book", + "name", "edge-label-book-1"); + Vertex book2 = graph.addVertex(T.label, "book", + "name", "edge-label-book-2"); + Vertex book3 = graph.addVertex(T.label, "book", + "name", "edge-label-book-3"); + + reader.addEdge("reviewed", book1, "time", "2026-1-1", "score", 1); + reader.addEdge("recommended", book2, "time", "2026-1-1", "score", 2); + reader.addEdge("reviewed", book3, "time", "2026-1-2", "score", 3); + + graph.tx().commit(); + return reader; + } + private void init100LookEdges() { HugeGraph graph = graph(); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeLabelCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeLabelCoreTest.java index 8629f78b3e..f490641f7c 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeLabelCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeLabelCoreTest.java @@ -20,15 +20,15 @@ import java.util.Date; import java.util.List; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.exception.ExistedException; import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.exception.NotFoundException; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.Userdata; +import org.apache.hugegraph.struct.schema.Userdata; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.define.Frequency; import org.apache.hugegraph.util.DateUtil; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/IdPredicateCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/IdPredicateCoreTest.java new file mode 100644 index 0000000000..8051f54bac --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/IdPredicateCoreTest.java @@ -0,0 +1,520 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.core; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.UUID; + +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.traversal.optimize.HugeCountStepStrategy; +import org.apache.hugegraph.traversal.optimize.HugeGraphStepStrategy; +import org.apache.tinkerpop.gremlin.process.traversal.GType; +import org.apache.tinkerpop.gremlin.process.traversal.NotP; +import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.Step; +import org.apache.tinkerpop.gremlin.process.traversal.TextP; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.process.traversal.step.GValue; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.junit.Test; + +public class IdPredicateCoreTest extends BaseCoreTest { + + private static final UUID UUID1 = UUID.fromString("835e1153-9281-4957-8691-cf79258e90eb"); + private static final UUID UUID2 = UUID.fromString("835e1153-9281-4957-8691-cf79258e90ee"); + private static final UUID MISSING = UUID.fromString("835e1153-9281-4957-8691-cf79258e90ef"); + + private GraphTraversalSource[] sources() { + return new GraphTraversalSource[] { + graph().traversal(), + graph().traversal().withoutStrategies(HugeGraphStepStrategy.class, + HugeCountStepStrategy.class) + }; + } + + private Vertex[] initUuidGraph() { + graph().schema().propertyKey("name").asText().create(); + graph().schema().vertexLabel("uuid").useCustomizeUuidId().properties("name").create(); + Vertex first = graph().addVertex(T.label, "uuid", T.id, UUID1, "name", "first"); + Vertex second = graph().addVertex(T.label, "uuid", T.id, UUID2, "name", "second"); + commitTx(); + return new Vertex[] {first, second}; + } + + private static HasContainer localIdContainer(GraphTraversal traversal) { + traversal.asAdmin().applyStrategies(); + Assert.assertInstanceOf(GraphStep.class, traversal.asAdmin().getStartStep()); + Assert.assertEquals(0, ((GraphStep) traversal.asAdmin().getStartStep()).getIds().length); + for (Step step : traversal.asAdmin().getSteps()) { + if (!(step instanceof HasStep)) { + continue; + } + for (HasContainer has : ((HasStep) step).getHasContainers()) { + if (T.id.getAccessor().equals(has.getKey())) { + return has; + } + } + } + throw new AssertionError("The local ID predicate must remain in HasStep"); + } + + private static void assertLocalId(GraphTraversalSource source, P predicate, + Vertex... expected) { + GraphTraversal traversal = source.V().hasId(predicate); + localIdContainer(traversal); + List actual = traversal.toList(); + Assert.assertEquals(expected.length, actual.size()); + Assert.assertEquals(new HashSet<>(Arrays.asList(expected)), new HashSet<>(actual)); + } + + @Test + public void testNegatedUuidId() { + Vertex[] vertices = this.initUuidGraph(); + GraphTraversal equality = graph().traversal().V().hasId(P.eq(UUID1)); + equality.asAdmin().applyStrategies(); + Assert.assertEquals(1, ((GraphStep) equality.asAdmin().getStartStep()).getIds().length); + Assert.assertEquals(Collections.singletonList(vertices[0]), equality.toList()); + for (GraphTraversalSource source : this.sources()) { + Assert.assertEquals(Collections.singletonList(vertices[0]), + source.V().hasId(P.eq(vertices[0].id())).toList()); + assertLocalId(source, P.not(P.neq(UUID1)), vertices[0]); + assertLocalId(source, P.not(P.eq(UUID1)), vertices[1]); + assertLocalId(source, P.not(P.neq(MISSING))); + assertLocalId(source, P.not(P.neq(vertices[0].id())), vertices[0]); + } + } + + @Test + public void testNegatedUuidKeepsWholeHasStep() { + Vertex[] vertices = this.initUuidGraph(); + for (GraphTraversalSource source : this.sources()) { + GraphTraversal traversal = source.V() + .hasId(P.not(P.neq(UUID1))).has("name", "first"); + HasContainer id = localIdContainer(traversal); + Assert.assertInstanceOf(NotP.class, id.getPredicate()); + for (Step step : traversal.asAdmin().getSteps()) { + if (step instanceof HasStep) { + List containers = ((HasStep) step).getHasContainers(); + Assert.assertEquals(2, containers.size()); + Assert.assertEquals(T.id.getAccessor(), containers.get(0).getKey()); + Assert.assertEquals("name", containers.get(1).getKey()); + } + } + Assert.assertEquals(Collections.singletonList(vertices[0]), traversal.toList()); + } + } + + private void assertUuidEqRetainedByProperty(P property) { + Vertex[] vertices = this.initUuidGraph(); + P predicate = P.eq(UUID1); + for (GraphTraversalSource source : this.sources()) { + GraphTraversal traversal = source.V() + .hasId(predicate).has("name", property); + HasContainer id = localIdContainer(traversal); + Assert.assertEquals(Collections.singletonList(vertices[0]), traversal.toList()); + Assert.assertNotSame(predicate, id.getPredicate()); + Assert.assertEquals(UUID1, predicate.getValue()); + } + } + + @Test + public void testUuidEqRetainedByNegatedProperty() { + this.assertUuidEqRetainedByProperty(P.not(P.neq("first"))); + } + + @Test + public void testUuidEqRetainedByTextProperty() { + this.assertUuidEqRetainedByProperty(TextP.containing("ir")); + } + + @Test + public void testUuidEqRetainedByTypePredicate() { + this.assertUuidEqRetainedByProperty(P.typeOf(GType.STRING)); + } + + @Test + public void testNegatedUuidCollection() { + Vertex[] vertices = this.initUuidGraph(); + List operands = Arrays.asList(UUID1, vertices[1].id()); + P predicate = P.not(P.without(operands)); + for (GraphTraversalSource source : this.sources()) { + assertLocalId(source, predicate, vertices); + assertLocalId(source, P.not(P.within(UUID1)), vertices[1]); + assertLocalId(source, P.not(P.without(MISSING))); + } + Assert.assertEquals(operands, ((NotP) predicate).negate().getValue()); + Assert.assertInstanceOf(UUID.class, operands.get(0)); + } + + @Test + public void testNestedUuidPredicatesPreserveOriginal() { + Vertex[] vertices = this.initUuidGraph(); + P predicate = P.not(P.neq(UUID1)).and(P.not(P.eq(UUID2))); + P original = predicate.clone(); + for (GraphTraversalSource source : this.sources()) { + assertLocalId(source, predicate, vertices[0]); + assertLocalId(source, P.not(P.neq(MISSING)).or(P.not(P.neq(UUID2))), vertices[1]); + assertLocalId(source, new NotP<>(P.neq(UUID1).and(P.neq(UUID2))), vertices); + assertLocalId(source, new NotP<>(new NotP<>(P.eq(UUID1))), vertices[0]); + } + Assert.assertEquals(original, predicate); + Assert.assertTrue(predicate.test(UUID1)); + Assert.assertFalse(predicate.test(UUID2)); + } + + @Test + public void testNegatedUuidPredicateClone() { + Vertex[] vertices = this.initUuidGraph(); + P predicate = P.not(P.neq(UUID1)); + GraphTraversal traversal = graph().traversal().V().hasId(predicate); + HasContainer has = localIdContainer(traversal); + Assert.assertInstanceOf(NotP.class, has.getPredicate()); + Assert.assertNotSame(predicate, has.getPredicate()); + Assert.assertEquals(UUID1, predicate.getValue()); + GraphTraversal.Admin clone = traversal.asAdmin().clone(); + Assert.assertEquals(Collections.singletonList(vertices[0]), traversal.toList()); + Assert.assertEquals(Collections.singletonList(vertices[0]), clone.toList()); + Assert.assertEquals(Collections.singletonList(vertices[0]), + graph().traversal().V().hasId(predicate).toList()); + Assert.assertEquals(UUID1, predicate.getValue()); + } + + @Test + public void testNegatedNumericIdPreservesComparison() { + graph().schema().vertexLabel("number").useCustomizeNumberId().create(); + Vertex first = graph().addVertex(T.label, "number", T.id, 42L); + Vertex second = graph().addVertex(T.label, "number", T.id, 43L); + commitTx(); + for (GraphTraversalSource source : this.sources()) { + assertLocalId(source, P.not(P.neq(42)), first); + assertLocalId(source, P.not(P.neq(42L)), first); + assertLocalId(source, P.not(P.neq(42.0)), first); + assertLocalId(source, P.not(P.neq(42.5))); + assertLocalId(source, P.not(P.eq(42.5)), first, second); + assertLocalId(source, P.not(P.without(42, 43L)), first, second); + } + } + + @Test + public void testNegatedStringIdPreservesStringMode() { + graph().schema().vertexLabel("string").useCustomizeStringId().create(); + Vertex first = graph().addVertex(T.label, "string", T.id, "first"); + Vertex second = graph().addVertex(T.label, "string", T.id, "second"); + commitTx(); + for (GraphTraversalSource source : this.sources()) { + assertLocalId(source, P.not(P.neq("first")), first); + assertLocalId(source, P.not(P.eq("first")), second); + assertLocalId(source, P.not(P.without("first", "second")), first, second); + assertLocalId(source, P.not(P.within("missing")), first, second); + assertLocalId(source, new NotP<>(new NotP<>(P.eq("first"))), first); + } + } + + @Test + public void testHeterogeneousIdCollections() { + graph().schema().propertyKey("name").asText().create(); + graph().schema().vertexLabel("number").useCustomizeNumberId().properties("name").create(); + graph().schema().vertexLabel("string").useCustomizeStringId().properties("name").create(); + graph().schema().vertexLabel("uuid").useCustomizeUuidId().properties("name").create(); + Vertex number = graph().addVertex(T.label, "number", T.id, 42L, "name", "member"); + Vertex string = graph().addVertex(T.label, "string", T.id, "alpha", "name", "member"); + Vertex uuid = graph().addVertex(T.label, "uuid", T.id, UUID1, "name", "member"); + commitTx(); + for (GraphTraversalSource source : this.sources()) { + this.assertHeterogeneousIdCollection(source, Arrays.asList(42, "alpha"), uuid, number, string); + this.assertHeterogeneousIdCollection(source, Arrays.asList(UUID1, "alpha"), number, uuid, string); + this.assertHeterogeneousIdCollection(source, Arrays.asList(number.id(), "alpha"), uuid, number, string); + // Pure String collections retain HasContainer's string-ID mode. + assertLocalId(source, P.not(P.without("42", "alpha")), number, string); + assertLocalId(source, P.not(P.without(42.5, "alpha")), string); + assertLocalId(source, P.not(P.without("42", uuid.id())), uuid); + } + } + + @Test + public void testHeterogeneousNamedIdCollectionPreservesBindings() { + graph().schema().vertexLabel("number").useCustomizeNumberId().create(); + graph().schema().vertexLabel("string").useCustomizeStringId().create(); + Vertex number = graph().addVertex(T.label, "number", T.id, 42L); + Vertex string = graph().addVertex(T.label, "string", T.id, "alpha"); + commitTx(); + for (GraphTraversalSource source : this.sources()) { + P leaf = P.without(Arrays.asList(GValue.of("targetId", 42L), "alpha")); + Collection bindings = new HashSet<>(leaf.getGValues()); + P predicate = P.not(leaf); + P clone = predicate.clone(); + GraphTraversal traversal = source.V().hasId(predicate); + GraphTraversal.Admin traversalClone = traversal.asAdmin().clone(); + Assert.assertEquals(new HashSet<>(Arrays.asList(number, string)), new HashSet<>(traversal.toList())); + Assert.assertEquals(new HashSet<>(Arrays.asList(number, string)), + new HashSet<>(traversalClone.toList())); + assertLocalId(source, predicate, number, string); + assertLocalId(source, clone, number, string); + Assert.assertEquals(new HashSet<>(Arrays.asList(42L, "alpha")), + new HashSet<>((Collection) leaf.getValue())); + Assert.assertTrue(leaf.isParameterized()); + Assert.assertTrue(((NotP) clone).negate().isParameterized()); + Assert.assertEquals(bindings, new HashSet<>(((NotP) clone).negate().getGValues())); + Assert.assertEquals(bindings, new HashSet<>(leaf.getGValues())); + leaf.updateVariable("targetId", 43L); + assertLocalId(source, predicate, string); + Assert.assertEquals(new HashSet<>(Arrays.asList(43L, "alpha")), + new HashSet<>((Collection) leaf.getValue())); + } + } + + @Test + public void testMixedEdgeIdCollections() { + graph().schema().vertexLabel("edge-id").useCustomizeNumberId().create(); + graph().schema().edgeLabel("mixed-id-edge").sourceLabel("edge-id").targetLabel("edge-id").create(); + Vertex first = graph().addVertex(T.label, "edge-id", T.id, 101L); + Vertex second = graph().addVertex(T.label, "edge-id", T.id, 102L); + Vertex third = graph().addVertex(T.label, "edge-id", T.id, 103L); + Edge one = first.addEdge("mixed-id-edge", second); + Edge two = first.addEdge("mixed-id-edge", third); + Edge excluded = second.addEdge("mixed-id-edge", third); + commitTx(); + List> collections = Arrays.asList(Arrays.asList(one.id().toString(), two.id()), + Arrays.asList(one.id(), two.id().toString()), + Arrays.asList(one.id(), two.id()), + Arrays.asList(one.id().toString(), two.id().toString())); + for (GraphTraversalSource source : this.sources()) { + for (List ids : collections) { + Assert.assertEquals(new HashSet<>(Arrays.asList(one.id(), two.id())), + new HashSet<>(graph().traversal().E().hasId(P.within(ids)).id().toList())); + GraphTraversal local = source.E().hasId(P.not(P.without(ids))); + localIdContainer(local); + Assert.assertEquals(new HashSet<>(Arrays.asList(one, two)), new HashSet<>(local.toList())); + Assert.assertEquals(2L, source.E().hasId(P.not(P.without(ids))).count().next().longValue()); + Assert.assertEquals(Collections.singletonList(excluded), + source.E().hasId(P.not(P.within(ids))).toList()); + Assert.assertEquals(1L, source.E().hasId(P.not(P.within(ids))).count().next().longValue()); + } + Assert.assertEquals(Collections.singletonList(one), source.E() + .hasId(P.not(P.without(Arrays.asList("invalid", one.id())))).toList()); + } + } + + private void assertHeterogeneousIdCollection(GraphTraversalSource source, List operands, + Vertex excluded, Vertex... expected) { + P within = P.within(operands); + P original = within.clone(); + GraphTraversal pushed = graph().traversal().V().hasId(within); + pushed.asAdmin().applyStrategies(); + Assert.assertEquals(operands.size(), + ((GraphStep) pushed.asAdmin().getStartStep()).getIds().length); + Assert.assertEquals(new HashSet<>(Arrays.asList(expected)), new HashSet<>(pushed.toList())); + assertLocalId(source, P.not(P.without(operands)), expected); + assertLocalId(source, P.not(within), excluded); + Assert.assertEquals((long) expected.length, + source.V().hasId(P.not(P.without(operands))).count().next().longValue()); + Assert.assertEquals(1L, source.V().hasId(P.not(within)).count().next().longValue()); + for (P predicate : Arrays.asList(within, P.without(operands))) { + Vertex[] result = predicate == within ? expected : new Vertex[] {excluded}; + GraphTraversal traversal = source.V().hasId(predicate) + .has("name", P.typeOf(GType.STRING)); + localIdContainer(traversal); + List actual = traversal.toList(); + Assert.assertEquals(result.length, actual.size()); + Assert.assertEquals(new HashSet<>(Arrays.asList(result)), new HashSet<>(actual)); + Assert.assertEquals((long) result.length, source.V().hasId(predicate) + .has("name", P.typeOf(GType.STRING)).count().next().longValue()); + } + Assert.assertEquals(original, within); + Assert.assertEquals(operands, within.getValue()); + } + + @Test + public void testIdTypeOperandRemainsSemanticMarker() { + Vertex[] vertices = this.initUuidGraph(); + P predicate = P.not(P.typeOf(GType.STRING)); + for (GraphTraversalSource source : this.sources()) { + GraphTraversal traversal = source.V().hasId(predicate); + HasContainer has = localIdContainer(traversal); + Assert.assertEquals(GType.STRING, has.getPredicate().getValue()); + Assert.assertEquals(new HashSet<>(Arrays.asList(vertices)), + new HashSet<>(traversal.toList())); + } + Assert.assertEquals(GType.STRING, predicate.getValue()); + } + + @Test + public void testCombinedStringIdPredicates() { + graph().schema().vertexLabel("string").useCustomizeStringId().create(); + Vertex first = graph().addVertex(T.label, "string", T.id, "first"); + Vertex second = graph().addVertex(T.label, "string", T.id, "second"); + Vertex third = graph().addVertex(T.label, "string", T.id, "third"); + commitTx(); + for (GraphTraversalSource source : this.sources()) { + P firstNot = P.not(P.eq("first")); + P secondNot = P.not(P.eq("second")); + P conjunction = firstNot.and(secondNot); + Assert.assertEquals(Collections.singletonList(third), source.V() + .hasId(firstNot).hasId(secondNot).toList()); + assertLocalId(source, conjunction, third); + Assert.assertEquals(1L, source.V().hasId(conjunction).count().next().longValue()); + P disjunction = P.not(P.neq("first")).or(P.not(P.neq("second"))); + assertLocalId(source, disjunction, first, second); + Assert.assertEquals(2L, source.V().hasId(disjunction).count().next().longValue()); + Assert.assertEquals(Collections.singletonList(third), + source.V().hasId(new NotP<>(disjunction)).toList()); + Assert.assertEquals(1L, source.V().hasId(new NotP<>(disjunction)) + .count().next().longValue()); + assertLocalId(source, P.not(P.gt("second")), first, second); + Assert.assertTrue(conjunction.test("third")); + Assert.assertFalse(conjunction.test("first")); + } + } + + @Test + public void testNamedUuidIdPredicateStateIsolation() { + Vertex[] vertices = this.initUuidGraph(); + for (GraphTraversalSource source : this.sources()) { + P predicate = P.eq(GValue.of("id", UUID1)); + P clone = predicate.clone(); + GraphTraversal traversal = source.V().hasId(predicate) + .has("name", P.typeOf(GType.STRING)); + GraphTraversal.Admin traversalClone = traversal.asAdmin().clone(); + Assert.assertEquals(Collections.singletonList(vertices[0]), traversal.toList()); + Assert.assertEquals(Collections.singletonList(vertices[0]), traversalClone.toList()); + for (int i = 0; i < 3; i++) { + Assert.assertEquals(Collections.singletonList(vertices[0]), source.V() + .hasId(predicate).has("name", P.typeOf(GType.STRING)).toList()); + Assert.assertEquals(Collections.singletonList(vertices[0]), source.V() + .hasId(clone).has("name", P.typeOf(GType.STRING)).toList()); + Assert.assertEquals(UUID1, predicate.getValue()); + Assert.assertEquals(UUID1, clone.getValue()); + Assert.assertTrue(predicate.isParameterized()); + } + predicate.updateVariable("id", UUID2); + Assert.assertEquals(Collections.singletonList(vertices[1]), source.V() + .hasId(predicate).has("name", P.typeOf(GType.STRING)).toList()); + Assert.assertEquals(UUID2, predicate.getValue()); + } + } + + @Test + public void testNamedPropertyPredicateStateIsolation() { + graph().schema().propertyKey("name").asText().create(); + graph().schema().vertexLabel("uuid").useCustomizeUuidId().properties("name").create(); + Vertex[] vertices = { + graph().addVertex(T.label, "uuid", T.id, UUID1, "name", "first"), + graph().addVertex(T.label, "uuid", T.id, UUID2, "name", "second") + }; + commitTx(); + for (GraphTraversalSource source : this.sources()) { + P predicate = P.eq(GValue.of("name", "first")); + P clone = predicate.clone(); + GraphTraversal traversal = source.V(vertices[0].id(), vertices[1].id()) + .has("name", predicate); + GraphTraversal.Admin traversalClone = traversal.asAdmin().clone(); + Assert.assertEquals(Collections.singletonList(vertices[0]), traversal.toList()); + Assert.assertEquals(Collections.singletonList(vertices[0]), traversalClone.toList()); + for (int i = 0; i < 3; i++) { + Assert.assertEquals(Collections.singletonList(vertices[0]), + source.V(vertices[0].id(), vertices[1].id()).has("name", predicate).toList()); + Assert.assertEquals(Collections.singletonList(vertices[0]), + source.V(vertices[0].id(), vertices[1].id()).has("name", clone).toList()); + Assert.assertEquals("first", predicate.getValue()); + Assert.assertEquals("first", clone.getValue()); + Assert.assertTrue(predicate.isParameterized()); + } + predicate.updateVariable("name", "second"); + Assert.assertEquals(Collections.singletonList(vertices[1]), + source.V(vertices[0].id(), vertices[1].id()).has("name", predicate).toList()); + Assert.assertEquals("second", predicate.getValue()); + } + } + + @Test + public void testNamedIdCollectionPredicateStateIsolation() { + graph().schema().vertexLabel("number").useCustomizeNumberId().create(); + Vertex first = graph().addVertex(T.label, "number", T.id, 42L); + Vertex second = graph().addVertex(T.label, "number", T.id, 43L); + Vertex third = graph().addVertex(T.label, "number", T.id, 44L); + commitTx(); + for (GraphTraversalSource source : this.sources()) { + P leaf = P.within(GValue.of("targetId", 42L)); + Collection bindings = new HashSet<>(leaf.getGValues()); + P predicate = P.not(leaf); + P clone = predicate.clone(); + GraphTraversal traversal = source.V().hasId(predicate); + GraphTraversal.Admin traversalClone = traversal.asAdmin().clone(); + Assert.assertEquals(new HashSet<>(Arrays.asList(second, third)), + new HashSet<>(traversal.toList())); + Assert.assertEquals(new HashSet<>(Arrays.asList(second, third)), + new HashSet<>(traversalClone.toList())); + assertLocalId(source, predicate, second, third); + assertLocalId(source, clone, second, third); + Assert.assertEquals(Collections.singletonList(42L), leaf.getValue()); + Assert.assertEquals(Collections.singletonList(42L), clone.getValue()); + Assert.assertTrue(leaf.isParameterized()); + Assert.assertEquals(bindings, new HashSet<>(leaf.getGValues())); + leaf.updateVariable("targetId", 43L); + assertLocalId(source, predicate, first, third); + Assert.assertEquals(Collections.singletonList(43L), leaf.getValue()); + } + } + + @Test + public void testMixedNamedPropertyCollectionPredicateStateIsolation() { + graph().schema().propertyKey("age").asInt().create(); + graph().schema().vertexLabel("number").useCustomizeNumberId().properties("age").create(); + Vertex first = graph().addVertex(T.label, "number", T.id, 42L, "age", 20); + Vertex second = graph().addVertex(T.label, "number", T.id, 43L, "age", 30); + Vertex third = graph().addVertex(T.label, "number", T.id, 44L, "age", 40); + commitTx(); + for (GraphTraversalSource source : this.sources()) { + P predicate = P.within(Arrays.asList(GValue.of("age", 20), 40)); + Collection bindings = new HashSet<>(predicate.getGValues()); + Collection values = new HashSet<>((Collection) predicate.getValue()); + P clone = predicate.clone(); + GraphTraversal traversal = source.V(first.id(), second.id(), third.id()) + .has("age", predicate); + GraphTraversal.Admin traversalClone = traversal.asAdmin().clone(); + Assert.assertEquals(new HashSet<>(Arrays.asList(first, third)), + new HashSet<>(traversal.toList())); + Assert.assertEquals(new HashSet<>(Arrays.asList(first, third)), + new HashSet<>(traversalClone.toList())); + Assert.assertEquals(new HashSet<>(Arrays.asList(first, third)), new HashSet<>(source + .V(first.id(), second.id(), third.id()).has("age", predicate).toList())); + Assert.assertEquals(new HashSet<>(Arrays.asList(first, third)), new HashSet<>(source + .V(first.id(), second.id(), third.id()).has("age", clone).toList())); + Assert.assertEquals(values, new HashSet<>((Collection) predicate.getValue())); + Assert.assertEquals(values, new HashSet<>((Collection) clone.getValue())); + Assert.assertTrue(predicate.isParameterized()); + Assert.assertTrue(clone.isParameterized()); + Assert.assertEquals(bindings, new HashSet<>(predicate.getGValues())); + Assert.assertEquals(bindings, new HashSet<>(clone.getGValues())); + predicate.updateVariable("age", 30); + Assert.assertEquals(new HashSet<>(Arrays.asList(second, third)), new HashSet<>(source + .V(first.id(), second.id(), third.id()).has("age", predicate).toList())); + } + } + +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/IndexLabelCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/IndexLabelCoreTest.java index 24a905427c..6a7fdb60a9 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/IndexLabelCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/IndexLabelCoreTest.java @@ -20,21 +20,24 @@ import java.util.Date; import java.util.List; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.exception.ExistedException; import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.exception.NotFoundException; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.Userdata; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.IndexType; import org.apache.hugegraph.type.define.WriteType; import org.apache.hugegraph.util.DateUtil; +import org.apache.hugegraph.util.Events; import org.apache.tinkerpop.gremlin.process.traversal.P; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.T; @@ -1297,6 +1300,76 @@ public void testRemoveIndexLabelOfVertex() { }); } + @Test + public void testUpdateCachedVertexAfterIndexRemoval() throws Exception { + super.initPropertyKeys(); + SchemaManager schema = graph().schema(); + schema.vertexLabel("person").properties("name", "city").primaryKeys("name").create(); + schema.indexLabel("personByCity").onV("person").secondary().by("city").create(); + Vertex original = graph().addVertex(T.label, "person", "name", "cache-test", "city", "old"); + graph().tx().commit(); + Vertex cached = graph().vertices(original.id()).next(); + graph().tx().commit(); + // Schema cache eviction must not make a retained vertex's index list authoritative. + HugeGraphParams params = Whitebox.getInternalState(graph(), "params"); + params.schemaEventHub().notify(Events.CACHE, "clear", null).get(); + + schema.indexLabel("personByCity").remove(); + Assert.assertThrows(NotFoundException.class, () -> schema.getIndexLabel("personByCity")); + cached.property("city", "new"); + graph().tx().commit(); + Assert.assertEquals("new", graph().vertices(original.id()).next().value("city")); + graph().tx().commit(); + schema.indexLabel("personByCity").onV("person").secondary().by("city").create(); + schema.propertyKey("extra").asText().create(); + schema.vertexLabel("person").properties("extra").nullableKeys("extra").append(); + schema.indexLabel("personByExtra").onV("person").secondary().by("extra").create(); + graph().taskScheduler().waitUntilAllTasksCompleted(30); + cached.property("city", "newer"); + graph().tx().commit(); + Assert.assertFalse(graph().traversal().V().has("city", "new").hasNext()); + Assert.assertEquals(original.id(), graph().traversal().V().has("city", "newer").next().id()); + graph().tx().commit(); + cached.remove(); + graph().tx().commit(); + Assert.assertFalse(graph().vertices(original.id()).hasNext()); + } + + @Test + public void testUpdateCachedEdgeAfterIndexRemoval() throws Exception { + super.initPropertyKeys(); + SchemaManager schema = graph().schema(); + schema.vertexLabel("author").properties("id", "name").primaryKeys("id").create(); + schema.vertexLabel("book").properties("name").primaryKeys("name").create(); + schema.edgeLabel("authored").singleTime().link("author", "book").properties("contribution").create(); + schema.indexLabel("authoredByContri").onE("authored").secondary().by("contribution").create(); + Vertex author = graph().addVertex(T.label, "author", "id", 1, "name", "author"); + Vertex book = graph().addVertex(T.label, "book", "name", "book"); + Edge original = author.addEdge("authored", book, "contribution", "old"); + graph().tx().commit(); + Edge cached = graph().edges(original.id()).next(); + graph().tx().commit(); + HugeGraphParams params = Whitebox.getInternalState(graph(), "params"); + params.schemaEventHub().notify(Events.CACHE, "clear", null).get(); + + schema.indexLabel("authoredByContri").remove(); + Assert.assertThrows(NotFoundException.class, () -> schema.getIndexLabel("authoredByContri")); + cached.property("contribution", "new"); + graph().tx().commit(); + Assert.assertEquals("new", graph().edges(original.id()).next().value("contribution")); + graph().tx().commit(); + schema.indexLabel("authoredByContri").onE("authored").secondary().by("contribution").create(); + graph().taskScheduler().waitUntilAllTasksCompleted(30); + cached.property("contribution", "newer"); + graph().tx().commit(); + Assert.assertFalse(graph().traversal().E().has("contribution", "new").hasNext()); + Assert.assertEquals(original.id(), graph().traversal().E().has("contribution", "newer").next().id()); + graph().tx().commit(); + cached.remove(); + graph().tx().commit(); + Assert.assertFalse(graph().edges(original.id()).hasNext()); + } + @Test public void testRemoveIndexLabelOfEdge() { super.initPropertyKeys(); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/MultiGraphsTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/MultiGraphsTest.java index 7ed172acd9..28f6a4ccc0 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/MultiGraphsTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/MultiGraphsTest.java @@ -17,30 +17,35 @@ package org.apache.hugegraph.core; +import java.nio.channels.OverlappingFileLockException; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Objects; import org.apache.commons.configuration2.BaseConfiguration; import org.apache.commons.configuration2.Configuration; import org.apache.commons.configuration2.PropertiesConfiguration; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.store.BackendStoreInfo; import org.apache.hugegraph.backend.store.rocksdb.RocksDBOptions; +import org.apache.hugegraph.backend.tx.IdCounter; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.exception.ExistedException; import org.apache.hugegraph.masterelection.GlobalMasterInfo; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Utils; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.type.HugeType; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.util.GraphFactory; @@ -121,6 +126,51 @@ public void testTruncateBackendKeepsVersionAndResetsSchemaIds() { } } + @Test + public void testHstoreTruncateBackendKeepsSchemaIdCounters() { + Assume.assumeTrue("only hstore keeps the schema id counters in PD", + "hstore".equals(graph().backend())); + + HugeGraph graph = openGraphs("truncate_hs").get(0); + try { + graph.clearBackend(); + graph.initBackend(); + graph.serverStarted(GlobalMasterInfo.master("server-truncate")); + + SchemaManager schema = graph.schema(); + PropertyKey name = schema.propertyKey("name").asText().create(); + + graph.truncateBackend(); + + // The schema lives in PD meta and survives the truncate + Assert.assertEquals(name.id(), schema.getPropertyKey("name").id()); + + // Another Server holds no cached id range, it asks PD for one: drop + // the range this process cached for the graph's property key + // counter, under the key the counter itself builds + Object schemaTx = Whitebox.invoke(graph.getClass(), + "schemaTransaction", graph); + IdCounter counter = Whitebox.getInternalState(schemaTx, "idCounter"); + String key = Whitebox.invoke(IdCounter.class, + new Class[]{String.class, + HugeType.class}, + "toKey", counter, + Whitebox.getInternalState(counter, + "graphName"), + HugeType.PROPERTY_KEY); + Map ranges = Whitebox.getInternalState(IdCounter.class, "ids"); + Assert.assertNotNull(ranges.remove(key)); + + PropertyKey age = schema.propertyKey("age").asInt().create(); + Assert.assertTrue(String.format("id %s reused after %s", age.id(), name.id()), + age.id().asLong() > name.id().asLong()); + + graph.clearBackend(); + } finally { + destroyGraphs(ImmutableList.of(graph)); + } + } + @Test public void testCreateMultiGraphs() { List graphs = openGraphs("g_1", NAME48); @@ -373,7 +423,7 @@ public void testCreateGraphsWithDifferentNameDifferentBackends() { } @Test - public void testOpenGraphWithDeprecatedTaskSchedulerType() { + public void testOpenGraphWithRemovedTaskSchedulerType() { HugeGraph graph = openGraphWithBackend("legacySchedulerType", "rocksdb", "binary", "task.scheduler_type", "local"); @@ -396,12 +446,14 @@ public void testCreateGraphsWithMultiDisksForRocksDB() { "[g/range_int_index:rocksdb-index1]"); g2[0].initBackend(); }, e -> { - Throwable root = HugeException.rootCause(e); - Assert.assertInstanceOf(RocksDBException.class, root); + RocksDBException failure = rocksDBCause(e); + Assert.assertNotNull(failure.getCause()); Assert.assertContains("lock hold by current process", - root.getMessage()); + failure.getMessage()); Assert.assertContains("No locks available", - root.getMessage()); + failure.getMessage()); + Assert.assertInstanceOf(OverlappingFileLockException.class, + HugeException.rootCause(failure)); }); final HugeGraph[] g3 = new HugeGraph[1]; @@ -411,15 +463,25 @@ public void testCreateGraphsWithMultiDisksForRocksDB() { "[g/secondary_index:/]"); g3[0].initBackend(); }, e -> { - Throwable root = HugeException.rootCause(e); - Assert.assertInstanceOf(RocksDBException.class, root); - Assert.assertContains("While mkdir if missing", - root.getMessage()); + RocksDBException failure = rocksDBCause(e); + Assert.assertNotNull(failure.getCause()); + Assert.assertContains("Cannot lock database for open/recovery", + failure.getMessage()); + Assert.assertContains("/g", failure.getMessage()); }); destroyGraphs(ImmutableList.of(g1)); } + private static RocksDBException rocksDBCause(Throwable exception) { + Throwable cause = exception; + while (cause != null && !(cause instanceof RocksDBException)) { + cause = cause.getCause(); + } + Assert.assertInstanceOf(RocksDBException.class, cause); + return (RocksDBException) cause; + } + private static List openGraphs(String... graphNames) { List graphs = new ArrayList<>(graphNames.length); for (String graphName : graphNames) { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/OltpTraverserCursorTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/OltpTraverserCursorTest.java new file mode 100644 index 0000000000..430bcd568e --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/OltpTraverserCursorTest.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.hugegraph.core; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.configuration2.MapConfiguration; +import org.apache.commons.io.FileUtils; +import org.apache.hugegraph.HugeFactory; +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.backend.query.QueryBatch; +import org.apache.hugegraph.backend.query.QueryResults; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions; +import org.apache.hugegraph.backend.tx.GraphTransaction; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.iterator.MapperIterator; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.testutil.Utils; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.traversal.algorithm.OltpTraverser; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.util.Consumers; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.rocksdb.RocksIterator; + +public class OltpTraverserCursorTest { + + @Before + public void requireRocksDB() { + Assume.assumeTrue("Native cursor regression requires RocksDB", + "rocksdb".equals(Utils.getConf().getString("backend"))); + CoreTestSuite.initEnv(); + } + + @Test + public void testConsumerFailureClosesNativeCursor() throws Exception { + this.checkNativeCursor("consumer"); + } + + @Test + public void testStopExecutionClosesNativeCursor() throws Exception { + this.checkNativeCursor("stop"); + } + + @Test + public void testInterruptedConsumerClosesNativeCursor() throws Exception { + this.checkNativeCursor("interrupt"); + } + + @Test + public void testPrepareFailureClosesNativeCursor() throws Exception { + this.checkNativeCursor("prepare"); + } + + @Test + public void testCloseFailureIsSuppressedAfterNativeCursorCloses() throws Exception { + this.checkNativeCursor("close"); + } + + @Test + public void testAsyncCapacityStopClosesNativeCursor() throws Exception { + this.checkNativeCursor("async-capacity"); + } + + @Test + public void testAsyncCapacityStopPropagatesCloseFailure() throws Exception { + this.checkNativeCursor("async-capacity-close"); + } + + @Test + public void testAsyncCapacityStopReleasesBlockedProducer() throws Exception { + this.checkNativeCursor("async-capacity-producer"); + } + + @Test + public void testAsyncCapacityCloseFailureReleasesBlockedProducer() throws Exception { + this.checkNativeCursor("async-capacity-producer-close"); + } + + @Test + public void testAsyncConsumerFailureKeepsSuppressedCloseFailure() throws Exception { + this.checkNativeCursor("async-consumer-close"); + } + + @Test + public void testAsyncBusinessFailureReleasesBlockedProducer() throws Exception { + this.checkNativeCursor("async-consumer-producer-close"); + } + + @Test + public void testAsyncFatalErrorReleasesBlockedProducer() throws Exception { + this.checkNativeCursor("async-fatal-producer"); + } + + @Test + public void testCapacityStopEndsActualLazyTraversal() throws Exception { + this.checkNativeCursor("lazy-producer"); + } + + private void checkNativeCursor(String outcome) throws Exception { + Path directory = Files.createTempDirectory("oltp-cursor-"); + HugeConfig config = new HugeConfig(new MapConfiguration(Collections.emptyMap())); + config.setProperty("backend", "rocksdb"); + config.setProperty("serializer", "binary"); + config.setProperty("store", "cursor_" + UUID.randomUUID().toString().replace("-", "")); + config.setProperty("oltp.concurrent_threads", "lazy-producer".equals(outcome) ? 1 : 0); + config.setProperty("rocksdb.data_path", directory.toString()); + config.setProperty("rocksdb.wal_path", directory.toString()); + HugeGraph graph = null; + RocksDBSessions control = null; + Iterator edges = null; + RocksIterator nativeCursor = null; + int producerReads = -1; + try { + graph = HugeFactory.open(config); + graph.initBackend(); + graph.schema().propertyKey("seq").asInt().create(); + graph.schema().vertexLabel("vertex").useCustomizeNumberId().create(); + graph.schema().edgeLabel("linked").sourceLabel("vertex").targetLabel("vertex") + .multiTimes().properties("seq").sortKeys("seq").create(); + Vertex source = graph.addVertex(T.label, "vertex", T.id, 1L); + Vertex target = graph.addVertex(T.label, "vertex", T.id, 2L); + // The backend prefetches 500 edges; record 501 keeps its native cursor open. + for (int i = 0; i < 501; i++) { + source.addEdge("linked", target, "seq", i); + } + graph.tx().commit(); + Id label = graph.schema().getEdgeLabel("linked").id(); + Query query = GraphTransaction.constructEdgesQuery(IdGenerator.of(1L), Directions.OUT, + new Id[]{label}); + edges = graph.edges(query); + Assert.assertTrue(edges.hasNext()); + nativeCursor = nativeCursor(edges, Collections.newSetFromMap(new IdentityHashMap<>())); + Assert.assertNotNull("Expected the real RocksDB scan cursor", nativeCursor); + Assert.assertTrue(nativeCursor.isOwningHandle()); + Object store = graph.storeProvider().loadGraphStore(config); + RocksDBSessions sessions = Whitebox.getInternalState(store, "sessions"); + // Keep the native DB alive during handle observation after caller FINISHED cleanup. + control = sessions.copy(config, "cursor-control", "g"); + control.session(); + if ("lazy-producer".equals(outcome)) { + // This isolated JVM fixture owns the single-worker pool used below. + OltpTraverser.destroy(); + } + Probe traverser = new Probe(graph); + Throwable failure = outcome.contains("fatal") ? new AssertionError("fatal failure") : + "stop".equals(outcome) ? new Consumers.StopExecution("limit") : + new IllegalStateException("business failure"); + RuntimeException closeFailure = new IllegalStateException("close failure"); + if (outcome.endsWith("close")) { + edges = new MapperIterator(edges, edge -> edge) { + @Override + public void close() throws Exception { + super.close(); + throw closeFailure; + } + }; + } + Iterator input = edges; + OltpTraverser.EdgesConsumer consumer; + if ("prepare".equals(outcome)) { + consumer = traverser.new EdgesConsumer(id -> Assert.fail(), OltpTraverser.NO_LIMIT) { + @Override + protected Iterator prepare(Iterator iterator) { + if (failure instanceof Error) { + throw (Error) failure; + } + throw (RuntimeException) failure; + } + }; + } else { + consumer = traverser.new OneStepEdgeIterConsumer(id -> { + if (!outcome.startsWith("async-capacity") && !"lazy-producer".equals(outcome)) { + if (failure instanceof Error) { + throw (Error) failure; + } + throw (RuntimeException) failure; + } + }, outcome.startsWith("async-capacity") || "lazy-producer".equals(outcome) ? + 1L : OltpTraverser.NO_LIMIT); + } + if ("lazy-producer".equals(outcome)) { + producerReads = this.consumeLazyProducer(traverser, consumer, input); + Assert.assertEquals(501L, traverser.edgeIterCounter.get()); + } else if (outcome.startsWith("async-")) { + AtomicReference handled = new AtomicReference<>(); + Throwable thrown = this.consumeAsync(consumer, input, handled, outcome); + if (outcome.startsWith("async-capacity")) { + Assert.assertEquals(501L, traverser.edgeIterCounter.get()); + if (outcome.endsWith("close")) { + Assert.assertNotNull("Close failure must remain visible after capacity Stop", thrown); + Assert.assertFalse(thrown instanceof Consumers.StopExecution); + Assert.assertTrue(handled.get() instanceof Consumers.StopExecution); + Assert.assertSame(handled.get().getSuppressed()[0], thrown); + Assert.assertSame(closeFailure, thrown.getCause()); + } else { + Assert.assertNull(thrown); + Assert.assertTrue(handled.get() instanceof Consumers.StopExecution); + } + } else { + Assert.assertSame(failure, thrown); + Assert.assertSame(failure, handled.get()); + Assert.assertEquals(outcome.endsWith("close") ? 1 : 0, thrown.getSuppressed().length); + if (outcome.endsWith("close")) { + Assert.assertSame(closeFailure, thrown.getSuppressed()[0].getCause()); + } + } + } else if ("interrupt".equals(outcome)) { + Thread.currentThread().interrupt(); + try { + consumer.accept(input); + Assert.assertTrue(Thread.currentThread().isInterrupted()); + } finally { + Thread.interrupted(); + } + } else { + RuntimeException thrown = org.apache.hugegraph.testutil.Assert.assertThrows( + RuntimeException.class, () -> consumer.accept(input)); + Assert.assertSame(failure, thrown); + if ("close".equals(outcome)) { + Assert.assertEquals(1, thrown.getSuppressed().length); + Assert.assertTrue(thrown.getSuppressed()[0].getCause().getMessage().contains("close failure")); + } + } + HugeFactory.closeCurrentThreadTransactions(); + Assert.assertTrue("Control lease must keep the DB alive", control.databaseOpened()); + Assert.assertFalse("Traversal retained its native cursor after " + outcome, + nativeCursor.isOwningHandle()); + if ("lazy-producer".equals(outcome)) { + Assert.assertEquals("Capacity Stop must end the lazy producer", 2, producerReads); + } + } finally { + // These are teardown only; the native handle assertion above must run first. + try { + if (nativeCursor != null && nativeCursor.isOwningHandle()) { + nativeCursor.close(); + } + if (edges != null && !outcome.endsWith("close")) { + CloseableIterator.closeIterator(edges); + } + } finally { + try { + HugeFactory.closeCurrentThreadTransactions(); + if (control != null) { + control.close(); + } + if (graph != null && !graph.closed()) { + graph.drop(); + } + } finally { + if (graph != null) { + HugeFactory.remove(graph); + } + FileUtils.deleteDirectory(directory.toFile()); + } + } + } + } + + private Throwable consumeAsync(OltpTraverser.EdgesConsumer consumer, + Iterator input, AtomicReference handled, + String outcome) throws Exception { + ExecutorService worker = Executors.newFixedThreadPool(1); + ExecutorService waiter = Executors.newSingleThreadExecutor(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + boolean fullQueue = outcome.startsWith("async-capacity") || outcome.contains("producer"); + try { + Consumers> consumers = new Consumers<>(worker, iterator -> { + entered.countDown(); + try { + if (!release.await(5L, TimeUnit.SECONDS)) { + throw new AssertionError("Consumer was not released"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } + consumer.accept(iterator); + }, HugeFactory::closeCurrentThreadTransactions, handled::set, 1); + consumers.start("cursor-close-failure"); + try { + consumers.provide(input); + Assert.assertTrue(entered.await(5L, TimeUnit.SECONDS)); + if (fullQueue) { + // One active input and two queued inputs fill the real queue. + consumers.provide(Collections.emptyIterator()); + consumers.provide(Collections.emptyIterator()); + } + } catch (Throwable failure) { + throw new AssertionError(failure); + } + CountDownLatch calling = new CountDownLatch(1); + Future result = waiter.submit(() -> { + calling.countDown(); + Throwable failure = null; + try { + if (outcome.contains("producer")) { + consumers.provide(Collections.emptyIterator()); + } + } catch (Consumers.StopExecution ignored) { + // Stop ends production successfully, as in Oltp's producer loop. + } catch (Throwable providedFailure) { + failure = providedFailure; + } + try { + consumers.await(); + } catch (Throwable awaitedFailure) { + if (failure == null) { + failure = awaitedFailure; + } + } + return failure; + }); + Assert.assertTrue(calling.await(5L, TimeUnit.SECONDS)); + if (fullQueue) { + // The producer or await END enqueue must wait on the full queue. + org.apache.hugegraph.testutil.Assert.assertThrows( + java.util.concurrent.TimeoutException.class, + () -> result.get(100L, TimeUnit.MILLISECONDS)); + } + release.countDown(); + return result.get(3L, TimeUnit.SECONDS); + } finally { + release.countDown(); + waiter.shutdownNow(); + worker.shutdownNow(); + // The pre-fix producer can swallow one interrupt before entering await(). + for (int i = 0; i < 50 && !waiter.awaitTermination(100L, TimeUnit.MILLISECONDS); i++) { + waiter.shutdownNow(); + } + Assert.assertTrue(waiter.isTerminated()); + Assert.assertTrue(worker.awaitTermination(5L, TimeUnit.SECONDS)); + } + } + + private int consumeLazyProducer(Probe traverser, OltpTraverser.EdgesConsumer consumer, + Iterator input) throws Exception { + Consumers.ExecutorPool pool = Whitebox.getInternalState(OltpTraverser.class, "executors"); + ExecutorService worker = pool.getExecutor(); + pool.returnExecutor(worker); + CountDownLatch releaseConsumer = new CountDownLatch(1); + class LazyInputs implements Iterator>, AutoCloseable { + + private int reads; + private boolean closed; + + @Override + public boolean hasNext() { + if (this.reads == 1) { + // The first provide() has returned; Stop now occurs before the second one. + releaseConsumer.countDown(); + try { + // Runs after the real Consumers task and its worker transaction cleanup. + worker.submit(() -> { }).get(5L, TimeUnit.SECONDS); + } catch (Exception e) { + throw new AssertionError(e); + } + } + return this.reads < 100; + } + + @Override + public Iterator next() { + if (this.reads >= 100) { + throw new java.util.NoSuchElementException(); + } + return this.reads++ == 0 ? input : Collections.emptyIterator(); + } + + @Override + public void close() { + this.closed = true; + } + } + LazyInputs source = new LazyInputs(); + try { + long total = traverser.traverseLazy(source, iterator -> { + try { + Assert.assertTrue("Producer must return from its first provide()", + releaseConsumer.await(5L, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } + consumer.accept(iterator); + }); + Assert.assertEquals((long) source.reads, total); + Assert.assertTrue("Traversal must close its lazy producer", source.closed); + return source.reads; + } finally { + releaseConsumer.countDown(); + OltpTraverser.destroy(); + Assert.assertTrue(worker.awaitTermination(5L, TimeUnit.SECONDS)); + } + } + + private static RocksIterator nativeCursor(Object value, Set visited) throws Exception { + if (value instanceof RocksIterator) { + return (RocksIterator) value; + } + if (!(value instanceof Iterator || value instanceof QueryResults || value instanceof QueryBatch) || + !visited.add(value)) { + return null; + } + for (Class type = value.getClass(); type != null && + type.getName().startsWith("org.apache.hugegraph."); type = type.getSuperclass()) { + for (Field field : type.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + field.setAccessible(true); + RocksIterator found = nativeCursor(field.get(value), visited); + if (found != null) { + return found; + } + } + } + return null; + } + + private static class Probe extends OltpTraverser { + + Probe(HugeGraph graph) { + super(graph); + } + + long traverseLazy(Iterator> source, java.util.function.Consumer> consumer) { + return this.traverseByOne(source, consumer, "native-lazy-producer-stop"); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/OltpTraverserRocksDBTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/OltpTraverserRocksDBTest.java new file mode 100644 index 0000000000..9e9e9c6e4e --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/OltpTraverserRocksDBTest.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.hugegraph.core; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.configuration2.MapConfiguration; +import org.apache.commons.io.FileUtils; +import org.apache.hugegraph.HugeFactory; +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.testutil.Utils; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.traversal.algorithm.KneighborTraverser; +import org.apache.hugegraph.traversal.algorithm.OltpTraverser; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.util.Consumers; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.ColumnFamilyOptions; +import org.rocksdb.DBOptions; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; + +public class OltpTraverserRocksDBTest { + + @Before + public void requireRocksDB() { + Assume.assumeTrue("Native lease regression requires the RocksDB profile", + "rocksdb".equals(Utils.getConf().getString("backend"))); + CoreTestSuite.initEnv(); + } + + @Test + public void testKneighborReturnsWorkerLeasesAndAllowsImmediateReopen() throws Exception { + this.checkNativeCleanup(false); + } + + @Test + public void testKneighborPreservesPendingCallerTransaction() throws Exception { + this.checkNativeCleanup(true); + } + + private void checkNativeCleanup(boolean pendingCaller) throws Exception { + Path directory = Files.createTempDirectory("oltp-bfs-rocks-"); + HugeConfig config = new HugeConfig(new MapConfiguration(Collections.emptyMap())); + config.setProperty("backend", "rocksdb"); + config.setProperty("serializer", "binary"); + config.setProperty("store", "oltp_bfs"); + config.setProperty("oltp.concurrent_threads", 1); + config.setProperty("rocksdb.data_path", directory.toString()); + config.setProperty("rocksdb.wal_path", directory.toString()); + OltpTraverser.destroy(); + HugeGraph graph; + try { + graph = HugeFactory.open(config); + } catch (RuntimeException | Error failure) { + FileUtils.deleteDirectory(directory.toFile()); + throw failure; + } + ExecutorService worker = null; + Consumers.ExecutorPool executors = null; + RocksDBSessions sessions = null; + try { + graph.initBackend(); + graph.schema().propertyKey("name").asText().create(); + graph.schema().vertexLabel("person").properties("name").primaryKeys("name").create(); + graph.schema().edgeLabel("knows").sourceLabel("person").targetLabel("person").create(); + Vertex a = graph.addVertex(T.label, "person", "name", "a"); + Vertex b = graph.addVertex(T.label, "person", "name", "b"); + Vertex c = graph.addVertex(T.label, "person", "name", "c"); + a.addEdge("knows", b); + b.addEdge("knows", c); + graph.tx().commit(); + HugeFactory.closeCurrentThreadTransactions(); + Vertex pending = pendingCaller ? graph.addVertex(T.label, "person", "name", "pending") : null; + try (KneighborTraverser traverser = new KneighborTraverser(graph)) { + // Pending writes prohibit a limited backend query; use unlimited degree for that case. + long degree = pendingCaller ? OltpTraverser.NO_LIMIT : 1L; + Assert.assertEquals(Set.of(b.id(), c.id()), + traverser.kneighbor((Id) a.id(), Directions.OUT, "knows", 2, degree, 100L)); + } + Object store = graph.storeProvider().loadGraphStore(config); + sessions = Whitebox.getInternalState(store, "sessions"); + Map leases = Whitebox.getInternalState(sessions, "sessions"); + executors = Whitebox.getInternalState(OltpTraverser.class, "executors"); + worker = executors.getExecutor(); + worker.submit(() -> { + Assert.assertFalse(graph.tx().isOpen()); + Assert.assertFalse("OLTP worker retained a native session despite closing its owner", + leases.containsKey(Thread.currentThread().getId())); + }).get(10L, TimeUnit.SECONDS); + if (pendingCaller) { + Assert.assertTrue(graph.tx().isOpen()); + this.assertVertexName(graph, pending.id(), "pending"); + graph.tx().commit(); + this.assertVertexName(graph, pending.id(), "pending"); + } + HugeFactory.closeCurrentThreadTransactions(); + graph.close(); + // Preserve the original directory and all CFs: the first open must reuse this database. + Path data = directory.resolve("g"); + Assert.assertTrue(Files.isRegularFile(data.resolve("CURRENT"))); + List families; + try (Options options = new Options()) { + families = RocksDB.listColumnFamilies(options, data.toString()); + } + Assert.assertTrue("Fixture must contain the original graph CFs", families.size() > 1); + try (DBOptions options = new DBOptions().setCreateIfMissing(false); + ColumnFamilyOptions familyOptions = new ColumnFamilyOptions()) { + List descriptors = new ArrayList<>(); + for (byte[] family : families) { + descriptors.add(new ColumnFamilyDescriptor(family, familyOptions)); + } + List handles = new ArrayList<>(); + RocksDB reopened = null; + try { + reopened = RocksDB.open(options, data.toString(), descriptors, handles); + Assert.assertTrue(reopened.isOwningHandle()); + Assert.assertEquals(families.size(), handles.size()); + } finally { + try { + for (ColumnFamilyHandle handle : handles) { + handle.close(); + } + } finally { + if (reopened != null) { + reopened.close(); + } + } + } + } + } finally { + try { + if (worker != null) { + RocksDBSessions cleanupSessions = sessions; + try { + worker.submit(() -> { + HugeFactory.closeCurrentThreadTransactions(); + // Repair leaked leases only in teardown so the failing baseline releases native locks. + ThreadLocal local = Whitebox.getInternalState(cleanupSessions, "threadLocalSession"); + for (int i = 0; local.get() != null && i < 8; i++) { + cleanupSessions.close(); + } + Assert.assertNull(local.get()); + }).get(10L, TimeUnit.SECONDS); + } finally { + executors.returnExecutor(worker); + } + } + } finally { + try { + HugeFactory.closeCurrentThreadTransactions(); + if (!graph.closed()) { + graph.drop(); + } + } finally { + HugeFactory.remove(graph); + OltpTraverser.destroy(); + FileUtils.deleteDirectory(directory.toFile()); + } + } + } + } + + private void assertVertexName(HugeGraph graph, Object id, String name) { + Iterator vertices = graph.vertices(id); + try { + Assert.assertTrue(vertices.hasNext()); + Assert.assertEquals(name, vertices.next().value("name")); + Assert.assertFalse(vertices.hasNext()); + } finally { + CloseableIterator.closeIterator(vertices); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/ParentAndSubEdgeCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/ParentAndSubEdgeCoreTest.java index 4d7cc3eea3..b657b62c57 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/ParentAndSubEdgeCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/ParentAndSubEdgeCoreTest.java @@ -21,7 +21,7 @@ import java.util.List; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.testutil.Assert; import org.apache.tinkerpop.gremlin.structure.Edge; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/PropertyCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/PropertyCoreTest.java index 873efd5e84..755380fc8b 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/PropertyCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/PropertyCoreTest.java @@ -26,7 +26,7 @@ import java.util.UUID; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Utils; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/PropertyKeyCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/PropertyKeyCoreTest.java index 0609f607ba..6cc2de8389 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/PropertyKeyCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/PropertyKeyCoreTest.java @@ -19,19 +19,24 @@ import java.util.Date; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.exception.ExistedException; import org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.exception.NotFoundException; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.Userdata; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.task.TaskStatus; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.define.AggregateType; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.GraphReadMode; import org.apache.hugegraph.type.define.WriteType; import org.apache.hugegraph.util.DateUtil; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Vertex; import org.junit.Assume; import org.junit.Test; @@ -452,6 +457,48 @@ public void testClearOlapPropertyKey() { Assert.assertEquals(WriteType.OLAP_SECONDARY, wcc.writeType()); } + @Test + public void testClearAndRemoveOlapDataKeepsOtherProperty() throws Exception { + Assume.assumeTrue("Not support olap properties", storeFeatures().supportsOlapProperties()); + SchemaManager schema = graph().schema(); + schema.propertyKey("olap-owner").asText().create(); + PropertyKey clear = schema.propertyKey("olap-clear").asText() + .writeType(WriteType.OLAP_COMMON).create(); + schema.propertyKey("olap-keep").asText().writeType(WriteType.OLAP_COMMON).create(); + schema.vertexLabel("olap-person").properties("olap-owner").primaryKeys("olap-owner").create(); + Vertex owner = graph().addVertex(T.label, "olap-person", "olap-owner", "alice"); + this.commitTx(); + graph().addVertex(T.id, owner.id(), "olap-clear", "removed"); + this.commitTx(); + graph().addVertex(T.id, owner.id(), "olap-keep", "kept"); + this.commitTx(); + try { + graph().readMode(GraphReadMode.ALL); + Vertex before = graph().vertices(owner.id()).next(); + Assert.assertEquals("removed", before.value("olap-clear")); + Assert.assertEquals("kept", before.value("olap-keep")); + this.commitTx(); + Id clearTask = graph().clearPropertyKey(clear); + Assert.assertEquals(TaskStatus.SUCCESS, + graph().taskScheduler().waitUntilTaskCompleted(clearTask, 60).status()); + Assert.assertEquals(clear.id(), schema.getPropertyKey("olap-clear").id()); + Vertex after = graph().vertices(owner.id()).next(); + Assert.assertFalse(after.property("olap-clear").isPresent()); + Assert.assertEquals("kept", after.value("olap-keep")); + this.commitTx(); + + graph().addVertex(T.id, owner.id(), "olap-clear", "removed-again"); + this.commitTx(); + Id removeTask = schema.propertyKey("olap-clear").remove(); + Assert.assertEquals(TaskStatus.SUCCESS, + graph().taskScheduler().waitUntilTaskCompleted(removeTask, 60).status()); + Assert.assertThrows(NotFoundException.class, () -> schema.getPropertyKey("olap-clear")); + Assert.assertEquals("kept", graph().vertices(owner.id()).next().value("olap-keep")); + } finally { + graph().readMode(GraphReadMode.OLTP_ONLY); + } + } + @Test public void testRemoveOlapPropertyKey() { Assume.assumeTrue("Not support olap properties", diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/RamTableTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/RamTableTest.java index 8a1f6c4910..359505d945 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/RamTableTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/RamTableTest.java @@ -24,9 +24,9 @@ import org.apache.commons.io.FileUtils; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.store.ram.RamTable; import org.apache.hugegraph.backend.tx.GraphTransaction; import org.apache.hugegraph.schema.SchemaManager; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/RoleElectionStateMachineTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/RoleElectionStateMachineTest.java deleted file mode 100644 index eab1e6b791..0000000000 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/RoleElectionStateMachineTest.java +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.core; - -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.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.locks.LockSupport; - -import org.apache.hugegraph.masterelection.ClusterRole; -import org.apache.hugegraph.masterelection.ClusterRoleStore; -import org.apache.hugegraph.masterelection.Config; -import org.apache.hugegraph.masterelection.RoleElectionStateMachine; -import org.apache.hugegraph.masterelection.RoleListener; -import org.apache.hugegraph.masterelection.StandardRoleElectionStateMachine; -import org.apache.hugegraph.masterelection.StateMachineContext; -import org.apache.hugegraph.testutil.Assert; -import org.apache.hugegraph.testutil.Utils; -import org.junit.Test; - -public class RoleElectionStateMachineTest { - - private static class LogEntry { - - private final Integer epoch; - - private final String node; - - private final Role role; - - enum Role { - master, - worker, - candidate, - abdication, - unknown - } - - public LogEntry(Integer epoch, String node, Role role) { - this.epoch = epoch; - this.node = node; - this.role = role; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof LogEntry)) { - return false; - } - LogEntry logEntry = (LogEntry) obj; - return Objects.equals(this.epoch, logEntry.epoch) && - Objects.equals(this.node, logEntry.node) && - this.role == logEntry.role; - } - - @Override - public int hashCode() { - return Objects.hash(this.epoch, this.node, this.role); - } - - @Override - public String toString() { - return "LogEntry{" + - "epoch=" + this.epoch + - ", node='" + this.node + '\'' + - ", role=" + this.role + - '}'; - } - } - - private static class TestConfig implements Config { - - private final String node; - - public TestConfig(String node) { - this.node = node; - } - - @Override - public String node() { - return this.node; - } - - @Override - public String url() { - return "http://127.0.0.1:8080"; - } - - @Override - public int exceedsFailCount() { - return 2; - } - - @Override - public long randomTimeoutMillisecond() { - return 400; - } - - @Override - public long heartBeatIntervalSecond() { - return 1; - } - - @Override - public int masterDeadTimes() { - return 5; - } - - @Override - public long baseTimeoutMillisecond() { - return 100; - } - } - - @Test - public void testStateMachine() throws InterruptedException { - final int MAX_COUNT = 200; - CountDownLatch stop = new CountDownLatch(4); - List logRecords = Collections.synchronizedList(new ArrayList<>(MAX_COUNT)); - List masterNodes = Collections.synchronizedList(new ArrayList<>(MAX_COUNT)); - RoleListener callback = new RoleListener() { - - @Override - public void onAsRoleMaster(StateMachineContext context) { - Integer epochId = context.epoch(); - String node = context.node(); - logRecords.add(new LogEntry(epochId, node, LogEntry.Role.master)); - if (logRecords.size() > MAX_COUNT) { - context.stateMachine().shutdown(); - } - Utils.println("master node: " + node); - masterNodes.add(node); - } - - @Override - public void onAsRoleWorker(StateMachineContext context) { - Integer epochId = context.epoch(); - String node = context.node(); - logRecords.add(new LogEntry(epochId, node, LogEntry.Role.worker)); - if (logRecords.size() > MAX_COUNT) { - context.stateMachine().shutdown(); - } - } - - @Override - public void onAsRoleCandidate(StateMachineContext context) { - Integer epochId = context.epoch(); - String node = context.node(); - logRecords.add(new LogEntry(epochId, node, LogEntry.Role.candidate)); - if (logRecords.size() > MAX_COUNT) { - context.stateMachine().shutdown(); - } - } - - @Override - public void unknown(StateMachineContext context) { - Integer epochId = context.epoch(); - String node = context.node(); - logRecords.add(new LogEntry(epochId, node, LogEntry.Role.unknown)); - if (logRecords.size() > MAX_COUNT) { - context.stateMachine().shutdown(); - } - } - - @Override - public void onAsRoleAbdication(StateMachineContext context) { - Integer epochId = context.epoch(); - String node = context.node(); - logRecords.add(new LogEntry(epochId, node, LogEntry.Role.abdication)); - if (logRecords.size() > MAX_COUNT) { - context.stateMachine().shutdown(); - } - } - - @Override - public void error(StateMachineContext context, Throwable e) { - Utils.println("state machine error: node " + - context.node() + " message " + e.getMessage()); - } - }; - - final List clusterRoleLogs = Collections.synchronizedList( - new ArrayList<>(100)); - - final ClusterRoleStore clusterRoleStore = new ClusterRoleStore() { - - volatile int epoch = 0; - - final Map data = new ConcurrentHashMap<>(); - - ClusterRole copy(ClusterRole clusterRole) { - if (clusterRole == null) { - return null; - } - return new ClusterRole(clusterRole.node(), clusterRole.url(), - clusterRole.epoch(), clusterRole.clock()); - } - - @Override - public boolean updateIfNodePresent(ClusterRole clusterRole) { - if (clusterRole.epoch() < this.epoch) { - return false; - } - - ClusterRole copy = this.copy(clusterRole); - ClusterRole newClusterRole = this.data.compute(copy.epoch(), (key, value) -> { - if (copy.epoch() > this.epoch) { - this.epoch = copy.epoch(); - Assert.assertNull(value); - clusterRoleLogs.add(copy); - Utils.println("The node " + copy + " become new master:"); - return copy; - } - - Assert.assertEquals(value.epoch(), copy.epoch()); - if (Objects.equals(value.node(), copy.node()) && - value.clock() <= copy.clock()) { - Utils.println("The master node " + copy + " keep heartbeat"); - clusterRoleLogs.add(copy); - if (value.clock() == copy.clock()) { - Assert.fail("Clock must increase when same epoch and node id"); - } - return copy; - } - return value; - - }); - return Objects.equals(newClusterRole, copy); - } - - @Override - public Optional query() { - return Optional.ofNullable(this.copy(this.data.get(this.epoch))); - } - }; - - RoleElectionStateMachine[] machines = new RoleElectionStateMachine[4]; - Thread node1 = new Thread(() -> { - Config config = new TestConfig("1"); - RoleElectionStateMachine stateMachine = - new StandardRoleElectionStateMachine(config, clusterRoleStore); - machines[1] = stateMachine; - stateMachine.start(callback); - stop.countDown(); - }); - - Thread node2 = new Thread(() -> { - Config config = new TestConfig("2"); - RoleElectionStateMachine stateMachine = - new StandardRoleElectionStateMachine(config, clusterRoleStore); - machines[2] = stateMachine; - stateMachine.start(callback); - stop.countDown(); - }); - - Thread node3 = new Thread(() -> { - Config config = new TestConfig("3"); - RoleElectionStateMachine stateMachine = - new StandardRoleElectionStateMachine(config, clusterRoleStore); - machines[3] = stateMachine; - stateMachine.start(callback); - stop.countDown(); - }); - - node1.start(); - node2.start(); - node3.start(); - - Thread randomShutdown = new Thread(() -> { - Set dropNodes = new HashSet<>(); - while (dropNodes.size() < 3) { - LockSupport.parkNanos(5_000_000_000L); - int size = masterNodes.size(); - if (size < 1) { - continue; - } - String node = masterNodes.get(size - 1); - if (dropNodes.contains(node)) { - continue; - } - machines[Integer.parseInt(node)].shutdown(); - dropNodes.add(node); - Utils.println("----shutdown machine " + node); - } - stop.countDown(); - }); - - randomShutdown.start(); - stop.await(); - - Assert.assertGt(0, logRecords.size()); - Map masters = new HashMap<>(); - for (LogEntry entry : logRecords) { - if (entry.role == LogEntry.Role.master) { - String lastNode = masters.putIfAbsent(entry.epoch, entry.node); - if (lastNode != null) { - Assert.assertEquals(lastNode, entry.node); - } - } - } - - Assert.assertGt(0, masters.size()); - } -} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SchemaCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SchemaCoreTest.java index 0d70bb203b..b459224706 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SchemaCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SchemaCoreTest.java @@ -22,12 +22,12 @@ import java.util.List; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.testutil.Assert; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.Vertex; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java new file mode 100644 index 0000000000..f132091f1c --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java @@ -0,0 +1,1240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.core; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.apache.hugegraph.backend.page.PageInfo; +import org.apache.hugegraph.backend.serializer.AbstractSerializer; +import org.apache.hugegraph.backend.store.BackendEntry; +import org.apache.hugegraph.backend.store.BackendStore; +import org.apache.hugegraph.backend.tx.GraphIndexTransaction; +import org.apache.hugegraph.exception.NoIndexException; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.schema.SchemaManager; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.tinkerpop.TestGraph; +import org.apache.hugegraph.traversal.optimize.HugeGraphStep; +import org.apache.hugegraph.traversal.optimize.Text; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.GraphReadMode; +import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.hugegraph.type.define.IdStrategy; +import org.apache.hugegraph.type.define.SchemaStatus; +import org.apache.hugegraph.type.define.WriteType; +import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.mockito.AdditionalAnswers; +import org.mockito.Mockito; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +public class SourceIndexCoverageTest extends BaseCoreTest { + + @Before + public void initSchema() { + SchemaManager schema = graph().schema(); + schema.propertyKey("coverageName").asText().create(); + schema.propertyKey("coverageScore").asInt().create(); + schema.propertyKey("coverageBody").asText().create(); + schema.vertexLabel("coverageA") + .properties("coverageName", "coverageScore", "coverageBody") + .useAutomaticId().create(); + schema.vertexLabel("coverageB") + .properties("coverageName", "coverageScore", "coverageBody") + .enableLabelIndex(false).useAutomaticId().create(); + schema.vertexLabel("unrelated").properties("coverageBody").useAutomaticId().create(); + } + + private Set addVertices() { + Vertex a = graph().addVertex(T.label, "coverageA", "coverageName", "same", + "coverageScore", 10, "coverageBody", "gold silver"); + Vertex b = graph().addVertex(T.label, "coverageB", "coverageName", "same", + "coverageScore", 20, "coverageBody", "gold bronze"); + this.commitTx(); + return ImmutableSet.of(a, b); + } + + private void nameIndex(String label) { + graph().schema().indexLabel(label + "ByName").onV(label) + .by("coverageName").secondary().create(); + } + + @Test + public void testCompleteCandidatesReturnAllResults() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + Set expected = this.addVertices(); + Assert.assertEquals(expected, + graph().traversal().V().has("coverageName", "same").toSet()); + } + + @Test + public void testTinkerPopModernSchemaCoversGlobalPropertyQueries() { + new TestGraph(graph()).initModernSchema(IdStrategy.AUTOMATIC); + Vertex person = graph().addVertex(T.label, "person", "name", "marko", "age", 29); + Vertex software = graph().addVertex(T.label, "software", "name", "lop"); + Edge created = person.addEdge("created", software, "weight", 0.4d); + this.commitTx(); + + Assert.assertEquals(ImmutableSet.of(person), + graph().traversal().V().has("name", "marko").toSet()); + Assert.assertEquals(ImmutableSet.of(person), + graph().traversal().V().has("age", 29).toSet()); + Assert.assertEquals(ImmutableSet.of(created), + graph().traversal().E().has("weight", 0.4d).toSet()); + } + + @Test + public void testTinkerPopBasicSchemaCoversSharedProperties() { + new TestGraph(graph()).initBasicSchema(IdStrategy.AUTOMATIC, TestGraph.DEFAULT_VL); + Vertex vertex = graph().addVertex(T.label, TestGraph.DEFAULT_VL, "name", "shared"); + Vertex person = graph().addVertex(T.label, "person", "name", "shared"); + Vertex software = graph().addVertex(T.label, "software", "name", "shared"); + Edge self = vertex.addEdge("self", vertex, "name", "shared"); + Edge friend = vertex.addEdge("friend", vertex, "name", "shared"); + Edge link = vertex.addEdge("l", vertex, "name", "shared"); + Edge partition = vertex.addEdge("aTOa", vertex, + "gremlin.partitionGraphStrategy.partition", "shared"); + this.commitTx(); + + Assert.assertEquals(ImmutableSet.of(vertex, person, software), + graph().traversal().V().has("name", "shared").toSet()); + Assert.assertEquals(ImmutableSet.of(self, friend, link), + graph().traversal().E().has("name", "shared").toSet()); + Assert.assertEquals(ImmutableSet.of(partition), graph().traversal().E() + .has("gremlin.partitionGraphStrategy.partition", "shared").toSet()); + } + + @Test + public void testTinkerPopSinkSchemaCoversName() { + new TestGraph(graph()).initSinkSchema(); + Vertex message = graph().addVertex(T.label, "message", "name", "ping"); + this.commitTx(); + + Assert.assertEquals(ImmutableSet.of(message), + graph().traversal().V().has("name", "ping").toSet()); + } + + @Test + public void testPartialCoverageRejectsIncompleteSource() { + this.nameIndex("coverageA"); + Set expected = this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().V().has("coverageName", "same").toSet(), error -> { + Assert.assertContains("Incomplete index coverage", error.getMessage()); + Assert.assertFalse(error.getMessage().contains("coverageB")); + }); + Assert.assertEquals(1, graph().traversal().V().hasLabel("coverageA") + .has("coverageName", "same").toList().size()); + this.nameIndex("coverageB"); + Assert.assertEquals(expected, + graph().traversal().V().has("coverageName", "same").toSet()); + } + + @Test + public void testOrCoverageFailureClosesEarlierBranchCursors() { + this.prepareOrIndexes(false); + this.addVertices(); + try (IndexCursorTracker tracker = new IndexCursorTracker(null)) { + NoIndexException failure = Assert.assertThrows(NoIndexException.class, + () -> graph().vertices(this.orQuery())); + Assert.assertContains("Incomplete index coverage", failure.getMessage()); + tracker.assertCounts(2, 1); + Assert.assertEquals(0, failure.getSuppressed().length); + // Closing must happen before rollback or test-fixture cleanup. + graph().tx().rollback(); + tracker.assertCounts(2, 1); + } + } + + @Test + public void testOrCoverageFailurePreservesOriginalErrorWhenCloseFails() { + this.prepareOrIndexes(false); + this.addVertices(); + RuntimeException closeFailure = new IllegalStateException("index cursor close failed"); + try (IndexCursorTracker tracker = new IndexCursorTracker(closeFailure)) { + NoIndexException failure = Assert.assertThrows(NoIndexException.class, + () -> graph().vertices(this.orQuery())); + Assert.assertContains("Incomplete index coverage", failure.getMessage()); + // A failed close on the first cursor must not skip the second one. + tracker.assertCounts(2, 1); + Assert.assertEquals(1, failure.getSuppressed().length); + Assert.assertSame(closeFailure, Throwables.getRootCause(failure.getSuppressed()[0])); + } + } + + @Test + public void testCompleteOrPlanKeepsCursorsUntilResultClose() { + this.prepareOrIndexes(true); + Set expected = this.addVertices(); + try (IndexCursorTracker tracker = new IndexCursorTracker(null)) { + Iterator results = graph().vertices(this.orQuery()); + try { + tracker.assertCounts(4, 0); + Assert.assertTrue(results.hasNext()); + Assert.assertTrue(expected.contains(results.next())); + } finally { + CloseableIterator.closeIterator(results); + } + // Includes the later OR branch whose results were never requested. + tracker.assertCounts(4, 1); + } + } + + @Test + public void testLabeledSubEdgeUsesParentIndex() { + Assume.assumeTrue(storeFeatures().supportsFatherAndSubEdgeLabel()); + SchemaManager schema = graph().schema(); + List edges = this.addSubEdges(); + Edge expected = edges.get(0); + Edge sibling = edges.get(1); + schema.indexLabel("parentEdgeByName").onE("parentEdge") + .by("coverageName").secondary().create(); + Assert.assertEquals(2, + graph().traversal().E().has("coverageName", "same").toList().size()); + Assert.assertEquals(ImmutableList.of(sibling), graph().traversal().E() + .hasLabel("subB").has("coverageName", "same").limit(1).toList()); + Assert.assertEquals(ImmutableSet.of(expected), graph().traversal().E() + .hasLabel("subA").has("coverageName", "same").toSet()); + Assert.assertEquals(ImmutableList.of(expected), graph().traversal().E() + .hasLabel("subA").has("coverageName", "same").limit(1).toList()); + if (storeFeatures().supportsQueryByPage()) { + for (Edge edge : edges) { + Set actual = new HashSet<>(); + String page = ""; + int pages = 0; + do { + ConditionQuery query = this.nameQuery(HugeType.EDGE); + query.eq(HugeKeys.LABEL, graph().edgeLabel(edge.label()).id()); + query.limit(1); + query.page(page); + Iterator results = graph().edges(query); + try { + results.forEachRemaining(actual::add); + page = PageInfo.pageInfo(results); + } finally { + CloseableIterator.closeIterator(results); + } + Assert.assertTrue(++pages < 10); + } while (page != null && !page.isEmpty()); + Assert.assertEquals(ImmutableSet.of(edge), actual); + } + } + + SchemaLabel parentLabel = schema.getEdgeLabel("parentEdge"); + try { + this.params().schemaTransaction().updateSchemaStatus(parentLabel, SchemaStatus.DELETING); + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().E() + .hasLabel("subA").has("coverageName", "same").toSet()); + ConditionQuery query = this.nameQuery(HugeType.EDGE); + query.eq(HugeKeys.LABEL, graph().edgeLabel("subA").id()); + query.showDeleting(true); + Assert.assertEquals(ImmutableSet.of(expected), ImmutableSet.copyOf(graph().edges(query))); + } finally { + this.params().schemaTransaction().updateSchemaStatus(parentLabel, SchemaStatus.CREATED); + } + + IndexLabel parent = schema.getIndexLabel("parentEdgeByName"); + try { + this.params().schemaTransaction().updateSchemaStatus(parent, SchemaStatus.REBUILDING); + try (IndexCursorTracker tracker = new IndexCursorTracker(null)) { + Assert.assertThrows(IllegalArgumentException.class, () -> graph().traversal().E() + .hasLabel("subA").has("coverageName", "same").toSet(), error -> + Assert.assertContains("REBUILDING", error.getMessage())); + tracker.assertCounts(0, 0); + } + schema.indexLabel("subAByName").onE("subA").by("coverageName").secondary().create(); + // A compatible direct index is preferred over the parent fallback. + try (IndexCursorTracker tracker = new IndexCursorTracker(null)) { + Assert.assertEquals(ImmutableSet.of(expected), graph().traversal().E() + .hasLabel("subA").has("coverageName", "same").toSet()); + tracker.assertCounts(1, 1); + } + } finally { + this.params().schemaTransaction().updateSchemaStatus(parent, SchemaStatus.CREATED); + } + } + + @Test + public void testLabeledSubEdgeUsesParentRangeJointAndSearchIndexes() { + Assume.assumeTrue(storeFeatures().supportsFatherAndSubEdgeLabel()); + SchemaManager schema = graph().schema(); + Edge expected = this.addSubEdges().get(1); + schema.indexLabel("parentByScore").onE("parentEdge").by("coverageScore").range().create(); + Assert.assertEquals(ImmutableList.of(expected), graph().traversal().E() + .hasLabel("subB").has("coverageScore", P.gte(1)).limit(1).toList()); + schema.indexLabel("parentByName").onE("parentEdge").by("coverageName").secondary().create(); + Assert.assertEquals(ImmutableList.of(expected), graph().traversal().E() + .hasLabel("subB").has("coverageName", "same") + .has("coverageScore", P.gte(1)).limit(1).toList()); + schema.indexLabel("parentByName").remove(); + schema.indexLabel("parentSearchByName").onE("parentEdge").by("coverageName").search().create(); + Assert.assertEquals(ImmutableList.of(expected), graph().traversal().E() + .hasLabel("subB").has("coverageName", Text.contains("same")).limit(1).toList()); + } + + private List addSubEdges() { + SchemaManager schema = graph().schema(); + schema.edgeLabel("parentEdge").asBase().properties("coverageName", "coverageScore") + .multiTimes().sortKeys("coverageScore").create(); + for (String label : new String[]{"subA", "subB"}) { + schema.edgeLabel(label).withBase("parentEdge").link("coverageA", "coverageB") + .properties("coverageName", "coverageScore") + .multiTimes().sortKeys("coverageScore").create(); + } + Set vertices = this.addVertices(); + Vertex a = vertices.stream().filter(v -> v.label().equals("coverageA")).findFirst().get(); + Vertex b = vertices.stream().filter(v -> v.label().equals("coverageB")).findFirst().get(); + Edge first = a.addEdge("subA", b, "coverageName", "same", "coverageScore", 1); + Edge second = a.addEdge("subB", b, "coverageName", "same", "coverageScore", 2); + this.commitTx(); + return ImmutableList.of(first, second); + } + + @Test + public void testSingleBranchFailureClosesPartialCursors() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + this.addVertices(); + RuntimeException injected = new IllegalStateException("second index query failed"); + try (IndexCursorTracker tracker = new IndexCursorTracker(null, injected, 2)) { + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> graph().vertices(this.nameQuery(HugeType.VERTEX))); + Assert.assertSame(injected, Throwables.getRootCause(failure)); + tracker.assertCounts(1, 1); + } + } + + @Test + public void testJointBranchFailureClosesPartialCursors() { + this.prepareJointIndexes(); + ConditionQuery query = this.jointQuery(); + RuntimeException injected = new IllegalStateException("second joint index query failed"); + try (IndexCursorTracker tracker = new IndexCursorTracker(null, injected, 2)) { + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> graph().vertices(query)); + Assert.assertSame(injected, Throwables.getRootCause(failure)); + tracker.assertCounts(1, 1); + } + } + + @Test + public void testSingleBranchFailurePreservesErrorAndClosesAllCursors() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + graph().schema().vertexLabel("coverageC").properties("coverageName") + .useAutomaticId().create(); + this.nameIndex("coverageC"); + this.addVertices(); + RuntimeException injected = new IllegalStateException("third index query failed"); + RuntimeException closing = new IllegalStateException("first index cursor close failed"); + try (IndexCursorTracker tracker = new IndexCursorTracker(closing, injected, 3)) { + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> graph().vertices(this.nameQuery(HugeType.VERTEX))); + Assert.assertSame(injected, Throwables.getRootCause(failure)); + tracker.assertCounts(2, 1); + Assert.assertEquals(1, injected.getSuppressed().length); + Assert.assertSame(closing, Throwables.getRootCause(injected.getSuppressed()[0])); + } + } + + @Test + public void testJointPlanClosesUnselectedCursorsAndTransfersSelected() { + Set expected = this.prepareJointIndexes(); + ConditionQuery query = this.jointQuery(); + try (IndexCursorTracker tracker = new IndexCursorTracker(null)) { + Iterator results = graph().vertices(query); + try { + Assert.assertEquals(2, tracker.cursors.size()); + Assert.assertEquals(1, tracker.cursors.stream().mapToInt(cursor -> cursor.closes).sum()); + Assert.assertEquals(expected, ImmutableSet.copyOf(results)); + } finally { + CloseableIterator.closeIterator(results); + } + tracker.assertCounts(2, 1); + } + } + + @Test + public void testJointReadFailureClosesAllCursorsAndPreservesError() { + this.prepareJointIndexes(); + RuntimeException injected = new IllegalStateException("second joint cursor read failed"); + RuntimeException closing = new IllegalStateException("first joint cursor close failed"); + try (IndexCursorTracker tracker = new IndexCursorTracker(closing)) { + tracker.failOnRead = 2; + tracker.readFailure = injected; + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> graph().vertices(this.jointQuery())); + Assert.assertSame(injected, Throwables.getRootCause(failure)); + tracker.assertCounts(2, 1); + Assert.assertEquals(1, injected.getSuppressed().length); + Assert.assertSame(closing, Throwables.getRootCause(injected.getSuppressed()[0])); + } + } + + @Test + public void testSearchReadFailureClosesCursorAndPreservesError() { + graph().schema().indexLabel("coverageASearch").onV("coverageA") + .by("coverageBody").search().create(); + this.addVertices(); + RuntimeException injected = new IllegalStateException("search index cursor read failed"); + RuntimeException closing = new IllegalStateException("search index cursor close failed"); + try (IndexCursorTracker tracker = new IndexCursorTracker(closing)) { + tracker.failOnRead = 1; + tracker.readFailure = injected; + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> graph().traversal().V().hasLabel("coverageA") + .has("coverageBody", Text.contains("gold")).toSet()); + Assert.assertSame(injected, Throwables.getRootCause(failure)); + tracker.assertCounts(1, 1); + Assert.assertEquals(1, injected.getSuppressed().length); + Assert.assertSame(closing, Throwables.getRootCause(injected.getSuppressed()[0])); + } + } + + @Test + public void testSearchDecodeFailureClosesCursorAndPreservesError() { + graph().schema().indexLabel("coverageASearch").onV("coverageA") + .by("coverageBody").search().create(); + this.addVertices(); + RuntimeException injected = new IllegalStateException("search index decode failed"); + RuntimeException closing = new IllegalStateException("search index cursor close failed"); + GraphIndexTransaction indexTx = Whitebox.getInternalState(params().graphTransaction(), "indexTx"); + AbstractSerializer original = Whitebox.getInternalState(indexTx, "serializer"); + AbstractSerializer observed = Mockito.mock(AbstractSerializer.class, + AdditionalAnswers.delegatesTo(original)); + Mockito.doAnswer(invocation -> { + // Decode a real backend entry before failing outside the iterator wrapper. + original.readIndex(invocation.getArgument(0), invocation.getArgument(1), + invocation.getArgument(2)); + throw injected; + }).when(observed).readIndex(Mockito.any(), Mockito.any(), Mockito.any()); + Whitebox.setInternalState(indexTx, "serializer", observed); + try (IndexCursorTracker tracker = new IndexCursorTracker(closing)) { + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> graph().traversal().V().hasLabel("coverageA") + .has("coverageBody", Text.contains("gold")).toSet()); + Assert.assertSame(injected, Throwables.getRootCause(failure)); + tracker.assertCounts(1, 1); + Assert.assertEquals(1, injected.getSuppressed().length); + Assert.assertSame(closing, Throwables.getRootCause(injected.getSuppressed()[0])); + } finally { + Whitebox.setInternalState(indexTx, "serializer", original); + } + } + + private Set prepareJointIndexes() { + this.nameIndex("coverageA"); + graph().schema().indexLabel("coverageAByScore").onV("coverageA") + .by("coverageScore").secondary().create(); + // Exceed the fixture's intersection threshold of 2 to retain lazy cursors. + Set expected = new HashSet<>(); + for (int i = 0; i < 3; i++) { + expected.add(graph().addVertex(T.label, "coverageA", "coverageName", "same", + "coverageScore", 10, "coverageBody", "gold")); + } + this.commitTx(); + return expected; + } + + private ConditionQuery jointQuery() { + ConditionQuery query = this.nameQuery(HugeType.VERTEX); + query.eq(HugeKeys.LABEL, graph().vertexLabel("coverageA").id()); + query.query(Condition.eq(graph().propertyKey("coverageScore").id(), 10)); + return query; + } + + private void prepareOrIndexes(boolean complete) { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + graph().schema().indexLabel("coverageAByScore").onV("coverageA") + .by("coverageScore").secondary().create(); + if (complete) { + graph().schema().indexLabel("coverageBByScore").onV("coverageB") + .by("coverageScore").secondary().create(); + } + } + + private ConditionQuery orQuery() { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.query(Condition.or( + Condition.eq(graph().propertyKey("coverageName").id(), "same"), + Condition.eq(graph().propertyKey("coverageScore").id(), 10))); + return query; + } + + private final class IndexCursorTracker implements AutoCloseable { + + private final GraphIndexTransaction indexTx; + private final BackendStore original; + private final List cursors = new ArrayList<>(); + private int failOnRead; + private RuntimeException readFailure; + + private IndexCursorTracker(RuntimeException closeFailure) { + this(closeFailure, null, 0); + } + + private IndexCursorTracker(RuntimeException closeFailure, RuntimeException queryFailure, + int failOnQuery) { + this.indexTx = Whitebox.getInternalState(params().graphTransaction(), "indexTx"); + this.original = Whitebox.getInternalState(this.indexTx, "store"); + BackendStore observed = Mockito.mock(BackendStore.class, + AdditionalAnswers.delegatesTo(this.original)); + Mockito.doAnswer(invocation -> { + if (queryFailure != null && this.cursors.size() + 1 == failOnQuery) { + throw queryFailure; + } + // Keep real index matching, validation, serialization and backend queries. + Iterator entries = this.original.query((Query) invocation.getArgument(0)); + CountingIndexCursor cursor = new CountingIndexCursor(entries, + this.cursors.isEmpty() ? closeFailure : null); + this.cursors.add(cursor); + if (this.cursors.size() == this.failOnRead) { + cursor.readFailure = this.readFailure; + } + return cursor; + }).when(observed).query(Mockito.any(Query.class)); + Whitebox.setInternalState(this.indexTx, "store", observed); + } + + private void assertCounts(int opened, int closesPerCursor) { + Assert.assertEquals(opened, this.cursors.size()); + for (CountingIndexCursor cursor : this.cursors) { + Assert.assertEquals(closesPerCursor, cursor.closes); + } + } + + @Override + public void close() { + Whitebox.setInternalState(this.indexTx, "store", this.original); + // Release leaks even when the regression assertion fails on old code. + for (CountingIndexCursor cursor : this.cursors) { + if (cursor.closes == 0) { + CloseableIterator.closeIterator(cursor.entries); + } + } + } + } + + private static final class CountingIndexCursor implements CloseableIterator { + + private final Iterator entries; + private final RuntimeException closeFailure; + private int closes; + private RuntimeException readFailure; + + private CountingIndexCursor(Iterator entries, RuntimeException closeFailure) { + this.entries = entries; + this.closeFailure = closeFailure; + } + + @Override + public boolean hasNext() { + if (this.readFailure != null) { + throw this.readFailure; + } + return this.entries.hasNext(); + } + + @Override + public BackendEntry next() { + return this.entries.next(); + } + + @Override + public void close() { + this.closes++; + CloseableIterator.closeIterator(this.entries); + if (this.closeFailure != null) { + throw this.closeFailure; + } + } + } + + @Test + public void testNegativeRootLabelKeepsPropertyFilterLocal() { + SchemaManager schema = graph().schema(); + schema.propertyKey("name").asText().create(); + for (String label : new String[]{"indexed", "unindexed"}) { + schema.vertexLabel(label).properties("name").useAutomaticId().create(); + } + schema.indexLabel("indexedByName").onV("indexed").by("name").secondary().create(); + Vertex allowed = graph().addVertex(T.label, "indexed", "name", "alice"); + graph().addVertex(T.label, "unindexed", "name", "alice"); + graph().addVertex(T.label, "indexed", "name", "bob"); + this.commitTx(); + + // Control: the unqualified indexed query really has incomplete coverage. + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().V() + .has("name", "alice").toSet(), error -> + Assert.assertContains("Incomplete index coverage", error.getMessage())); + + try (IndexCursorTracker tracker = new IndexCursorTracker(null)) { + GraphTraversal query = graph().traversal().V() + .hasLabel(P.neq("unindexed")).has("name", "alice"); + query.asAdmin().applyStrategies(); + HugeGraphStep source = (HugeGraphStep) query.asAdmin().getStartStep(); + // The negative label keeps both predicates local, so the source + // scans vertices without running property-index coverage checks. + Assert.assertTrue(source.getHasContainers().isEmpty()); + Assert.assertEquals(ImmutableSet.of(allowed), query.toSet()); + tracker.assertCounts(0, 0); + } + } + + @Test + public void testInvalidValuePrecedesPartialCoverageError() { + this.addVertices(); + ConditionQuery invalid = new ConditionQuery(HugeType.VERTEX); + invalid.query(Condition.eq(graph().propertyKey("coverageScore").id(), "not-an-int")); + Assert.assertThrows(NoIndexException.class, () -> graph().vertices(invalid).hasNext()); + + graph().schema().indexLabel("coverageAByScore").onV("coverageA") + .by("coverageScore").secondary().create(); + Assert.assertEquals(ImmutableList.of(), ImmutableList.copyOf(graph().vertices(invalid))); + + ConditionQuery valid = new ConditionQuery(HugeType.VERTEX); + valid.query(Condition.eq(graph().propertyKey("coverageScore").id(), 10)); + Assert.assertThrows(NoIndexException.class, () -> graph().vertices(valid).hasNext()); + } + + @Test + public void testOlapOnlyQueryAcrossLabels() { + // The existing OLAP secondary-property query also returns no results + // on HStore master; its backend coverage is tracked in #3090. + Assume.assumeFalse("HStore OLAP secondary query is tracked in #3090", + "hstore".equals(graph().backend())); + Assume.assumeTrue("Not support olap properties", + storeFeatures().supportsOlapProperties()); + graph().schema().propertyKey("coverageOlap").asText() + .writeType(WriteType.OLAP_SECONDARY).create(); + Set expected = this.addVertices(); + for (Vertex vertex : expected) { + graph().addVertex(T.id, vertex.id(), "coverageOlap", "same"); + } + this.commitTx(); + + GraphReadMode previous = graph().readMode(); + graph().readMode(GraphReadMode.ALL); + try { + Assert.assertEquals(expected, + graph().traversal().V().has("coverageOlap", "same").toSet()); + } finally { + graph().readMode(previous); + } + } + + @Test + public void testEmptyCandidateLabelStillRequiresCoverage() { + this.nameIndex("coverageA"); + graph().addVertex(T.label, "coverageA", "coverageName", "same", + "coverageScore", 10, "coverageBody", "gold"); + this.commitTx(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().V().has("coverageName", "same").toList()); + } + + @Test + public void testNoCoverageAndExplicitIds() { + Set expected = this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().V().has("coverageName", "same").toList()); + Object[] ids = expected.stream().map(Vertex::id).toArray(); + Assert.assertEquals(expected, + graph().traversal().V(ids).has("coverageName", "same").toSet()); + graph().schema().edgeLabel("coverageLink").link("coverageA", "coverageB").create(); + Vertex a = expected.stream().filter(v -> v.label().equals("coverageA")).findFirst().get(); + Vertex b = expected.stream().filter(v -> v.label().equals("coverageB")).findFirst().get(); + a.addEdge("coverageLink", b); + this.commitTx(); + Assert.assertEquals(ImmutableSet.of(b), graph().traversal().V(a.id()) + .out("coverageLink").has("coverageName", "same").toSet()); + } + + @Test + public void testDownstreamStepsCannotHidePartialCoverage() { + this.nameIndex("coverageA"); + this.addVertices(); + for (GraphTraversal query : new GraphTraversal[]{ + graph().traversal().V().has("coverageName", "same").limit(1), + graph().traversal().V().has("coverageName", "same").count(), + graph().traversal().V().has("coverageName", "same").out(), + graph().traversal().V().has("coverageName", "same").where(__.out()), + graph().traversal().V().has("coverageName", "same").barrier()}) { + Assert.assertThrows(NoIndexException.class, query::toList); + } + } + + @Test + public void testPreparedTraversalChecksCurrentSchema() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + this.addVertices(); + GraphTraversal query = graph().traversal().V().has("coverageName", "same"); + query.asAdmin().applyStrategies(); + graph().schema().vertexLabel("coverageLater").properties("coverageName") + .useAutomaticId().create(); + graph().addVertex(T.label, "coverageLater", "coverageName", "same"); + this.commitTx(); + Assert.assertThrows(NoIndexException.class, query::toList); + this.nameIndex("coverageLater"); + Assert.assertEquals(3, graph().traversal().V().has("coverageName", "same").toList().size()); + graph().schema().indexLabel("coverageBByName").remove(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().V().has("coverageName", "same").toList()); + } + + @Test + public void testRangeRequiresCompatibleCoverage() { + SchemaManager schema = graph().schema(); + schema.indexLabel("coverageAByScore").onV("coverageA").by("coverageScore").range().create(); + schema.indexLabel("coverageBByScore").onV("coverageB") + .by("coverageScore").secondary().create(); + Set expected = this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().V().has("coverageScore", P.gte(10)).toList()); + // Equality is supported by both RANGE and SECONDARY indexes. + Assert.assertEquals(1, graph().traversal().V().has("coverageScore", 10).toList().size()); + schema.indexLabel("coverageBByScore").remove(); + schema.indexLabel("coverageBRange").onV("coverageB").by("coverageScore").range().create(); + Assert.assertEquals(expected, + graph().traversal().V().has("coverageScore", P.gte(10)).toSet()); + } + + @Test + public void testSearchRequiresCompatibleCoverage() { + SchemaManager schema = graph().schema(); + schema.indexLabel("coverageASearch").onV("coverageA").by("coverageBody").search().create(); + schema.indexLabel("coverageBSecondary").onV("coverageB") + .by("coverageBody").secondary().create(); + this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().V().has("coverageBody", Text.contains("(gold)")).toList()); + schema.indexLabel("coverageBSearch").onV("coverageB").by("coverageBody").search().create(); + schema.indexLabel("unrelatedSearch").onV("unrelated").by("coverageBody").search().create(); + Assert.assertEquals(2, graph().traversal().V() + .has("coverageBody", Text.contains("(gold)")).toList().size()); + } + + @Test + public void testCompositeCoverage() { + SchemaManager schema = graph().schema(); + schema.indexLabel("coverageAComposite").onV("coverageA") + .by("coverageName", "coverageScore").secondary().create(); + this.nameIndex("coverageB"); + this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().V() + .has("coverageName", "same").has("coverageScore", 10).toList()); + schema.indexLabel("coverageBComposite").onV("coverageB") + .by("coverageName", "coverageScore").secondary().create(); + Assert.assertEquals(1, graph().traversal().V().has("coverageName", "same") + .has("coverageScore", 10).toList().size()); + } + + @Test + public void testJointIndexCoverage() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + SchemaManager schema = graph().schema(); + schema.indexLabel("coverageARange").onV("coverageA") + .by("coverageScore").range().create(); + Set vertices = this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().V() + .has("coverageName", "same").has("coverageScore", P.gte(15)).toList()); + schema.indexLabel("coverageBRange").onV("coverageB") + .by("coverageScore").range().create(); + Vertex expected = vertices.stream().filter(v -> v.label().equals("coverageB")) + .findFirst().get(); + Assert.assertEquals(ImmutableSet.of(expected), graph().traversal().V() + .has("coverageName", "same").has("coverageScore", P.gte(15)).toSet()); + } + + @Test + public void testUnlabeledMixedCompositeAndJointIndexes() { + SchemaManager schema = graph().schema(); + schema.indexLabel("coverageAComposite").onV("coverageA") + .by("coverageName", "coverageScore").secondary().create(); + this.nameIndex("coverageB"); + schema.indexLabel("coverageBRange").onV("coverageB") + .by("coverageScore").range().create(); + Set expected = this.addVertices(); + for (Vertex vertex : expected) { + vertex.property("coverageScore", 10); + } + // Each individual index also matches a vertex outside the intersection. + graph().addVertex(T.label, "coverageB", "coverageName", "other", + "coverageScore", 10, "coverageBody", "gold"); + graph().addVertex(T.label, "coverageB", "coverageName", "same", + "coverageScore", 30, "coverageBody", "gold"); + this.commitTx(); + Assert.assertEquals(expected, graph().traversal().V() + .has("coverageName", "same").has("coverageScore", 10).toSet()); + + IndexLabel index = schema.getIndexLabel("coverageBRange"); + try { + this.params().schemaTransaction().updateSchemaStatus(index, SchemaStatus.REBUILDING); + Assert.assertThrows(IllegalArgumentException.class, () -> graph().traversal().V() + .has("coverageName", "same").has("coverageScore", 10).toSet()); + } finally { + this.params().schemaTransaction().updateSchemaStatus(index, SchemaStatus.CREATED); + } + Assert.assertEquals(expected, graph().traversal().V() + .has("coverageName", "same").has("coverageScore", 10).toSet()); + } + + @Test + public void testAllVertexCandidatesDeletingReturnEmpty() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + Set expected = this.addVertices(); + SchemaLabel a = graph().schema().getVertexLabel("coverageA"); + SchemaLabel b = graph().schema().getVertexLabel("coverageB"); + try { + for (SchemaStatus status : new SchemaStatus[]{SchemaStatus.DELETING, + SchemaStatus.UNDELETED}) { + this.params().schemaTransaction().updateSchemaStatus(a, status); + this.params().schemaTransaction().updateSchemaStatus(b, status); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().V() + .has("coverageName", "same").toSet()); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().V() + .hasLabel("coverageA").has("coverageName", "same").toSet()); + this.assertEmptyPage(this.nameQuery(HugeType.VERTEX)); + ConditionQuery query = this.nameQuery(HugeType.VERTEX); + query.showDeleting(true); + Assert.assertEquals(expected, ImmutableSet.copyOf(graph().vertices(query))); + ConditionQuery child = query.copy(); + child.showDeleting(false); + child.setOriginQuery(query); + Assert.assertEquals(expected, ImmutableSet.copyOf(graph().vertices(child))); + } + graph().schema().indexLabel("coverageAByName").remove(); + graph().schema().indexLabel("coverageBByName").remove(); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().V() + .has("coverageName", "same").toSet()); + ConditionQuery query = this.nameQuery(HugeType.VERTEX); + query.showDeleting(true); + Assert.assertThrows(NoIndexException.class, () -> graph().vertices(query).hasNext()); + // A visible, unindexed candidate must still fail next to an invisible one. + this.params().schemaTransaction().updateSchemaStatus(a, SchemaStatus.CREATED); + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().V() + .has("coverageName", "same").toSet()); + } finally { + this.params().schemaTransaction().updateSchemaStatus(a, SchemaStatus.CREATED); + this.params().schemaTransaction().updateSchemaStatus(b, SchemaStatus.CREATED); + } + } + + @Test + public void testOnlyEdgeCandidateDeletingReturnsEmpty() { + SchemaManager schema = graph().schema(); + schema.edgeLabel("deletingEdge").link("coverageA", "coverageB") + .properties("coverageName").create(); + schema.indexLabel("deletingEdgeByName").onE("deletingEdge") + .by("coverageName").secondary().create(); + Set vertices = this.addVertices(); + Vertex a = vertices.stream().filter(v -> v.label().equals("coverageA")).findFirst().get(); + Vertex b = vertices.stream().filter(v -> v.label().equals("coverageB")).findFirst().get(); + Edge edge = a.addEdge("deletingEdge", b, "coverageName", "same"); + this.commitTx(); + SchemaLabel label = schema.getEdgeLabel("deletingEdge"); + try { + this.params().schemaTransaction().updateSchemaStatus(label, SchemaStatus.DELETING); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().E() + .has("coverageName", "same").toSet()); + this.assertEmptyPage(this.nameQuery(HugeType.EDGE)); + ConditionQuery query = this.nameQuery(HugeType.EDGE); + query.showDeleting(true); + Assert.assertEquals(ImmutableSet.of(edge), ImmutableSet.copyOf(graph().edges(query))); + schema.indexLabel("deletingEdgeByName").remove(); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().E() + .has("coverageName", "same").toSet()); + Assert.assertThrows(NoIndexException.class, () -> graph().edges(query).hasNext()); + } finally { + this.params().schemaTransaction().updateSchemaStatus(label, SchemaStatus.CREATED); + } + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().E() + .has("coverageName", "same").toSet()); + } + + @Test + public void testOnlyHiddenCandidateReturnsEmpty() { + SchemaManager schema = graph().schema(); + schema.propertyKey("hiddenName").asText().create(); + VertexLabel hidden = schema.vertexLabel("~coverageHidden") + .properties("hiddenName").useAutomaticId().build(); + this.params().schemaTransaction().addVertexLabel(hidden); + try { + schema.indexLabel("hiddenByName").onV("~coverageHidden") + .by("hiddenName").secondary().create(); + Vertex vertex = this.params().graphTransaction().addVertex( + this.params().graphTransaction().constructVertex( + false, T.label, "~coverageHidden", "hiddenName", "same")); + this.commitTx(); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().V() + .has("hiddenName", "same").toSet()); + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.query(Condition.eq(graph().propertyKey("hiddenName").id(), "same")); + this.assertEmptyPage(query.copy()); + query.showHidden(true); + Assert.assertEquals(ImmutableSet.of(vertex), ImmutableSet.copyOf(graph().vertices(query))); + schema.indexLabel("hiddenByName").remove(); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().V() + .has("hiddenName", "same").toSet()); + Assert.assertThrows(NoIndexException.class, () -> graph().vertices(query).hasNext()); + } finally { + // System schema cannot be deleted individually. Reset the test + // graph so the hidden label cannot retain this test's property key. + CoreTestSuite.clear(); + } + } + + @Test + public void testIndexStatusAndRebuild() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + Set expected = this.addVertices(); + graph().schema().indexLabel("coverageBByName").rebuild(); + Assert.assertEquals(expected, + graph().traversal().V().has("coverageName", "same").toSet()); + IndexLabel index = graph().schema().getIndexLabel("coverageBByName"); + try { + for (SchemaStatus status : new SchemaStatus[]{SchemaStatus.CREATING, + SchemaStatus.REBUILDING, + SchemaStatus.DELETING}) { + this.params().schemaTransaction().updateSchemaStatus(index, status); + Assert.assertThrows(IllegalArgumentException.class, () -> + graph().traversal().V().has("coverageName", "same").toList()); + } + } finally { + this.params().schemaTransaction().updateSchemaStatus(index, SchemaStatus.CREATED); + } + Assert.assertEquals(expected, + graph().traversal().V().has("coverageName", "same").toSet()); + } + + @Test + public void testDeletingVertexLabelUsesQueryVisibility() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + Set expected = this.addVertices(); + Vertex visible = expected.stream().filter(v -> v.label().equals("coverageA")) + .findFirst().get(); + SchemaLabel deleting = graph().schema().getVertexLabel("coverageB"); + try { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.DELETING); + IndexLabel index = graph().schema().getIndexLabel("coverageBByName"); + this.params().schemaTransaction().updateSchemaStatus(index, SchemaStatus.DELETING); + // A hidden label's unavailable index must not be validated either. + Assert.assertEquals(ImmutableSet.of(visible), graph().traversal().V() + .has("coverageName", "same").toSet()); + this.params().schemaTransaction().updateSchemaStatus(index, SchemaStatus.CREATED); + ConditionQuery includeDeleting = this.nameQuery(HugeType.VERTEX); + includeDeleting.showDeleting(true); + Assert.assertEquals(expected, ImmutableSet.copyOf(graph().vertices(includeDeleting))); + graph().schema().indexLabel("coverageBByName").remove(); + + for (SchemaStatus status : new SchemaStatus[]{SchemaStatus.DELETING, + SchemaStatus.UNDELETED}) { + this.params().schemaTransaction().updateSchemaStatus(deleting, status); + Assert.assertEquals(ImmutableSet.of(visible), graph().traversal().V() + .has("coverageName", "same").toSet()); + ConditionQuery query = this.nameQuery(HugeType.VERTEX); + query.showDeleting(true); + Assert.assertThrows(NoIndexException.class, + () -> graph().vertices(query).hasNext()); + // Flattened queries inherit visibility from the outer request. + ConditionQuery child = query.copy(); + child.showDeleting(false); + child.setOriginQuery(query); + Assert.assertThrows(NoIndexException.class, + () -> graph().vertices(child).hasNext()); + } + } finally { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.CREATED); + } + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().V() + .has("coverageName", "same").toSet()); + this.nameIndex("coverageB"); + Assert.assertEquals(expected, + graph().traversal().V().has("coverageName", "same").toSet()); + } + + @Test + public void testDeletingEdgeLabelUsesQueryVisibility() { + SchemaManager schema = graph().schema(); + for (String label : new String[]{"visibleEdge", "deletingEdge"}) { + schema.edgeLabel(label).link("coverageA", "coverageB") + .properties("coverageName").create(); + schema.indexLabel(label + "ByName").onE(label) + .by("coverageName").secondary().create(); + } + Set vertices = this.addVertices(); + Vertex a = vertices.stream().filter(v -> v.label().equals("coverageA")).findFirst().get(); + Vertex b = vertices.stream().filter(v -> v.label().equals("coverageB")).findFirst().get(); + Edge visible = a.addEdge("visibleEdge", b, "coverageName", "same"); + Edge removed = a.addEdge("deletingEdge", b, "coverageName", "same"); + this.commitTx(); + SchemaLabel deleting = schema.getEdgeLabel("deletingEdge"); + try { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.DELETING); + ConditionQuery includeDeleting = this.nameQuery(HugeType.EDGE); + includeDeleting.showDeleting(true); + Assert.assertEquals(ImmutableSet.of(visible, removed), + ImmutableSet.copyOf(graph().edges(includeDeleting))); + schema.indexLabel("deletingEdgeByName").remove(); + for (SchemaStatus status : new SchemaStatus[]{SchemaStatus.DELETING, + SchemaStatus.UNDELETED}) { + this.params().schemaTransaction().updateSchemaStatus(deleting, status); + Assert.assertEquals(ImmutableSet.of(visible), graph().traversal().E() + .has("coverageName", "same").toSet()); + ConditionQuery query = this.nameQuery(HugeType.EDGE); + query.showDeleting(true); + Assert.assertThrows(NoIndexException.class, () -> graph().edges(query).hasNext()); + } + } finally { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.CREATED); + } + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().E() + .has("coverageName", "same").toSet()); + schema.indexLabel("deletingEdgeByName").onE("deletingEdge") + .by("coverageName").secondary().create(); + Assert.assertEquals(ImmutableSet.of(visible, removed), graph().traversal().E() + .has("coverageName", "same").toSet()); + } + + @Test + public void testPagingWhileVertexLabelIsDeleting() { + Assume.assumeTrue(storeFeatures().supportsQueryByPage()); + this.nameIndex("coverageA"); + Set vertices = this.addVertices(); + Vertex a = vertices.stream().filter(v -> v.label().equals("coverageA")).findFirst().get(); + Vertex another = graph().addVertex(T.label, "coverageA", "coverageName", "same", + "coverageScore", 30, "coverageBody", "gold"); + this.commitTx(); + SchemaLabel deleting = graph().schema().getVertexLabel("coverageB"); + try { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.DELETING); + Set actual = new HashSet<>(); + String page = ""; + int requests = 0; + do { + Iterator iterator = graph().vertices(this.pageQuery(page)); + try { + while (iterator.hasNext()) { + Assert.assertTrue(actual.add(iterator.next())); + } + page = PageInfo.pageInfo(iterator); + } finally { + CloseableIterator.closeIterator(iterator); + } + this.params().schemaTransaction().updateSchemaStatus(deleting, + SchemaStatus.UNDELETED); + Assert.assertTrue(++requests < 10); + } while (page != null && !page.isEmpty()); + Assert.assertEquals(ImmutableSet.of(a, another), actual); + Assert.assertTrue(requests > 1); + } finally { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.CREATED); + } + Assert.assertThrows(NoIndexException.class, () -> this.page("")); + } + + private ConditionQuery nameQuery(HugeType type) { + ConditionQuery query = new ConditionQuery(type); + query.query(Condition.eq(graph().propertyKey("coverageName").id(), "same")); + return query; + } + + @Test + public void testGlobalEdgeCoverage() { + SchemaManager schema = graph().schema(); + schema.edgeLabel("edgeA").link("coverageA", "coverageB") + .properties("coverageName").create(); + schema.edgeLabel("edgeB").link("coverageA", "coverageB") + .properties("coverageName").enableLabelIndex(false).create(); + schema.indexLabel("edgeAByName").onE("edgeA").by("coverageName").secondary().create(); + this.addVertices(); + Iterator vertices = graph().traversal().V().toList().iterator(); + Vertex a = vertices.next(); + Vertex b = vertices.next(); + if (a.label().equals("coverageB")) { + Vertex swap = a; + a = b; + b = swap; + } + a.addEdge("edgeA", b, "coverageName", "same"); + a.addEdge("edgeB", b, "coverageName", "same"); + this.commitTx(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().E().has("coverageName", "same").toList()); + schema.indexLabel("edgeBByName").onE("edgeB").by("coverageName").secondary().create(); + Assert.assertEquals(2, + graph().traversal().E().has("coverageName", "same").toList().size()); + } + + @Test + public void testParentEdgeIndexCoversSubLabels() { + Assume.assumeTrue(storeFeatures().supportsFatherAndSubEdgeLabel()); + SchemaManager schema = graph().schema(); + schema.edgeLabel("parentEdge").asBase().properties("coverageName", "coverageScore") + .multiTimes().sortKeys("coverageScore").create(); + for (String label : new String[]{"subA", "subB"}) { + schema.edgeLabel(label).withBase("parentEdge").link("coverageA", "coverageB") + .properties("coverageName", "coverageScore") + .multiTimes().sortKeys("coverageScore").create(); + } + schema.indexLabel("parentByName").onE("parentEdge") + .by("coverageName").secondary().create(); + Set vertices = this.addVertices(); + Vertex a = vertices.stream().filter(v -> v.label().equals("coverageA")).findFirst().get(); + Vertex b = vertices.stream().filter(v -> v.label().equals("coverageB")).findFirst().get(); + a.addEdge("subA", b, "coverageName", "same", "coverageScore", 1); + a.addEdge("subB", b, "coverageName", "same", "coverageScore", 2); + this.commitTx(); + Assert.assertEquals(2, + graph().traversal().E().has("coverageName", "same").toList().size()); + schema.indexLabel("parentByName").remove(); + schema.indexLabel("subAByName").onE("subA").by("coverageName").secondary().create(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().E().has("coverageName", "same").toList()); + schema.indexLabel("subBByName").onE("subB").by("coverageName").secondary().create(); + Assert.assertEquals(2, + graph().traversal().E().has("coverageName", "same").toList().size()); + + SchemaLabel deleting = schema.getEdgeLabel("subB"); + try { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.DELETING); + schema.indexLabel("subBByName").remove(); + Assert.assertEquals(ImmutableList.of("subA"), graph().traversal().E() + .has("coverageName", "same").label().toList()); + ConditionQuery query = this.nameQuery(HugeType.EDGE); + query.showDeleting(true); + Assert.assertThrows(NoIndexException.class, () -> graph().edges(query).hasNext()); + } finally { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.CREATED); + } + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().E() + .has("coverageName", "same").toList()); + schema.indexLabel("subBByName").onE("subB").by("coverageName").secondary().create(); + Assert.assertEquals(2, + graph().traversal().E().has("coverageName", "same").toList().size()); + + SchemaLabel subA = schema.getEdgeLabel("subA"); + try { + this.params().schemaTransaction().updateSchemaStatus(subA, SchemaStatus.DELETING); + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.DELETING); + schema.indexLabel("subAByName").remove(); + schema.indexLabel("subBByName").remove(); + // The visible base label has no direct records of its own. + Assert.assertEquals(ImmutableSet.of(), graph().traversal().E() + .has("coverageName", "same").toSet()); + } finally { + this.params().schemaTransaction().updateSchemaStatus(subA, SchemaStatus.CREATED); + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.CREATED); + } + } + + @Test + public void testPagingRequiresCoverageOnEveryRequest() { + Assume.assumeTrue(storeFeatures().supportsQueryByPage()); + this.nameIndex("coverageA"); + Set expected = this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> this.page("")); + this.nameIndex("coverageB"); + Set actual = new HashSet<>(); + String page = ""; + String resume = null; + int pages = 0; + do { + ConditionQuery query = this.pageQuery(page); + Iterator iterator = graph().vertices(query); + try { + iterator.forEachRemaining(actual::add); + page = PageInfo.pageInfo(iterator); + if (resume == null) { + resume = page; + } + } finally { + CloseableIterator.closeIterator(iterator); + } + Assert.assertTrue(++pages < 10); + } while (page != null && !page.isEmpty()); + Assert.assertEquals(expected, actual); + Assert.assertNotNull(resume); + String continuation = resume; + graph().schema().indexLabel("coverageBByName").remove(); + Assert.assertThrows(NoIndexException.class, () -> this.page(continuation)); + } + + private void assertEmptyPage(ConditionQuery query) { + if (!storeFeatures().supportsQueryByPage()) { + return; + } + query.limit(1); + query.page(""); + Iterator iterator = query.resultType().isVertex() ? graph().vertices(query) : + graph().edges(query); + try { + Assert.assertFalse(iterator.hasNext()); + Assert.assertNull(PageInfo.pageInfo(iterator)); + } finally { + CloseableIterator.closeIterator(iterator); + } + } + + private ConditionQuery pageQuery(String page) { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.query(Condition.eq(graph().propertyKey("coverageName").id(), "same")); + query.limit(1); + query.page(page); + return query; + } + + private void page(String page) { + Iterator iterator = graph().vertices(this.pageQuery(page)); + try { + while (iterator.hasNext()) { + iterator.next(); + } + } finally { + CloseableIterator.closeIterator(iterator); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/TaskCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/TaskCoreTest.java index 9e82692689..3a3837a2e1 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/TaskCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/TaskCoreTest.java @@ -28,11 +28,11 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.api.job.GremlinAPI.GremlinRequest; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.job.EphemeralJob; import org.apache.hugegraph.job.EphemeralJobBuilder; @@ -234,7 +234,8 @@ public void testDeleteIncompleteTask() { TaskScheduler scheduler = graph.taskScheduler(); Id id = IdGenerator.of(88891); - HugeTask task = new HugeTask<>(id, null, new SleepCallable<>()); + BlockingCallable.reset(); + HugeTask task = new HugeTask<>(id, null, new BlockingCallable<>()); task.type("test"); task.name("delete-incomplete-task"); scheduler.schedule(task); @@ -248,13 +249,14 @@ public void testDeleteIncompleteTask() { e.getMessage()); }); } else { - waitUntilTaskRunning(scheduler); + Assert.assertTrue(BlockingCallable.awaitStarted()); HugeTask deleted = scheduler.delete(id, false); Assert.assertNotNull(deleted); Assert.assertEquals(TaskStatus.DELETING, deleted.status()); Assert.assertEquals(TaskStatus.DELETING, scheduler.task(id).status()); } } finally { + BlockingCallable.release(); try { scheduler.waitUntilAllTasksCompleted(10); } catch (TimeoutException ignored) { @@ -604,6 +606,9 @@ public Object execute() throws Exception { Assert.assertEquals(TaskStatus.SUCCESS, task.status()); Assert.assertEquals("{\"k1\":13579,\"k2\":\"24680\"}", task.result()); + // Future.get() publishes the result before done() and native owner cleanup finish. + scheduler.waitUntilAllTasksCompleted(10); + // Ephemeral tasks are not stored in DB, so these should throw NotFoundException Assert.assertThrows(NotFoundException.class, () -> { scheduler.waitUntilTaskCompleted(task.id(), 10); @@ -1023,24 +1028,34 @@ public void testGremlinJobAndRestore() throws Exception { HugeGraph graph = graph(); TaskScheduler scheduler = graph.taskScheduler(); - String gremlin = "println('task start');" + + BlockingCallable.reset(); + String gremlin = "import org.apache.hugegraph.core.TaskCoreTest.BlockingCallable;" + + "println('task start');" + "for(int i=gremlinJob.progress(); i<=10; i++) {" + " gremlinJob.updateProgress(i);" + + " if (i == 1) BlockingCallable.pause();" + " Thread.sleep(200); " + " println('sleep=>'+i);" + "}; 100;"; HugeTask task = runGremlinJob(gremlin); - sleepAWhile(200 * 6); - task = scheduler.task(task.id()); - scheduler.cancel(task); - - task = scheduler.task(task.id()); - Assert.assertTrue("Task status should be CANCELLING or CANCELLED, but was " + task.status(), - task.status() == TaskStatus.CANCELLING || - task.status() == TaskStatus.CANCELLED); - - task = scheduler.waitUntilTaskCompleted(task.id(), 10); + try { + // RUNNING is set before script evaluation; wait for real progress. + Assert.assertTrue("Gremlin task did not reach progress 1", + BlockingCallable.awaitStarted()); + task = scheduler.task(task.id()); + scheduler.cancel(task); + + task = scheduler.task(task.id()); + Assert.assertTrue("Task status should be CANCELLING or CANCELLED, but was " + task.status(), + task.status() == TaskStatus.CANCELLING || + task.status() == TaskStatus.CANCELLED); + + task = scheduler.waitUntilTaskCompleted(task.id(), 10); + } finally { + // Restored execution can pass the progress barrier and finish. + BlockingCallable.release(); + } Assert.assertEquals(TaskStatus.CANCELLED, task.status()); Assert.assertTrue("progress=" + task.progress(), 0 < task.progress() && task.progress() < 10); @@ -1145,6 +1160,13 @@ public static void release() { release.countDown(); } + public static void pause() throws InterruptedException, TimeoutException { + started.countDown(); + if (!release.await(30L, TimeUnit.SECONDS)) { + throw new TimeoutException("Timed out waiting for task cancellation"); + } + } + @Override public V call() throws Exception { started.countDown(); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/TinkerPop37StepsCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/TinkerPop37StepsCoreTest.java new file mode 100644 index 0000000000..312dad762b --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/TinkerPop37StepsCoreTest.java @@ -0,0 +1,606 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.core; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +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 org.apache.hugegraph.schema.SchemaManager; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.traversal.optimize.HugeCountStepStrategy; +import org.apache.hugegraph.traversal.optimize.HugeCountStrategy; +import org.apache.hugegraph.traversal.optimize.HugeGraphStepStrategy; +import org.apache.hugegraph.traversal.optimize.HugeVertexStepStrategy; +import org.apache.tinkerpop.gremlin.process.traversal.DT; +import org.apache.tinkerpop.gremlin.process.traversal.GType; +import org.apache.tinkerpop.gremlin.process.traversal.NotP; +import org.apache.tinkerpop.gremlin.process.traversal.Merge; +import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.TextP; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AddPropertyStepContract; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.FailStep; +import org.apache.tinkerpop.gremlin.structure.Direction; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality; +import org.junit.Test; + +public class TinkerPop37StepsCoreTest extends BaseCoreTest { + + @Test + public void testStringManipulationSteps() { + Assert.assertEquals("123", graph().traversal().inject(123) + .asString().next()); + Assert.assertEquals(5, graph().traversal().inject("marko") + .length().next()); + Assert.assertEquals("marko", graph().traversal().inject("MARKO") + .toLower().next()); + Assert.assertEquals("MARKO", graph().traversal().inject("marko") + .toUpper().next()); + Assert.assertEquals("marko", graph().traversal().inject(" marko ") + .trim().next()); + Assert.assertEquals("marko ", graph().traversal().inject(" marko ") + .lTrim().next()); + Assert.assertEquals(" marko", graph().traversal().inject(" marko ") + .rTrim().next()); + Assert.assertEquals("huge-graph", graph().traversal().inject("huge_graph") + .replace("_", "-").next()); + Assert.assertEquals("hugegraph", graph().traversal().inject("huge") + .concat("graph").next()); + Assert.assertEquals("eguh", graph().traversal().inject("huge") + .reverse().next()); + Assert.assertEquals(Arrays.asList("huge", "graph"), + graph().traversal().inject("huge-graph") + .split("-").next()); + Assert.assertEquals("graph", graph().traversal().inject("hugegraph") + .substring(4).next()); + Assert.assertEquals("huge", graph().traversal().inject("hugegraph") + .substring(0, 4).next()); + + Map values = new HashMap<>(); + values.put("name", "marko"); + values.put("age", 29); + Assert.assertEquals("marko is 29 years old", + graph().traversal().inject(values) + .format("%{name} is %{age} years old") + .next()); + } + + @Test + public void testListManipulationSteps() { + List values = Arrays.asList(1, 2); + List other = Arrays.asList(2, 3); + + Assert.assertEquals(Arrays.asList(1, 2, 2, 3), + graph().traversal().inject(values) + .combine(other).next()); + Assert.assertEquals(setOf(1, 2, 3), + asSet(graph().traversal().inject(values) + .merge(other).next())); + Assert.assertEquals(setOf(2), + asSet(graph().traversal().inject(values) + .intersect(other).next())); + Assert.assertEquals(setOf(1), + asSet(graph().traversal().inject(values) + .difference(other).next())); + Assert.assertEquals(setOf(1, 3), + asSet(graph().traversal().inject(values) + .disjunct(other).next())); + Assert.assertEquals(Arrays.asList(Arrays.asList(1, 2), + Arrays.asList(1, 3), + Arrays.asList(2, 2), + Arrays.asList(2, 3)), + graph().traversal().inject(values) + .product(other).next()); + Assert.assertEquals(Arrays.asList(3, 2, 1), + graph().traversal().inject(Arrays.asList(1, 2, 3)) + .reverse().next()); + Assert.assertEquals("huge-graph", + graph().traversal() + .inject(Arrays.asList("huge", "graph")) + .conjoin("-").next()); + Assert.assertEquals(Arrays.asList(1, 2, 3), + graph().traversal() + .inject(Arrays.asList(1, 2, 3)) + .all(P.gt(0)).next()); + Assert.assertEquals(Arrays.asList(1, 2, 3), + graph().traversal() + .inject(Arrays.asList(1, 2, 3)) + .any(P.eq(2)).next()); + } + + @Test + public void testOffsetDateTimeQueryOnExistingDateSchema() { + SchemaManager schema = graph().schema(); + schema.propertyKey("birthday").asDate().create(); + schema.vertexLabel("dated").properties("birthday").create(); + schema.indexLabel("datedByBirthday").onV("dated") + .by("birthday").range().create(); + OffsetDateTime time = OffsetDateTime.parse("2023-08-02T08:00:00.123456789+08:00"); + Date date = Date.from(time.toInstant()); + Vertex first = graph().addVertex(T.label, "dated", "birthday", date); + Vertex later = graph().addVertex(T.label, "dated", "birthday", + Date.from(time.plusDays(1).toInstant())); + commitTx(); + Assert.assertEquals(Collections.singletonList(first.id()), + graph().traversal().V().hasLabel("dated") + .has("birthday", date).id().toList()); + Assert.assertEquals(Collections.singletonList(later.id()), + graph().traversal().V().hasLabel("dated") + .has("birthday", P.gt(date)).id().toList()); + Assert.assertEquals(Collections.singletonList(first.id()), + graph().traversal().V().hasLabel("dated") + .has("birthday", time).id().toList()); + Assert.assertEquals(Collections.singletonList(later.id()), + graph().traversal().V().hasLabel("dated") + .has("birthday", P.gt(time)).id().toList()); + } + + @Test + public void testOffsetDateTimeWithDateSchemaAndRangeIndex() { + SchemaManager schema = graph().schema(); + schema.propertyKey("birthday").asDate().create(); + schema.vertexLabel("dated").properties("birthday").create(); + schema.indexLabel("datedByBirthday").onV("dated") + .by("birthday").range().create(); + + OffsetDateTime time = graph().traversal() + .inject("2023-08-02T08:00:00.123456789+08:00") + .asDate().next(); + Date expected = Date.from(time.toInstant()); + Vertex legacy = graph().addVertex(T.label, "dated", "birthday", expected); + commitTx(); + Assert.assertEquals(expected, graph().traversal().V(legacy.id()) + .values("birthday").next()); + Assert.assertEquals(Collections.singletonList(legacy.id()), + graph().traversal().V().hasLabel("dated") + .has("birthday", expected).id().toList()); + + Vertex modern = graph().addVertex(T.label, "dated", "birthday", time); + Vertex later = graph().traversal().addV("dated") + .property("birthday", time.plusDays(1)).next(); + commitTx(); + Assert.assertEquals(expected, graph().traversal().V(modern.id()) + .values("birthday").next()); + Assert.assertEquals(Date.from(time.plusDays(1).toInstant()), + graph().traversal().V(later.id()).values("birthday").next()); + Assert.assertEquals(setOf(legacy.id(), modern.id()), + asSet(graph().traversal().V().hasLabel("dated") + .has("birthday", time).id().toList())); + Assert.assertEquals(setOf(legacy.id(), modern.id()), + asSet(graph().traversal().V().hasLabel("dated") + .has("birthday", time.withOffsetSameInstant( + ZoneOffset.UTC)).id().toList())); + Assert.assertEquals(Collections.singletonList(later.id()), + graph().traversal().V().hasLabel("dated") + .has("birthday", P.gt(time)).id().toList()); + Assert.assertEquals(setOf(legacy.id(), modern.id()), + asSet(graph().traversal().V().hasLabel("dated") + .has("birthday", P.gte(time).and( + P.lt(time.plusDays(1)))).id().toList())); + Assert.assertEquals(Collections.singletonList(later.id()), + graph().traversal().V().hasLabel("dated") + .has("birthday", P.gt(expected)).id().toList()); + } + + @Test + public void testDateManipulationSteps() { + OffsetDateTime start = OffsetDateTime.parse("2023-08-02T00:00:00Z"); + OffsetDateTime expected = OffsetDateTime.parse("2023-08-09T00:00:00Z"); + + OffsetDateTime actual = graph().traversal() + .inject("2023-08-02T00:00:00Z") + .asDate().dateAdd(DT.day, 7).next(); + long milliseconds = graph().traversal() + .inject("2023-08-02T00:00:00Z") + .asDate().dateAdd(DT.day, 7) + .dateDiff(start).next(); + + Assert.assertEquals(expected, actual); + Assert.assertEquals(604800000L, milliseconds); + } + + @Test + public void testAddVertexKeepsPropertiesFoldedByPrimaryKeyStrategy() { + this.initMutationSchema(); + + GraphTraversal traversal = graph().traversal() + .addV("person") + .property( + Cardinality.single, + "name", + __.constant("marko")) + .property( + Cardinality.single, + "status", "active"); + Assert.assertTrue(traversal.asAdmin().getSteps().stream().anyMatch( + step -> step instanceof AddPropertyStepContract)); + + Vertex vertex = traversal.next(); + commitTx(); + + Vertex stored = graph().traversal().V(vertex.id()).next(); + Assert.assertEquals("marko", stored.value("name")); + Assert.assertEquals("active", stored.value("status")); + } + + @Test + public void testMergeVertexWithHugeGraphIds() { + this.initMutationSchema(); + Map search = map(T.label, "person", + "name", "marko"); + + Vertex created = graph().traversal().mergeV(search) + .option(Merge.onCreate, + map("status", "created")) + .next(); + commitTx(); + Vertex matched = graph().traversal().mergeV(search) + .option(Merge.onMatch, + map("status", "matched")) + .next(); + commitTx(); + + Assert.assertEquals(created.id(), matched.id()); + Assert.assertEquals("matched", matched.value("status")); + Assert.assertEquals(1L, graph().traversal().V() + .hasLabel("person") + .has("name", "marko") + .count().next()); + } + + @Test + public void testMergeEdgeWithHugeGraphIds() { + this.initMutationSchema(); + Vertex marko = graph().addVertex(T.label, "person", + "name", "marko"); + Vertex vadas = graph().addVertex(T.label, "person", + "name", "vadas"); + commitTx(); + Map search = map(T.label, "knows", + Direction.OUT, marko.id(), + Direction.IN, vadas.id()); + + Edge created = graph().traversal().mergeE(search) + .option(Merge.onCreate, + map("status", "created", + "weight", 0.5D)) + .next(); + commitTx(); + Edge matched = graph().traversal().mergeE(search) + .option(Merge.onMatch, + map("status", "matched")) + .next(); + commitTx(); + + Assert.assertEquals(created.id(), matched.id()); + Assert.assertEquals("matched", matched.value("status")); + Assert.assertEquals(1L, graph().traversal().E() + .hasLabel("knows").count().next()); + } + + @Test + public void testMergeOnCreateValidation() { + this.initMutationSchema(); + Map search = map(T.label, "person", + "name", "marko"); + Map invalid = map(T.label, "person", + "name", "vadas"); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + graph().traversal().mergeV(search) + .option(Merge.onCreate, invalid); + }); + } + + @Test + public void testPropertyMapAndMidTraversalE() { + this.initMutationSchema(); + Vertex marko = graph().addVertex(T.label, "person", + "name", "marko"); + Vertex vadas = graph().addVertex(T.label, "person", + "name", "vadas"); + Edge edge = marko.addEdge("knows", vadas); + commitTx(); + + graph().traversal().V(marko.id()) + .property(map("status", "active")) + .iterate(); + commitTx(); + + Assert.assertEquals("active", graph().traversal().V(marko.id()) + .values("status").next()); + Assert.assertEquals(edge.id(), graph().traversal().inject(1) + .E(edge.id()).next().id()); + } + + @Test + public void testUnproductiveByFiltersMissingGroupKey() { + this.initTextData(); + graph().traversal().V() + .hasLabel("person") + .has("name", "marko") + .property("status", "active") + .iterate(); + commitTx(); + + Map grouped = graph().traversal().V() + .group() + .by("status") + .by("name") + .next(); + + Assert.assertEquals(1, grouped.size()); + Assert.assertEquals(Collections.singletonList("marko"), + grouped.get("active")); + Assert.assertFalse(grouped.containsKey(null)); + } + + @Test + public void testUnproductiveByOmitsProjectKey() { + this.initTextData(); + + Map projected = graph().traversal().V() + .hasLabel("person") + .has("name", "vadas") + .project("name", "status") + .by("name") + .by("status") + .next(); + + Assert.assertEquals("vadas", projected.get("name")); + Assert.assertFalse(projected.containsKey("status")); + } + + @Test + public void testMissingByValueCanUseExplicitFallback() { + this.initTextData(); + graph().traversal().V() + .hasLabel("person") + .has("name", "marko") + .property("status", "active") + .iterate(); + commitTx(); + + Map grouped = graph().traversal().V() + .group() + .by(__.coalesce( + __.values("status"), + __.constant("missing"))) + .by("name") + .next(); + + Assert.assertEquals(Collections.singletonList("marko"), + grouped.get("active")); + Assert.assertEquals(setOf("lop", "vadas"), + asSet(grouped.get("missing"))); + } + + @Test + public void testFailStep() { + Assert.assertThrows(FailStep.FailException.class, () -> { + graph().traversal().inject(1).fail("expected failure").iterate(); + }); + } + + @Test + public void testTextPContaining() { + this.initTextData(); + Assert.assertEquals(Arrays.asList("marko"), + this.names(TextP.containing("ark"))); + } + + @Test + public void testTextPStartingWith() { + this.initTextData(); + Assert.assertEquals(Arrays.asList("marko"), + this.names(TextP.startingWith("mar"))); + } + + @Test + public void testTextPEndingWith() { + this.initTextData(); + Assert.assertEquals(Arrays.asList("vadas"), + this.names(TextP.endingWith("das"))); + } + + @Test + public void testTextPRegex() { + this.initTextData(); + Assert.assertEquals(Arrays.asList("marko"), + this.names(TextP.regex("^mar"))); + } + + @Test + public void testTextPNegations() { + this.initTextData(); + Assert.assertEquals(Arrays.asList("lop", "vadas"), + this.names(TextP.notContaining("ar"))); + Assert.assertEquals(Arrays.asList("lop", "vadas"), + this.names(TextP.notStartingWith("mar"))); + Assert.assertEquals(Arrays.asList("lop", "marko"), + this.names(TextP.notEndingWith("das"))); + Assert.assertEquals(Arrays.asList("lop", "vadas"), + this.names(TextP.notRegex("^mar"))); + } + + @Test + public void testTextPWithLocalFilter() { + this.initTextData(); + + Assert.assertEquals(Arrays.asList("marko"), + this.namesWithLocalFilter( + TextP.containing("ark"))); + Assert.assertEquals(Arrays.asList("marko"), + this.namesWithLocalFilter( + TextP.startingWith("mar"))); + Assert.assertEquals(Arrays.asList("vadas"), + this.namesWithLocalFilter( + TextP.endingWith("das"))); + Assert.assertEquals(Arrays.asList("marko"), + this.namesWithLocalFilter(TextP.regex("^mar"))); + Assert.assertEquals(Arrays.asList("lop", "vadas"), + this.namesWithLocalFilter( + TextP.notContaining("ar"))); + Assert.assertEquals(Arrays.asList("lop", "vadas"), + this.namesWithLocalFilter( + TextP.notStartingWith("mar"))); + Assert.assertEquals(Arrays.asList("lop", "marko"), + this.namesWithLocalFilter( + TextP.notEndingWith("das"))); + Assert.assertEquals(Arrays.asList("lop", "vadas"), + this.namesWithLocalFilter( + TextP.notRegex("^mar"))); + } + + @Test + public void testTypeOfPredicatesStayLocal() { + SchemaManager schema = graph().schema(); + schema.propertyKey("name").asText().create(); + schema.propertyKey("age").asInt().create(); + schema.vertexLabel("person").properties("name", "age") + .primaryKeys("name").nullableKeys("age").create(); + graph().addVertex(T.label, "person", "name", "marko", "age", 29); + graph().addVertex(T.label, "person", "name", "vadas", "age", 27); + graph().addVertex(T.label, "person", "name", "lop"); + commitTx(); + + this.assertTypeFilter(T.label, P.typeOf(GType.STRING), + "lop", "marko", "vadas"); + this.assertTypeFilter(T.label, P.typeOf(GType.INT)); + this.assertTypeFilter("name", P.typeOf(GType.STRING), + "lop", "marko", "vadas"); + this.assertTypeFilter("name", P.typeOf(GType.INT)); + this.assertTypeFilter("age", P.typeOf(GType.INT), "marko", "vadas"); + this.assertTypeFilter("age", P.typeOf(GType.STRING)); + this.assertTypeFilter("age", P.typeOf(Integer.class), "marko", "vadas"); + this.assertTypeFilter("age", P.typeOf("Integer"), "marko", "vadas"); + this.assertTypeFilter("age", P.not(P.typeOf(GType.INT))); + this.assertTypeFilter("age", P.not(P.typeOf(GType.STRING)), "marko", "vadas"); + this.assertTypeFilter("age", P.typeOf(GType.INT).and(P.gt(28)), "marko"); + this.assertTypeFilter("age", P.typeOf(GType.STRING).or(P.eq(27)), "vadas"); + this.assertTypeFilter("age", P.not(P.typeOf(GType.STRING)).and(P.gt(28)), + "marko"); + this.assertTypeFilter("age", new NotP<>(P.typeOf(GType.INT).and(P.gt(28))), + "vadas"); + } + + private void assertTypeFilter(Object key, P predicate, String... expected) { + for (boolean optimized : new boolean[]{true, false}) { + GraphTraversalSource source = graph().traversal(); + if (!optimized) { + source = source.withoutStrategies(HugeGraphStepStrategy.class, + HugeVertexStepStrategy.class, + HugeCountStepStrategy.class, + HugeCountStrategy.class); + } + GraphTraversal traversal = source.V(); + if (key instanceof T) { + traversal = traversal.has((T) key, predicate.clone()); + } else { + traversal = traversal.has((String) key, predicate.clone()); + } + Assert.assertEquals(Arrays.asList(expected), + traversal.values("name").order().toList()); + GraphTraversal counted = source.V(); + if (key instanceof T) { + counted = counted.has((T) key, predicate.clone()); + } else { + counted = counted.has((String) key, predicate.clone()); + } + Assert.assertEquals((long) expected.length, counted.count().next().longValue()); + } + } + + private void initMutationSchema() { + SchemaManager schema = graph().schema(); + schema.propertyKey("name").asText().create(); + schema.propertyKey("status").asText().create(); + schema.propertyKey("weight").asDouble().create(); + schema.vertexLabel("person") + .properties("name", "status") + .primaryKeys("name") + .nullableKeys("status") + .create(); + schema.edgeLabel("knows") + .link("person", "person") + .properties("status", "weight") + .nullableKeys("status", "weight") + .create(); + } + + private void initTextData() { + this.initMutationSchema(); + graph().addVertex(T.label, "person", "name", "marko"); + graph().addVertex(T.label, "person", "name", "vadas"); + graph().addVertex(T.label, "person", "name", "lop"); + commitTx(); + } + + private List names(TextP predicate) { + return graph().traversal().V() + .hasLabel("person") + .has("name", predicate) + .values("name") + .order() + .toList(); + } + + private List namesWithLocalFilter(TextP predicate) { + return graph().traversal().V() + .hasLabel("person") + .filter(__.values("name").is(predicate)) + .values("name") + .order() + .toList(); + } + + private static Map map(Object... keyValues) { + Map result = new HashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + result.put(keyValues[i], keyValues[i + 1]); + } + return result; + } + + private static Set setOf(Object... values) { + return new HashSet<>(Arrays.asList(values)); + } + + private static Set asSet(Object values) { + Assert.assertInstanceOf(Iterable.class, values); + List list = new ArrayList<>(); + for (Object value : (Iterable) values) { + list.add(value); + } + return new HashSet<>(list); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java index 335130ede5..63cbc5c844 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java @@ -20,6 +20,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; @@ -31,38 +32,42 @@ import java.util.Random; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; -import org.apache.hugegraph.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.backend.cache.CachedGraphTransaction; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.Id.IdType; -import org.apache.hugegraph.backend.id.IdGenerator; import org.apache.hugegraph.backend.id.SnowflakeIdGenerator; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; +import org.apache.hugegraph.backend.page.IdHolderList; import org.apache.hugegraph.backend.page.PageInfo; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.Query; -import org.apache.hugegraph.backend.serializer.BytesBuffer; import org.apache.hugegraph.backend.store.BackendTable; -import org.apache.hugegraph.backend.store.Shard; import org.apache.hugegraph.backend.tx.GraphTransaction; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.exception.LimitExceedException; import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.exception.NotAllowException; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.id.Id.IdType; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.Userdata; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeElement; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.FakeObjects; import org.apache.hugegraph.testutil.Utils; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.traversal.optimize.ConditionP; +import org.apache.hugegraph.traversal.optimize.HugeGraphStep; import org.apache.hugegraph.traversal.optimize.Text; import org.apache.hugegraph.traversal.optimize.TraversalUtil; import org.apache.hugegraph.type.HugeType; @@ -72,11 +77,16 @@ import org.apache.hugegraph.util.Blob; import org.apache.hugegraph.util.CollectionUtil; import org.apache.hugegraph.util.DateUtil; +import org.apache.hugegraph.util.Events; import org.apache.hugegraph.util.LongEncoding; import org.apache.tinkerpop.gremlin.process.traversal.P; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer; +import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator; @@ -87,6 +97,7 @@ import org.junit.Test; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; public class VertexCoreTest extends BaseCoreTest { @@ -198,6 +209,23 @@ protected void initComputerIndex() { .create(); } + @Test + public void testDeleteVertexWithMissingSchemaLabel() throws InterruptedException, ExecutionException { + HugeGraph graph = graph(); + Vertex vertex = graph.addVertex(T.label, "author", "id", 1, "name", "James"); + graph.tx().commit(); + + // Simulate schema loss while the committed backend record remains. + params().schemaTransaction().removeSchema(graph.vertexLabel("author")); + params().graphEventHub().notify(Events.CACHE, "clear", null).get(); + Vertex orphan = graph.vertices(vertex.id()).next(); + Assert.assertEquals("~undefined", orphan.label()); + + orphan.remove(); + graph.tx().commit(); + Assert.assertFalse(graph.vertices(vertex.id()).hasNext()); + } + @Test public void testAddVertex() { HugeGraph graph = graph(); @@ -1281,11 +1309,14 @@ public void testAddVertexWithTtl() { @Test public void testAddVertexWithTtlAndTtlStartTime() { + // The input date must not reuse a previously closed transaction's timestamp. + long birth = DateUtil.now().getTime() - 1000L; Vertex vertex = graph().addVertex( T.label, "follower", "name", "Baby", "age", 3, "city", "Beijing", - "birth", graph().now() - 1000L); + "birth", birth); this.commitTx(); + Assert.assertEquals(birth + 3000L, ((HugeElement) vertex).expiredTime()); Iterator vertices = graph().vertices(vertex); Assert.assertTrue(vertices.hasNext()); @@ -1320,7 +1351,7 @@ public void testAddVertexWithSecondaryIndexAndTtl() { "age", 3, "city", "Beijing"); this.commitTx(); - Iterator vertices = graph().traversal().V() + Iterator vertices = graph().traversal().V().hasLabel("fan") .has("city", "Beijing"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex, vertices.next()); @@ -1332,7 +1363,7 @@ public void testAddVertexWithSecondaryIndexAndTtl() { // Ignore } - vertices = graph().traversal().V().has("city", "Beijing"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing"); Assert.assertFalse(vertices.hasNext()); } @@ -1355,23 +1386,23 @@ public void testAddVertexWithRangeIndexAndTtl() { "age", 7, "city", "Beijing"); this.commitTx(); - Iterator vertices = graph().traversal().V().has("age", 5); + Iterator vertices = graph().traversal().V().hasLabel("fan").has("age", 5); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex3, vertices.next()); - vertices = graph().traversal().V().has("age", P.gt(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex5, vertices.next()); - vertices = graph().traversal().V().has("age", P.gte(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.lt(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex1, vertices.next()); - vertices = graph().traversal().V().has("age", P.lte(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); this.commitTx(); @@ -1382,19 +1413,19 @@ public void testAddVertexWithRangeIndexAndTtl() { // Ignore } - vertices = graph().traversal().V().has("age", 5); + vertices = graph().traversal().V().hasLabel("fan").has("age", 5); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.gt(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.gte(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.lt(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(4)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.lte(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(4)); Assert.assertFalse(vertices.hasNext()); } @@ -1419,32 +1450,32 @@ public void testAddVertexWithShardIndexAndTtl() { "age", 5, "city", "Shanghai"); this.commitTx(); - Iterator vertices = graph().traversal().V().has("city", + Iterator vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex6, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex3, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex5, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex1, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); @@ -1456,26 +1487,26 @@ public void testAddVertexWithShardIndexAndTtl() { // Ignore } - vertices = graph().traversal().V().has("city", "Shanghai"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(4)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(4)); Assert.assertFalse(vertices.hasNext()); } @@ -1489,7 +1520,7 @@ public void testAddVertexWithSearchIndexAndTtl() { "age", 3, "city", "Beijing Haidian"); this.commitTx(); - Iterator vertices = graph().traversal().V().has( + Iterator vertices = graph().traversal().V().hasLabel("fan").has( "city", Text.contains("Haidian")); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex, vertices.next()); @@ -1501,7 +1532,7 @@ public void testAddVertexWithSearchIndexAndTtl() { // Ignore } - vertices = graph().traversal().V().has("city", + vertices = graph().traversal().V().hasLabel("fan").has("city", Text.contains("Haidian")); Assert.assertFalse(vertices.hasNext()); } @@ -1541,7 +1572,7 @@ public void testOverrideVertexWithSecondaryIndexAndTtl() { "age", 3, "city", "Beijing"); this.commitTx(); - Iterator vertices = graph().traversal().V() + Iterator vertices = graph().traversal().V().hasLabel("fan") .has("city", "Beijing"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex, vertices.next()); @@ -1560,10 +1591,10 @@ public void testOverrideVertexWithSecondaryIndexAndTtl() { // Due to overridden vertices are expired, // query will lead to async delete - vertices = graph().traversal().V().has("city", "Beijing"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Shanghai"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex, vertices.next()); this.commitTx(); @@ -1575,9 +1606,9 @@ public void testOverrideVertexWithSecondaryIndexAndTtl() { } // All edges are expired after 6s - vertices = graph().traversal().V().has("city", "Beijing"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Shanghai"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertFalse(vertices.hasNext()); } @@ -1600,23 +1631,23 @@ public void testOverrideVertexWithRangeIndexAndTtl() { "age", 11, "city", "Beijing"); this.commitTx(); - Iterator vertices = graph().traversal().V().has("age", 7); + Iterator vertices = graph().traversal().V().hasLabel("fan").has("age", 7); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex3, vertices.next()); - vertices = graph().traversal().V().has("age", P.gt(9)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(9)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex5, vertices.next()); - vertices = graph().traversal().V().has("age", P.gte(9)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(9)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.lt(5)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(5)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex1, vertices.next()); - vertices = graph().traversal().V().has("age", P.lte(5)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(5)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); this.commitTx(); @@ -1645,26 +1676,26 @@ public void testOverrideVertexWithRangeIndexAndTtl() { * Due to overridden vertices are expired, * query will lead to async delete */ - vertices = graph().traversal().V().has("age", 7); + vertices = graph().traversal().V().hasLabel("fan").has("age", 7); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", 8); + vertices = graph().traversal().V().hasLabel("fan").has("age", 8); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex8, vertices.next()); - vertices = graph().traversal().V().has("age", P.gt(10)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(10)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex10, vertices.next()); - vertices = graph().traversal().V().has("age", P.gte(10)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(10)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.lt(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex6, vertices.next()); - vertices = graph().traversal().V().has("age", P.lte(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); this.commitTx(); @@ -1676,22 +1707,22 @@ public void testOverrideVertexWithRangeIndexAndTtl() { } // All edges are expired after 6s - vertices = graph().traversal().V().has("age", 7); + vertices = graph().traversal().V().hasLabel("fan").has("age", 7); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", 8); + vertices = graph().traversal().V().hasLabel("fan").has("age", 8); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.gt(10)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(10)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.gte(10)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(10)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.lt(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.lte(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(6)); Assert.assertFalse(vertices.hasNext()); } @@ -1716,32 +1747,32 @@ public void testOverrideVertexWithShardIndexAndTtl() { "age", 5, "city", "Shanghai"); this.commitTx(); - Iterator vertices = graph().traversal().V().has("city", + Iterator vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex6, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex3, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(7)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex5, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(7)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(3)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex1, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(3)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); @@ -1773,38 +1804,38 @@ public void testOverrideVertexWithShardIndexAndTtl() { this.commitTx(); // Due to overridden vertices are expired, query will lead to async delete - vertices = graph().traversal().V().has("city", "Shanghai"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Shenzhen"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shenzhen"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex12, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 6); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex9, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(8)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex11, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(8)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex7, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); @@ -1817,33 +1848,33 @@ public void testOverrideVertexWithShardIndexAndTtl() { } // All vertices are expired after 6s - vertices = graph().traversal().V().has("city", "Shanghai"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Shenzhen"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shenzhen"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 6); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(8)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(8)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(4)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(4)); Assert.assertFalse(vertices.hasNext()); } @@ -1857,7 +1888,7 @@ public void testOverrideVertexWithSearchIndexAndTtl() { "age", 3, "city", "Beijing Haidian"); this.commitTx(); - Iterator vertices = graph().traversal().V().has( + Iterator vertices = graph().traversal().V().hasLabel("fan").has( "city", Text.contains("Haidian")); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex, vertices.next()); @@ -1874,11 +1905,11 @@ public void testOverrideVertexWithSearchIndexAndTtl() { this.commitTx(); // Due to overridden vertices are expired, query will lead to async delete - vertices = graph().traversal().V().has("city", + vertices = graph().traversal().V().hasLabel("fan").has("city", Text.contains("Haidian")); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", + vertices = graph().traversal().V().hasLabel("fan").has("city", Text.contains("Pudong")); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex, vertices.next()); @@ -1891,7 +1922,7 @@ public void testOverrideVertexWithSearchIndexAndTtl() { } // All vertices are expired after 6s - vertices = graph().traversal().V().has("city", + vertices = graph().traversal().V().hasLabel("fan").has("city", Text.contains("Pudong")); Assert.assertFalse(vertices.hasNext()); } @@ -1940,7 +1971,7 @@ public void testQueryVertexWithSecondaryIndexAndTtlInTx() { graph().addVertex(T.label, "fan", "name", "Baby2", "age", 3, "city", "Beijing"); - Iterator vertices = graph().traversal().V() + Iterator vertices = graph().traversal().V().hasLabel("fan") .has("city", "Beijing"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); @@ -1953,7 +1984,7 @@ public void testQueryVertexWithSecondaryIndexAndTtlInTx() { } // All vertices are expired after 3s - vertices = graph().traversal().V().has("city", "Beijing"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing"); Assert.assertFalse(vertices.hasNext()); } @@ -1986,23 +2017,23 @@ public void testQueryVertexWithRangeIndexAndTtlInTx() { graph().addVertex(T.label, "fan", "name", "Baby10", "age", 7, "city", "Beijing"); - Iterator vertices = graph().traversal().V().has("age", 5); + Iterator vertices = graph().traversal().V().hasLabel("fan").has("age", 5); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.gt(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.gte(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(4, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.lt(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.lte(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(4, IteratorUtils.count(vertices)); this.commitTx(); @@ -2014,19 +2045,19 @@ public void testQueryVertexWithRangeIndexAndTtlInTx() { } // All vertices are expired after 3s - vertices = graph().traversal().V().has("age", 5); + vertices = graph().traversal().V().hasLabel("fan").has("age", 5); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.gt(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.gte(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.lt(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(4)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.lte(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(4)); Assert.assertFalse(vertices.hasNext()); } @@ -2063,32 +2094,32 @@ public void testQueryVertexWithShardIndexAndTtlInTx() { graph().addVertex(T.label, "fan", "name", "Baby12", "age", 5, "city", "Shanghai"); - Iterator vertices = graph().traversal().V() + Iterator vertices = graph().traversal().V().hasLabel("fan") .has("city", "Shanghai"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(4, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(4, IteratorUtils.count(vertices)); @@ -2101,26 +2132,26 @@ public void testQueryVertexWithShardIndexAndTtlInTx() { } // All vertices are expired after 3s - vertices = graph().traversal().V().has("city", "Shanghai"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(4)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(4)); Assert.assertFalse(vertices.hasNext()); } @@ -2137,7 +2168,7 @@ public void testQueryVertexWithSearchIndexAndTtlInTx() { graph().addVertex(T.label, "fan", "name", "Baby2", "age", 3, "city", "Beijing Haidian"); - Iterator vertices = graph().traversal().V().has( + Iterator vertices = graph().traversal().V().hasLabel("fan").has( "city", Text.contains("Haidian")); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); @@ -2150,7 +2181,7 @@ public void testQueryVertexWithSearchIndexAndTtlInTx() { } // All vertices are expired after 3s - vertices = graph().traversal().V().has("city", Text.contains("Haidian")); + vertices = graph().traversal().V().hasLabel("fan").has("city", Text.contains("Haidian")); Assert.assertFalse(vertices.hasNext()); } @@ -3271,6 +3302,24 @@ public void testQueryByLabel() { SplicingIdGenerator.splicing(bookId, "java-5"))); } + @Test + public void testQueryByNullKeyAndLabel() { + HugeGraph graph = graph(); + init10Vertices(); + + Assert.assertFalse(graph.traversal().V() + .has((String) null, "test-null-key") + .hasNext()); + Assert.assertFalse(graph.traversal().V() + .hasLabel((String) null) + .hasNext()); + + List vertices = graph.traversal().V() + .hasLabel(null, "book") + .toList(); + Assert.assertEquals(5, vertices.size()); + } + @Test public void testQueryByLabelWithLimit() { HugeGraph graph = graph(); @@ -3621,15 +3670,15 @@ public void testQueryByIntPropWithDifferentDataType() { initPersonIndex(false); init5Persons(); - List vertices = graph.traversal().V().has("age", 21).toList(); + List vertices = graph.traversal().V().hasLabel("person").has("age", 21).toList(); Assert.assertEquals(1, vertices.size()); Assert.assertThrows(IllegalArgumentException.class, () -> { - graph.traversal().V().has("age", 21.0).toList(); + graph.traversal().V().hasLabel("person").has("age", 21.0).toList(); }); Assert.assertThrows(IllegalArgumentException.class, () -> { - graph.traversal().V().has("age", "21").toList(); + graph.traversal().V().hasLabel("person").has("age", "21").toList(); }); } @@ -3645,41 +3694,41 @@ public void testQueryByIntPropWithNegativeNumber() { "city", "Beijing", "age", -10, "birth", Utils.date("2029-01-01")); - List vertices = graph.traversal().V().has("age", -10).toList(); + List vertices = graph.traversal().V().hasLabel("person").has("age", -10).toList(); Assert.assertEquals(1, vertices.size()); assertContains(vertices, T.label, "person", "name", "Sean", "city", "Beijing", "age", -10, "birth", Utils.date("2029-01-01")); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("age", P.between(-11, 0)) .toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("Sean", vertices.get(0).value("name")); - vertices = graph.traversal().V().has("age", P.gt(-11)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.gt(-11)).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph.traversal().V().has("age", P.gte(-10)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.gte(-10)).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph.traversal().V().has("age", P.gt(-10)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.gt(-10)).toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("Louise", vertices.get(0).value("name")); - vertices = graph.traversal().V().has("age", P.gt(-9)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.gt(-9)).toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("Louise", vertices.get(0).value("name")); - vertices = graph.traversal().V().has("age", P.lt(-10)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.lt(-10)).toList(); Assert.assertEquals(0, vertices.size()); - vertices = graph.traversal().V().has("age", P.lte(-10)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.lte(-10)).toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("Sean", vertices.get(0).value("name")); - vertices = graph.traversal().V().has("age", P.lt(0)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.lt(0)).toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("Sean", vertices.get(0).value("name")); } @@ -4051,6 +4100,35 @@ public void testQueryByIntProperty() { }); } + @Test + public void testQueryByNegatedNullPredicate() { + HugeGraph graph = graph(); + + graph.addVertex(T.label, "person", "name", "marko", + "city", "Beijing", "age", 29); + graph.addVertex(T.label, "person", "name", "vadas", + "city", "Beijing", "age", 27); + graph.addVertex(T.label, "person", "name", "lop", + "city", "Shanghai"); + this.commitTx(); + + List negatedNull = graph.traversal().V() + .hasLabel("person") + .has("age", P.not(P.eq(null))) + .values("name") + .toList(); + List notEqualNull = graph.traversal().V() + .hasLabel("person") + .has("age", P.neq(null)) + .values("name") + .toList(); + + Set expected = ImmutableSet.of("marko", "vadas"); + Assert.assertEquals(expected, ImmutableSet.copyOf(negatedNull)); + Assert.assertEquals(expected, ImmutableSet.copyOf(notEqualNull)); + Assert.assertEquals(notEqualNull.size(), negatedNull.size()); + } + @Test public void testQueryByLongProperty() { HugeGraph graph = graph(); @@ -4914,14 +4992,10 @@ public void testQueryWithMultiLayerConditions() { .and(P.lt(29).or(P.eq(35)).or(P.gt(45))) ).values("name").toList(); - // There is duplicate results with OR condition - Assert.assertEquals(5, vertices.size()); - Set names = ImmutableSet.of("Hebe", "James", "Tom Cat", "Lisa"); - for (Object name : vertices) { - Assert.assertTrue(names.contains(name)); - } + Assert.assertEquals(names.size(), vertices.size()); + Assert.assertEquals(names, ImmutableSet.copyOf(vertices)); } @Test @@ -4935,9 +5009,9 @@ public void testQueryByIntPropOfOverrideVertex() { "city", "Hongkong", "age", 29); this.mayCommitTx(); - List vertices = graph.traversal().V().has("age", 28).toList(); + List vertices = graph.traversal().V().hasLabel("person").has("age", 28).toList(); Assert.assertEquals(0, vertices.size()); - vertices = graph.traversal().V().has("age", 29).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", 29).toList(); Assert.assertEquals(1, vertices.size()); } @@ -4952,10 +5026,10 @@ public void testQueryByStringPropOfOverrideVertex() { "city", "Hongkong", "age", 29); this.mayCommitTx(); - List vertices = graph.traversal().V().has("city", "Beijing") + List vertices = graph.traversal().V().hasLabel("person").has("city", "Beijing") .toList(); Assert.assertEquals(0, vertices.size()); - vertices = graph.traversal().V().has("city", "Hongkong").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(1, vertices.size()); } @@ -5005,7 +5079,7 @@ public void testQueryWithTxNotCommittedByIntProp() { "age", 18, "city", "Beijing"); List vertices; - vertices = graph.traversal().V().has("age", 18).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", 18).toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals(18, (int) vertices.get(0).value("age")); @@ -5070,41 +5144,41 @@ public void testQueryByJointIndexes() { List vertices; // results count < 2 - vertices = graph.traversal().V().has("age", 3).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph.traversal().V().has("city", "Hongkong").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Hongkong").has("age", 2).toList(); Assert.assertEquals(0, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Taipei").has("age", 3).toList(); Assert.assertEquals(0, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Hongkong").has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Hongkong").has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Hongkong").has("age", 3) .skip(1).toList(); Assert.assertEquals(0, vertices.size()); // results count >= 2 - vertices = graph.traversal().V().has("age", 20).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", 20).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph.traversal().V().has("age", 19).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", 19).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph.traversal().V().has("city", "Beijing").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "Beijing").toList(); Assert.assertEquals(3, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Beijing").has("age", P.lt(21)).toList(); Assert.assertEquals(3, vertices.size()); @@ -5117,11 +5191,11 @@ public void testQueryByJointIndexes() { Whitebox.setInternalState(tx, "indexTx.indexIntersectThresh", i); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Beijing").has("age", 20).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Beijing").has("age", 20) .skip(1).toList(); Assert.assertEquals(1, vertices.size()); @@ -5140,11 +5214,11 @@ public void testQueryByJointIndexesAndCompositeIndexForOneLabel() { this.commitTx(); List vertices; - vertices = graph().traversal().V().has("age", 3).toList(); + vertices = graph().traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph().traversal().V().has("city", "Hongkong").toList(); + vertices = graph().traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph().traversal().V().has("city", "Hongkong") + vertices = graph().traversal().V().hasLabel("person").has("city", "Hongkong") .has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); @@ -5155,11 +5229,11 @@ public void testQueryByJointIndexesAndCompositeIndexForOneLabel() { graph().schema().indexLabel("personByCityAndAge").onV("person") .by("city", "age").ifNotExist().create(); - vertices = graph().traversal().V().has("age", 3).toList(); + vertices = graph().traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph().traversal().V().has("city", "Hongkong").toList(); + vertices = graph().traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph().traversal().V().has("city", "Hongkong") + vertices = graph().traversal().V().hasLabel("person").has("city", "Hongkong") .has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); } @@ -5187,7 +5261,7 @@ public void testQueryByJointIndexesAndCompositeIndexForMultiLabel() { this.mayCommitTx(); List vertices; - vertices = graph().traversal().V().has("age", 3) + vertices = graph().traversal().V().hasLabel("dog", "cat").has("age", 3) .has("city", "Hongkong").toList(); Assert.assertEquals(2, vertices.size()); Set labels = new HashSet<>(); @@ -5211,7 +5285,7 @@ public void testQueryByJointIndexesOnlyWithCompositeIndex() { "city", "Hongkong", "age", 3); this.mayCommitTx(); - List vertices = graph().traversal().V().has("age", 3) + List vertices = graph().traversal().V().hasLabel("dog").has("age", 3) .has("city", "Hongkong") .has("name", "Tom").toList(); Assert.assertEquals(1, vertices.size()); @@ -5225,7 +5299,7 @@ public void testQueryByJointIndexesWithRangeIndex() { "city", "Hongkong", "age", 3); List vertices; - vertices = graph().traversal().V().has("age", P.gt(2)) + vertices = graph().traversal().V().hasLabel("person").has("age", P.gt(2)) .has("city", "Hongkong").toList(); Assert.assertEquals(1, vertices.size()); } @@ -5243,7 +5317,7 @@ public void testQueryByJointIndexesWithCompositeIndexIncludeOtherField() { "city", "Hongkong", "age", 3); this.mayCommitTx(); - List vertices = graph().traversal().V().has("age", P.gt(2)) + List vertices = graph().traversal().V().hasLabel("dog").has("age", P.gt(2)) .has("city", "Hongkong").toList(); Assert.assertEquals(1, vertices.size()); } @@ -5537,47 +5611,47 @@ public void testQueryByShardIndex() { "city", "Beijing", "age", 29); this.mayCommitTx(); - List vertices = graph().traversal().V() + List vertices = graph().traversal().V().hasLabel("person") .has("city", "Hongkong") .toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .toList(); Assert.assertEquals(3, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Hongkong") .has("age", 15).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Hongkong") .has("age", P.between(10, 20)).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .has("age", P.between(20, 30)).toList(); Assert.assertEquals(3, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .has("age", P.lt(29)).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .has("age", P.lte(29)).toList(); Assert.assertEquals(3, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .has("age", P.gt(21)).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .has("age", P.gte(21)).toList(); Assert.assertEquals(3, vertices.size()); @@ -6392,15 +6466,15 @@ public void testAddVertexPropertyWithSpecialValueForSecondaryIndex() { List vertices; - vertices = graph.traversal().V().has("city", "b\u0001").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "b\u0001").toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("1", vertices.get(0).value("name")); - vertices = graph.traversal().V().has("city", "c\u0002").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "c\u0002").toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("2", vertices.get(0).value("name")); - vertices = graph.traversal().V().has("city", "d\u0003").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "d\u0003").toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("3", vertices.get(0).value("name")); @@ -6423,7 +6497,7 @@ public void testAddVertexPropertyWithSpecialValueForSecondaryIndex() { "city", "a\u0000", "age", 0); this.commitTx(); - vertices = graph.traversal().V().has("city", "a\u0000").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "a\u0000").toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("0", vertices.get(0).value("name")); } @@ -6594,7 +6668,7 @@ public void testQueryVertexByPropertyWithEmptyString() { graph.addVertex(T.label, "person", "name", "Baby", "city", ""); this.commitTx(); - Vertex vertex = graph.traversal().V().has("city", "").next(); + Vertex vertex = graph.traversal().V().hasLabel("person").has("city", "").next(); Assert.assertEquals("Baby", vertex.value("name")); Assert.assertEquals("", vertex.value("city")); } @@ -6689,29 +6763,29 @@ public void testQueryVertexBeforeAfterUpdateMultiPropertyWithIndex() { "city", "Hongkong", "age", 3); this.commitTx(); - List vl = graph.traversal().V().has("age", 3).toList(); + List vl = graph.traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); - vl = graph.traversal().V().has("city", "Hongkong").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); - vl = graph.traversal().V().has("age", 5).toList(); + vl = graph.traversal().V().hasLabel("person").has("age", 5).toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().V().has("city", "Shanghai").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Shanghai").toList(); Assert.assertEquals(0, vl.size()); vertex.property("age", 5); vertex.property("city", "Shanghai"); this.commitTx(); - vl = graph.traversal().V().has("age", 3).toList(); + vl = graph.traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().V().has("city", "Hongkong").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().V().has("age", 5).toList(); + vl = graph.traversal().V().hasLabel("person").has("age", 5).toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); - vl = graph.traversal().V().has("city", "Shanghai").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Shanghai").toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); } @@ -6725,18 +6799,18 @@ public void testQueryVertexBeforeAfterUpdatePropertyWithSecondaryIndex() { "city", "Hongkong", "age", 3); this.commitTx(); - List vl = graph.traversal().V().has("city", "Hongkong").toList(); + List vl = graph.traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); - vl = graph.traversal().V().has("city", "Shanghai").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Shanghai").toList(); Assert.assertEquals(0, vl.size()); vertex.property("city", "Shanghai"); this.commitTx(); - vl = graph.traversal().V().has("city", "Hongkong").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().V().has("city", "Shanghai").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Shanghai").toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); } @@ -6750,18 +6824,18 @@ public void testQueryVertexBeforeAfterUpdatePropertyWithRangeIndex() { "city", "Hongkong", "age", 3); this.commitTx(); - List vl = graph.traversal().V().has("age", 3).toList(); + List vl = graph.traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); - vl = graph.traversal().V().has("age", 5).toList(); + vl = graph.traversal().V().hasLabel("person").has("age", 5).toList(); Assert.assertEquals(0, vl.size()); vertex.property("age", 5); this.commitTx(); - vl = graph.traversal().V().has("age", 3).toList(); + vl = graph.traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().V().has("age", 5).toList(); + vl = graph.traversal().V().hasLabel("person").has("age", 5).toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); } @@ -7829,7 +7903,7 @@ public void testScanVertexInPaging() { Iterator iterator = graph.vertices(query); while (iterator.hasNext()) { Vertex vertex = iterator.next(); - Assert.assertTrue(query.test((HugeElement) vertex)); + Assert.assertTrue(query.test(((HugeElement) vertex).element())); vertices.add(vertex); } page = PageInfo.pageInfo(iterator); @@ -8891,17 +8965,8 @@ public void testQueryByUnionIndexInPageWithSomeIndexNoData() { storeFeatures().supportsQueryByPage()); HugeGraph graph = graph(); - SchemaManager schema = graph.schema(); GraphTraversalSource g = graph.traversal(); - initPageTestData(); - - // Author has index by name but no data - schema.indexLabel("authorByName") - .onV("author") - .by("name") - .secondary() - .ifNotExist() - .create(); + initPageTestData(true); GraphTraversal iter = g.V().has("name", "marko") .has("~page", "").limit(1); @@ -8936,21 +9001,21 @@ public void testQueryBySecondaryIndexWithLimitAndOffset() { initPersonIndex(true); init5Persons(); - List vertices = graph().traversal().V() + List vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing").toList(); Assert.assertEquals(3, vertices.size()); - List verticesSkip = graph().traversal().V() + List verticesSkip = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .skip(1).toList(); Assert.assertEquals(2, verticesSkip.size()); - Set vertices1 = graph().traversal().V() + Set vertices1 = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .range(0, 2).toSet(); Assert.assertEquals(2, vertices1.size()); - Set vertices2 = graph().traversal().V() + Set vertices2 = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .range(2, 3).toSet(); Assert.assertEquals(1, vertices2.size()); @@ -8965,20 +9030,20 @@ public void testQueryByRangeIndexWithLimitAndOffset() { initPersonIndex(false); init5Persons(); - List vertices = graph().traversal().V() + List vertices = graph().traversal().V().hasLabel("person") .has("age", P.between(5, 22)).toList(); Assert.assertEquals(4, vertices.size()); - List verticesSkip = graph().traversal().V() + List verticesSkip = graph().traversal().V().hasLabel("person") .has("age", P.between(5, 22)) .skip(1).toList(); Assert.assertEquals(3, verticesSkip.size()); - Set vertices1 = graph().traversal().V() + Set vertices1 = graph().traversal().V().hasLabel("person") .has("age", P.between(5, 22)) .range(0, 3).toSet(); Assert.assertEquals(3, vertices1.size()); - Set vertices2 = graph().traversal().V() + Set vertices2 = graph().traversal().V().hasLabel("person") .has("age", P.between(5, 22)) .range(3, 4).toSet(); Assert.assertEquals(1, vertices2.size()); @@ -9193,6 +9258,703 @@ public void testQueryByJointLabels() { Assert.assertEquals(0, vertices.size()); } + @Test + public void testQueryByNonEqLabelAndIndexedProperty() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + + List names = g.V().has(T.label, P.neq("author")) + .has("city", "Beijing") + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + } + + @Test + public void testQueryByIndexedPropertyAndNonEqLabel() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + + List names = g.V().has("city", "Beijing") + .has(T.label, P.neq("author")) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + } + + @Test + public void testNegativeLabelAfterReturningToSourceThroughEdge() { + HugeGraph graph = graph(); + initPersonIndex(true); + graph.schema().edgeLabel("fanToPerson").link("fan", "person").create(); + graph.schema().edgeLabel("personToFan").link("person", "fan").create(); + Vertex outFan = graph.addVertex(T.label, "fan", "name", "out-fan", + "age", 20, "city", "Beijing"); + Vertex inFan = graph.addVertex(T.label, "fan", "name", "in-fan", + "age", 20, "city", "Beijing"); + Vertex target = graph.addVertex(T.label, "person", "name", "target", + "age", 20, "city", "Shanghai"); + outFan.addEdge("fanToPerson", target); + target.addEdge("personToFan", inFan); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertEquals(ImmutableList.of(outFan), g.V().has("city", "Beijing") + .outE().outV().hasLabel(P.neq("person")).toList()); + Assert.assertEquals(ImmutableList.of(inFan), g.V().has("city", "Beijing") + .inE().inV().hasLabel(P.neq("person")).toList()); + Assert.assertEquals(ImmutableList.of(outFan), g.V().has("city", "Beijing") + .where(__.outE().outV().hasLabel(P.neq("person"))).toList()); + Assert.assertEquals(ImmutableList.of(inFan), g.V().has("city", "Beijing") + .where(__.inE().inV().hasLabel(P.neq("person"))).toList()); + Assert.assertEquals(ImmutableSet.of(outFan, inFan), + ImmutableSet.copyOf(g.V().has("city", "Beijing") + .bothE().bothV().hasLabel(P.neq("person")).toList())); + Assert.assertEquals(ImmutableSet.of(outFan, inFan), + ImmutableSet.copyOf(g.V().has("city", "Beijing") + .where(__.bothE().bothV().hasLabel(P.neq("person"))).toList())); + Assert.assertEquals(ImmutableList.of(outFan), g.V().has("city", "Beijing") + .where(__.outE().barrier().outV().hasLabel(P.neq("person"))).toList()); + } + + @Test + public void testNegativeLabelAfterAdjacencyPreservesSourceCandidates() { + HugeGraph graph = graph(); + initPersonIndex(true); + graph.schema().vertexLabel("returnFan").properties("name", "age", "city") + .useAutomaticId().create(); + graph.schema().edgeLabel("fanLoop").link("returnFan", "returnFan").create(); + graph.schema().edgeLabel("personLoop").link("person", "person").create(); + Vertex fan = graph.addVertex(T.label, "returnFan", "name", "loop-fan", + "age", 20, "city", "Beijing"); + Vertex person = graph.addVertex(T.label, "person", "name", "loop-person", + "age", 20, "city", "Beijing"); + Edge fanEdge = fan.addEdge("fanLoop", fan); + Edge personEdge = person.addEdge("personLoop", person); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + // A downstream label filter cannot repair incomplete source coverage. + Assert.assertThrows(NoIndexException.class, () -> + g.V().has("city", "Beijing").out().hasLabel(P.neq("author")).toSet()); + for (String label : new String[]{"fan", "follower", "returnFan"}) { + graph.schema().indexLabel(label + "ByCity").onV(label) + .by("city").secondary().create(); + } + Set source = g.V().has("city", "Beijing").toSet(); + Assert.assertEquals(ImmutableSet.of(person, fan), source); + Assert.assertEquals(source, g.V().has("city", "Beijing").out().toSet()); + Assert.assertEquals(ImmutableSet.of(person), g.V().has("city", "Beijing") + .out().hasLabel("person").toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).dedup().toSet()); + Assert.assertEquals(ImmutableList.of(fan, person), g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).order().by("name").toList()); + Assert.assertEquals(ImmutableSet.of(person), g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).order().by("name").tail(1).toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).fold().unfold().toSet()); + Assert.assertEquals(ImmutableList.of(20, 20), g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).values("age").is(P.gt(1)).toList()); + Assert.assertEquals(ImmutableList.of(1, 1), g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).constant(1).toList()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).sample(2).toSet()); + Assert.assertEquals(ImmutableMap.of("person", ImmutableList.of(person), + "returnFan", ImmutableList.of(fan)), g.V() + .has("city", "Beijing").out().hasLabel(P.neq("author")).group().by(T.label).next()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).aggregate("x").toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).cyclicPath().toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().in().hasLabel(P.neq("author")).toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .both().both().hasLabel(P.neq("author")).toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().inE().outV().hasLabel(P.neq("author")).toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .where(__.out().in().hasLabel(P.neq("author"))).toSet()); + Assert.assertEquals(ImmutableSet.of(personEdge, fanEdge), g.V().has("city", "Beijing") + .outE().hasLabel(P.neq("knows")).toSet()); + Assert.assertEquals(ImmutableSet.of(personEdge, fanEdge), g.V().has("city", "Beijing") + .outE().hasLabel(P.neq("knows")).dedup().toSet()); + Assert.assertEquals(ImmutableSet.of(personEdge), g.V().has("city", "Beijing") + .outE().hasLabel(P.neq("knows")).order().by(T.label).tail(1).toSet()); + Assert.assertEquals(ImmutableSet.of(personEdge, fanEdge), g.V().has("city", "Beijing") + .outE().hasLabel(P.neq("knows")).fold().unfold().toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .where(__.out().hasLabel(P.neq("author")).tail(1)).toSet()); + } + + @Test + public void testChildSourceBeforeParentNegativeLabel() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + Vertex fan = graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Set expected = g.V().hasLabel(P.neq("author")) + .has("city", "Beijing").id().toSet(); + Assert.assertEquals(4, expected.size()); + Assert.assertTrue(expected.contains(fan.id())); + + List ids = g.inject(1).union(__.V().has("city", "Beijing")) + .hasLabel(P.neq("author")).id().toList(); + Assert.assertEquals(4, ids.size()); + Assert.assertEquals(expected, ImmutableSet.copyOf(ids)); + ids = g.inject(1).local(__.union(__.V().has("city", "Beijing"))) + .hasLabel(P.neq("author")).id().toList(); + Assert.assertEquals(4, ids.size()); + Assert.assertEquals(expected, ImmutableSet.copyOf(ids)); + } + + @Test + public void testLocalUuidIdBeforeNegativeLabel() { + HugeGraph graph = graph(); + graph.schema().vertexLabel("uuid").useCustomizeUuidId().create(); + UUID first = UUID.fromString("835e1153-9281-4957-8691-cf79258e90eb"); + UUID second = UUID.fromString("835e1153-9281-4957-8691-cf79258e90ec"); + Vertex one = graph.addVertex(T.label, "uuid", T.id, first); + Vertex two = graph.addVertex(T.label, "uuid", T.id, second); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertEquals(ImmutableList.of(one), g.V().hasId(first).toList()); + Assert.assertEquals(ImmutableList.of(one), + g.V().hasId(first).limit(10).hasLabel(P.neq("other")).toList()); + Assert.assertEquals(ImmutableSet.of(one, two), + g.V().hasId(P.within(first, second)).limit(10) + .hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableList.of(one), + g.V(first).hasId(P.within(first, second)).limit(10) + .hasLabel(P.neq("other")).toList()); + Assert.assertTrue(g.V(second).hasId(first).limit(10) + .hasLabel(P.neq("other")).toList().isEmpty()); + P predicate = P.eq(first).or(P.eq(second)); + Assert.assertEquals(ImmutableSet.of(one, two), + g.V().hasId(predicate).limit(10) + .hasLabel(P.neq("other")).toSet()); + // A reused predicate must still accept the caller's original UUIDs. + Assert.assertTrue(predicate.test(first)); + Assert.assertTrue(predicate.test(second)); + Assert.assertEquals(ImmutableList.of(two), + g.V().hasId(P.without(first)).limit(10) + .hasLabel(P.neq("other")).toList()); + } + + @Test + public void testLocalElementIdAndLabelRepresentations() { + HugeGraph graph = graph(); + graph.schema().vertexLabel("localV").useCustomizeNumberId().create(); + Vertex one = graph.addVertex(T.label, "localV", T.id, 123); + Vertex two = graph.addVertex(T.label, "localV", T.id, 456); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertEquals(ImmutableList.of(one), g.V().hasId(one).toList()); + Assert.assertEquals(ImmutableList.of(one), + g.V().hasId(one).limit(10).hasLabel(P.neq("other")).toList()); + Assert.assertEquals(ImmutableSet.of(one, two), + g.V().hasId(P.within(one, two)).limit(10) + .hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableList.of(one), + g.V(one.id()).hasId(P.within(one, two)).limit(10) + .hasLabel(P.neq("other")).toList()); + Assert.assertTrue(g.V(two.id()).hasId(one).limit(10) + .hasLabel(P.neq("other")).toList().isEmpty()); + P ids = P.eq(one).or(P.eq(two)); + Assert.assertEquals(ImmutableSet.of(one, two), + g.V().hasId(ids).limit(10).hasLabel(P.neq("other")).toSet()); + Assert.assertTrue(ids.test(one)); + Id label = graph.vertexLabel("localV").id(); + Set expected = ImmutableSet.of(one, two); + for (Object value : ImmutableList.of("localV", label, label.asLong())) { + Assert.assertEquals(expected, g.V().has(T.label, value).toSet()); + Assert.assertEquals(expected, g.V().has(T.label, value).limit(10) + .hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(expected, g.inject(1).local(__.union(__.V().has(T.label, value))) + .hasLabel(P.neq("other")).toSet()); + } + P labels = P.eq((Object) label).or(P.eq("localV")); + GraphTraversal traversal = g.V().has(T.label, labels) + .limit(10).hasLabel(P.neq("other")); + traversal.asAdmin().applyStrategies(); + GraphTraversal.Admin clone = traversal.asAdmin().clone(); + Assert.assertEquals(expected, traversal.toSet()); + Assert.assertEquals(expected, clone.toSet()); + traversal.asAdmin().reset(); + Assert.assertEquals(expected, traversal.toSet()); + Assert.assertTrue(labels.test(label)); + Assert.assertEquals(expected, g.V().has(T.label, P.eq((Object) label).and(P.neq(-1L))) + .limit(10).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(expected, g.V().has(T.label, P.within(label, IdGenerator.of(-1L))) + .limit(10).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(expected, g.V().has(T.label, P.without(IdGenerator.of(-1L))) + .limit(10).hasLabel(P.neq("other")).toSet()); + Assert.assertTrue(g.V().has(T.label, IdGenerator.of(-1L)).limit(10) + .hasLabel(P.neq("other")).toList().isEmpty()); + Assert.assertTrue(g.V().has(T.label, P.without(label)).limit(10) + .hasLabel(P.neq("other")).toList().isEmpty()); + } + + @Test + public void testNotLabelContextKeepsUnindexedCandidates() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + Vertex fan = graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Set expected = g.V().hasLabel(P.neq("author")).has("city", "Beijing").toSet(); + Assert.assertEquals(4, expected.size()); + Assert.assertTrue(expected.contains(fan)); + Assert.assertEquals(expected, g.V().has("city", "Beijing") + .not(__.hasLabel("author")).toSet()); + Assert.assertEquals(expected, g.V().has("city", "Beijing") + .not(__.or(__.hasLabel("author"), __.hasLabel("missing"))) + .toSet()); + Assert.assertEquals(expected, g.inject(1).union(__.V().has("city", "Beijing")) + .not(__.hasLabel("author")).toSet()); + } + + @Test + public void testNegativeLabelAfterElementChange() { + HugeGraph graph = graph(); + graph.schema().propertyKey("city").asText().ifNotExist().create(); + graph.schema().vertexLabel("indexed").properties("city").useAutomaticId().create(); + graph.schema().vertexLabel("target").useAutomaticId().create(); + graph.schema().indexLabel("byCity").onV("indexed").by("city").secondary().create(); + graph.schema().edgeLabel("link").link("indexed", "target").create(); + Vertex source = graph.addVertex(T.label, "indexed", "city", "Beijing"); + Vertex target = graph.addVertex(T.label, "target"); + source.addEdge("link", target); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertThrows(NoIndexException.class, () -> + g.V().has("city", "Beijing").out().hasLabel(P.neq("indexed")).toSet()); + for (String label : new String[]{"person", "fan", "follower"}) { + graph.schema().indexLabel(label + "ByCity").onV(label) + .by("city").secondary().create(); + } + for (GraphTraversal query : ImmutableList.of( + g.V().has("city", "Beijing").out().hasLabel(P.neq("indexed")), + g.V().has("city", "Beijing").out().where(__.not(__.hasLabel("indexed"))))) { + query.asAdmin().applyStrategies(); + HugeGraphStep step = (HugeGraphStep) query.asAdmin().getStartStep(); + Assert.assertTrue(step.getHasContainers().stream() + .anyMatch(h -> h.getKey().equals("city"))); + Assert.assertEquals(ImmutableList.of(target), query.toList()); + } + Assert.assertEquals(ImmutableList.of(source), g.V().has("city", "Beijing").as("a") + .out().select("a").hasLabel(P.neq("target")).toList()); + Assert.assertEquals(ImmutableList.of(source), g.V().has("city", "Beijing") + .out().path().unfold().hasLabel(P.neq("target")).toList()); + } + + @Test + public void testLocalContainsBeforeNegativeLabel() { + HugeGraph graph = graph(); + graph.schema().propertyKey("age").asInt().ifNotExist().create(); + graph.schema().propertyKey("key").asText().ifNotExist().create(); + graph.schema().propertyKey("value").asInt().ifNotExist().create(); + graph.schema().vertexLabel("containsV").properties("age", "key", "value") + .nullableKeys("age", "key", "value").useAutomaticId().create(); + graph.schema().edgeLabel("containsE").link("containsV", "containsV") + .properties("age", "key", "value").nullableKeys("age", "key", "value").create(); + Vertex one = graph.addVertex(T.label, "containsV", "age", 20); + Vertex two = graph.addVertex(T.label, "containsV", "age", 30); + Vertex misleading = graph.addVertex(T.label, "containsV", "key", "age", "value", 99); + Edge first = one.addEdge("containsE", two, "age", 20); + Edge second = one.addEdge("containsE", misleading, "key", "age", "value", 99); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertEquals(ImmutableSet.of(one, two), + g.V().hasKey("age").hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(one), + g.V().hasValue(20).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(one), + g.V(one.id(), misleading.id()).hasKey("age") + .hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(one), + g.V().hasKey("age").hasId(one.id()).hasValue(20) + .hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(two), + g.V(one.id()).out().hasKey("age").hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(first), + g.E().hasKey("age").hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(first), + g.E().hasValue(20).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(first), + g.E(first.id(), second.id()).hasKey("age") + .hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(first), + g.V(one.id()).outE().hasKey("age").hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(first), + g.V(one.id()).outE().hasValue(20).hasLabel(P.neq("other")).toSet()); + Assert.assertTrue(g.V().hasValue(-1).hasLabel(P.neq("other")).toList().isEmpty()); + Assert.assertTrue(g.V().hasKey("age").hasLabel(P.neq("containsV")).toList().isEmpty()); + Assert.assertThrows(IllegalArgumentException.class, () -> + g.V().hasKey("age", "key").hasLabel(P.neq("other")).toList()); + Assert.assertThrows(IllegalArgumentException.class, () -> + g.V().hasValue(20, 30).hasLabel(P.neq("other")).toList()); + GraphTraversal.Admin traversal = g.V().hasKey("age") + .hasLabel(P.neq("other")).asAdmin(); + traversal.applyStrategies(); + GraphTraversal.Admin clone = traversal.clone(); + Assert.assertEquals(ImmutableSet.of(one, two), traversal.toSet()); + Assert.assertEquals(ImmutableSet.of(one, two), clone.toSet()); + traversal.reset(); + Assert.assertEquals(ImmutableSet.of(one, two), traversal.toSet()); + } + + @Test + public void testLocalConnectiveStringIds() { + HugeGraph graph = graph(); + graph.schema().vertexLabel("stringV").useCustomizeStringId().create(); + graph.schema().edgeLabel("stringE").link("stringV", "stringV").create(); + Vertex one = graph.addVertex(T.label, "stringV", T.id, "a"); + Vertex two = graph.addVertex(T.label, "stringV", T.id, "b"); + Edge edge = one.addEdge("stringE", two); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + P either = P.eq("a").or(P.eq("b")); + Assert.assertEquals(ImmutableSet.of(one), g.V().hasId("a").toSet()); + Assert.assertEquals(ImmutableSet.of(two), g.V().hasId("b").toSet()); + Assert.assertEquals(ImmutableSet.of(one, two), + g.V().hasId(either).limit(10).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(one), + g.V().hasId(P.eq("a").and(P.neq("b"))) + .limit(10).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(one), + g.V(one.id()).hasId(either).hasLabel(P.neq("other")).toSet()); + Assert.assertTrue(either.test("a")); + Assert.assertTrue(either.test("b")); + Assert.assertEquals(ImmutableSet.of(one), + g.V().hasId(P.within("a", "b").and(P.neq("b"))) + .limit(10).hasLabel(P.neq("other")).toSet()); + P edgeIds = P.eq(edge.id().toString()).or(P.eq("missing")); + Assert.assertEquals(ImmutableSet.of(edge), + g.E().hasId(edgeIds).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(edge), + g.V(one.id()).outE().hasId(edgeIds).hasLabel(P.neq("other")).toSet()); + GraphTraversal.Admin traversal = g.V().hasId(either) + .hasLabel(P.neq("other")).asAdmin(); + traversal.applyStrategies(); + GraphTraversal.Admin clone = traversal.clone(); + Assert.assertEquals(ImmutableSet.of(one, two), traversal.toSet()); + Assert.assertEquals(ImmutableSet.of(one, two), clone.toSet()); + traversal.reset(); + Assert.assertEquals(ImmutableSet.of(one, two), traversal.toSet()); + } + + @Test + public void testRangeIndexAfterElementChangeWithTerminalSteps() { + HugeGraph graph = graph(); + graph.schema().propertyKey("age").asInt().ifNotExist().create(); + graph.schema().vertexLabel("indexed").properties("age").useAutomaticId().create(); + graph.schema().vertexLabel("unindexed").properties("age").useAutomaticId().create(); + graph.schema().vertexLabel("target").useAutomaticId().create(); + graph.schema().indexLabel("byAge").onV("indexed").by("age").range().create(); + graph.schema().edgeLabel("link").link("indexed", "target").create(); + Vertex source = graph.addVertex(T.label, "indexed", "age", 60); + graph.addVertex(T.label, "unindexed", "age", 60); + Vertex target = graph.addVertex(T.label, "target"); + source.addEdge("link", target); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertThrows(NoIndexException.class, () -> g.V().has("age", P.gte(60)) + .out().hasLabel(P.neq("indexed")).count().next()); + for (String label : new String[]{"person", "author", "fan", "follower", "unindexed"}) { + graph.schema().indexLabel(label + "ByAge").onV(label) + .by("age").range().create(); + } + List> queries = ImmutableList.of( + g.V().has("age", P.gte(60)).out().hasLabel(P.neq("indexed")).count(), + g.V().has("age", P.gte(60)).out().where(__.not(__.hasLabel("indexed"))).count(), + g.V().has("age", P.gte(60)).out().hasLabel(P.neq("indexed")).id(), + g.V().has("age", P.gte(60)).out().hasLabel(P.neq("indexed")).label()); + List expected = ImmutableList.of(1L, 1L, target.id(), "target"); + for (int i = 0; i < queries.size(); i++) { + GraphTraversal query = queries.get(i); + query.asAdmin().applyStrategies(); + HugeGraphStep step = (HugeGraphStep) query.asAdmin().getStartStep(); + Assert.assertTrue(step.getHasContainers().stream() + .anyMatch(h -> h.getKey().equals("age"))); + Assert.assertEquals(ImmutableList.of(expected.get(i)), query.toList()); + } + } + + @Test + public void testRepeatSiblingNegativeLabel() { + HugeGraph graph = graph(); + graph.schema().propertyKey("city").asText().ifNotExist().create(); + graph.schema().vertexLabel("indexed").properties("city").useAutomaticId().create(); + graph.schema().vertexLabel("unindexed").properties("city").useAutomaticId().create(); + graph.schema().indexLabel("byCity").onV("indexed").by("city").secondary().create(); + Vertex indexed = graph.addVertex(T.label, "indexed", "city", "Beijing"); + Vertex unindexed = graph.addVertex(T.label, "unindexed", "city", "Beijing"); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Set expected = ImmutableSet.of(indexed, unindexed); + Assert.assertEquals(expected, + g.V(indexed.id()).repeat(__.V().has("city", "Beijing")) + .until(__.hasLabel(P.neq("author"))).toSet()); + List emitted = g.V(indexed.id()).repeat(__.V().has("city", "Beijing")) + .emit(__.hasLabel(P.neq("author"))).times(2).toList(); + Assert.assertEquals(6, emitted.size()); + Assert.assertEquals(expected, ImmutableSet.copyOf(emitted)); + } + + @Test + public void testQueryByNonEqLabelAndIndexedPropertyAcrossBarrier() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + + List names = g.V().has(T.label, P.neq("author")) + .barrier().has("city", "Beijing") + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + + names = g.V().has("city", "Beijing").barrier() + .has(T.label, P.neq("author")) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + } + + @Test + public void testQueryByNonEqLabelAndIndexedPropertyAcrossRange() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + + List names = g.V().has("city", "Beijing").skip(0) + .has(T.label, P.neq("author")) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + + names = g.V().has("city", "Beijing").limit(1000) + .has(T.label, P.neq("author")) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + } + + @Test + public void testQueryByNonEqLabelAndIndexedPropertyAcrossMixedKeyOr() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + + List names = g.V().has("city", "Beijing") + .or(__.has(T.label, P.neq("author")), + __.has("age", 20)) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + } + + @Test + public void testQueryByNonEqLabelAndIndexedPropertyAcrossSideEffect() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + + List names = g.V().has("city", "Beijing").aggregate("x") + .has(T.label, P.neq("author")) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + + names = g.V().has("city", "Beijing").coin(1.0D) + .has(T.label, P.neq("author")) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + } + + @Test + public void testQueryByNonEqLabel() { + HugeGraph graph = graph(); + init10Vertices(); + + GraphTraversalSource g = graph.traversal(); + + List vertices = g.V().has(T.label, P.neq("author")).toList(); + Assert.assertEquals(8, vertices.size()); + for (Vertex vertex : vertices) { + Assert.assertNotEquals("author", vertex.label()); + } + } + + @Test + public void testQueryByMixedConnectiveLabel() { + HugeGraph graph = graph(); + init10Vertices(); + + GraphTraversalSource g = graph.traversal(); + + List vertices = g.V() + .has(T.label, P.eq("language") + .or(P.neq("author"))) + .toList(); + Assert.assertEquals(8, vertices.size()); + for (Vertex vertex : vertices) { + Assert.assertNotEquals("author", vertex.label()); + } + + vertices = g.V() + .has(T.label, P.eq("language") + .and(P.neq("author"))) + .toList(); + Assert.assertEquals(3, vertices.size()); + for (Vertex vertex : vertices) { + Assert.assertEquals("language", vertex.label()); + } + } + + @Test + public void testQueryByMultipleNegativeLabelContainers() { + HugeGraph graph = graph(); + init10Vertices(); + + GraphTraversalSource g = graph.traversal(); + + List vertices = g.V().hasLabel("language") + .has(T.label, P.neq("author")) + .toList(); + Assert.assertEquals(3, vertices.size()); + for (Vertex vertex : vertices) { + Assert.assertEquals("language", vertex.label()); + } + + vertices = g.V().has(T.label, P.neq("author")) + .has(T.label, P.neq("book")) + .toList(); + Assert.assertEquals(3, vertices.size()); + for (Vertex vertex : vertices) { + Assert.assertEquals("language", vertex.label()); + } + } + + @Test + public void testCollectMatchedIndexesByJointLabelsWithIndexedProperties() { + HugeGraph graph = graph(); + initPersonIndex(true); + + VertexLabel person = graph.vertexLabel("person"); + VertexLabel computer = graph.vertexLabel("computer"); + PropertyKey city = graph.propertyKey("city"); + + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.query(Condition.in(HugeKeys.LABEL, + ImmutableList.of(person.id(), computer.id()))); + query.query(Condition.eq(city.id(), "Beijing")); + + Set matchedIndexes = Whitebox.invoke(params().graphTransaction(), + "indexTx", + "collectMatchedIndexes", + query); + Assert.assertEquals(1, matchedIndexes.size()); + Object matchedIndex = matchedIndexes.iterator().next(); + SchemaLabel schemaLabel = Whitebox.getInternalState(matchedIndex, + "schemaLabel"); + Assert.assertEquals("person", schemaLabel.name()); + + ConditionQuery conflicting = new ConditionQuery(HugeType.VERTEX); + conflicting.eq(HugeKeys.LABEL, person.id()); + conflicting.eq(HugeKeys.LABEL, computer.id()); + conflicting.query(Condition.eq(city.id(), "Beijing")); + + Assert.assertTrue(conflicting.containsCondition(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(), + conflicting.conditionValues(HugeKeys.LABEL)); + + matchedIndexes = Whitebox.invoke(params().graphTransaction(), + "indexTx", + "collectMatchedIndexes", + conflicting); + Assert.assertEquals(0, matchedIndexes.size()); + } + + @Test + public void testQueryByUserpropWithConflictingLabels() { + HugeGraph graph = graph(); + initPersonIndex(true); + + VertexLabel person = graph.vertexLabel("person"); + VertexLabel computer = graph.vertexLabel("computer"); + PropertyKey city = graph.propertyKey("city"); + + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.eq(HugeKeys.LABEL, person.id()); + query.eq(HugeKeys.LABEL, computer.id()); + query.query(Condition.eq(city.id(), "Beijing")); + + IdHolderList holders = Whitebox.invoke(params().graphTransaction(), + "indexTx", + "queryByUserprop", + query); + Assert.assertTrue(holders.isEmpty()); + Assert.assertFalse(holders.paging()); + } + @Test public void testQueryByHasIdEmptyList() { HugeGraph graph = graph(); @@ -9348,19 +10110,19 @@ public void testQueryBySearchIndexWithSpecialSymbol() { Assert.assertEquals(vertex6.value("city"), city); List vertices; - vertices = g.V().has("city", Text.contains("abc")).toList(); + vertices = g.V().hasLabel("person").has("city", Text.contains("abc")).toList(); Assert.assertEquals(3, vertices.size()); Assert.assertTrue(vertices.contains(vertex1)); Assert.assertTrue(vertices.contains(vertex3)); Assert.assertTrue(vertices.contains(vertex5)); - vertices = g.V().has("city", Text.contains("\u0002")).toList(); + vertices = g.V().hasLabel("person").has("city", Text.contains("\u0002")).toList(); Assert.assertEquals(0, vertices.size()); - vertices = g.V().has("city", Text.contains("\u0003")).toList(); + vertices = g.V().hasLabel("person").has("city", Text.contains("\u0003")).toList(); Assert.assertEquals(0, vertices.size()); - vertices = g.V().has("city", Text.contains("\u0001")).toList(); + vertices = g.V().hasLabel("person").has("city", Text.contains("\u0001")).toList(); Assert.assertEquals(0, vertices.size()); String backend = graph.backend(); @@ -9397,6 +10159,15 @@ public void testEnhanceTextMatch() { graph.schema().indexLabel("personByName").onV("person") .by("name").search().ifNotExist().create(); + // Keep the global SEARCH path: qualifying person would select its + // primary-key path instead. All candidate labels need SEARCH coverage. + for (VertexLabel label : graph.schema().getVertexLabels()) { + if (!label.name().equals("person") && + label.properties().contains(graph.propertyKey("name").id())) { + graph.schema().indexLabel(label.name() + "ByNameSearchCoverage") + .onV(label.name()).by("name").search().create(); + } + } Vertex vertex1 = graph.addVertex(T.label, "person", "name", "秦始皇", "city", "Hongkong", "age", 15); @@ -9434,6 +10205,306 @@ public void testEnhanceTextMatch() { Assert.assertTrue(vertices.contains(vertex4)); } + @Test + public void testPositiveLabelBeforeUnsafeChildUsesLabelQuery() { + HugeGraph graph = graph(); + graph.schema().propertyKey("unindexedAge").asInt().create(); + graph.schema().vertexLabel("sourceV").properties("unindexedAge") + .useAutomaticId().create(); + graph.schema().vertexLabel("targetV").useAutomaticId().create(); + graph.schema().vertexLabel("unrelatedV").useAutomaticId().create(); + graph.schema().vertexLabel("withoutLabelIndex").useAutomaticId() + .enableLabelIndex(false).create(); + graph.schema().edgeLabel("positiveLink").link("sourceV", "targetV").create(); + Vertex source = graph.addVertex(T.label, "sourceV", "unindexedAge", 18); + Vertex target = graph.addVertex(T.label, "targetV"); + Vertex unindexed = graph.addVertex(T.label, "withoutLabelIndex"); + Edge edge = source.addEdge("positiveLink", target); + for (int i = 0; i < 5; i++) { + graph.addVertex(T.label, "unrelatedV"); + } + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Id label = graph.vertexLabel("sourceV").id(); + for (P predicate : new P[]{P.eq("sourceV"), P.eq(label), P.eq(label.asLong()), + P.within("sourceV", "targetV"), P.within(label, "sourceV")}) { + GraphTraversal query = g.V().has(T.label, predicate) + .has("unindexedAge", 18) + .where(__.hasLabel(P.neq("targetV"))); + query.asAdmin().applyStrategies(); + HugeGraphStep step = (HugeGraphStep) query.asAdmin().getStartStep(); + Assert.assertEquals(1, step.getHasContainers().size()); + Assert.assertEquals(T.label.getAccessor(), step.getHasContainers().get(0).getKey()); + Assert.assertEquals(ImmutableList.of(source), query.toList()); + GraphTraversal downstream = g.V().has(T.label, predicate) + .has("unindexedAge", 18) + .where(__.out().hasLabel(P.neq("sourceV"))); + downstream.asAdmin().applyStrategies(); + HugeGraphStep downstreamStep = + (HugeGraphStep) downstream.asAdmin().getStartStep(); + Assert.assertEquals(2, downstreamStep.getHasContainers().size()); + Assert.assertThrows(NoIndexException.class, downstream::toList); + Assert.assertEquals(ImmutableList.of(source), g.V(source.id(), target.id()) + .has(T.label, predicate).where(__.out().hasLabel(P.neq("sourceV"))).toList()); + } + Assert.assertEquals(Long.valueOf(1L), g.V().hasLabel("sourceV") + .where(__.out().hasLabel(P.neq("sourceV"))).count().next()); + Assert.assertTrue(g.V().hasLabel(P.within(Collections.emptyList())) + .where(__.out().hasLabel(P.neq("sourceV"))).toList().isEmpty()); + Assert.assertTrue(g.V().hasLabel("sourceV").hasLabel("targetV") + .where(__.out().hasLabel(P.neq("sourceV"))).toList().isEmpty()); + // Unknown labels are evaluated locally only when fallback is required. + Assert.assertEquals(ImmutableList.of(source), g.V().hasLabel(P.within("sourceV", "missing")) + .where(__.hasLabel(P.neq("targetV"))).toList()); + Assert.assertThrows(IllegalArgumentException.class, + () -> g.V().hasLabel(P.within("sourceV", "missing")) + .where(__.out().hasLabel(P.neq("sourceV"))).toList()); + Assert.assertTrue(g.V().hasLabel("missing").hasLabel(P.neq("sourceV")).toList().isEmpty()); + Assert.assertEquals(ImmutableList.of(unindexed), g.V().hasLabel("withoutLabelIndex") + .hasLabel(P.neq("sourceV")).toList()); + Assert.assertEquals(ImmutableList.of(target), g.V(source.id()).out().hasLabel("targetV") + .hasLabel(P.neq("sourceV")).toList()); + Assert.assertEquals(ImmutableList.of(target), g.V(source.id()).out().has(T.label, + graph.vertexLabel("targetV").id()).hasLabel(P.neq("sourceV")).toList()); + Id edgeLabel = graph.edgeLabel("positiveLink").id(); + Assert.assertEquals(ImmutableList.of(edge), g.E().has(T.label, edgeLabel) + .limit(10).hasLabel(P.neq("other")).toList()); + Assert.assertEquals(ImmutableList.of(edge), g.E(edge.id()).has(T.label, edgeLabel) + .hasLabel(P.neq("other")).toList()); + // A selective label query must not consume unrelated vertices' capacity. + if ("rocksdb".equals(graph.backend())) { + long old = Query.defaultCapacity(3L); + try { + Assert.assertEquals(ImmutableList.of(source), g.V().hasLabel("sourceV") + .where(__.out().hasLabel(P.neq("sourceV"))).toList()); + } finally { + Query.defaultCapacity(old); + } + } + } + + @Test + public void testUnindexedPropertyBeforeNegativeLabel() { + HugeGraph graph = graph(); + graph.schema().propertyKey("unindexedProp").asText().create(); + graph.schema().vertexLabel("scanDoc").properties("unindexedProp") + .useAutomaticId().create(); + graph.schema().vertexLabel("scanExcluded").properties("unindexedProp") + .useAutomaticId().create(); + Vertex match = graph.addVertex(T.label, "scanDoc", "unindexedProp", "x"); + graph.addVertex(T.label, "scanDoc", "unindexedProp", "y"); + graph.addVertex(T.label, "scanExcluded", "unindexedProp", "x"); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x").toList()); + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x") + .out().hasLabel(P.neq("scanExcluded")).toList()); + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x") + .out().hasLabel(P.neq("scanExcluded")).dedup().toList()); + for (GraphTraversal query : new GraphTraversal[]{ + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")).tail(1), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")) + .values("unindexedProp").is("y"), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")) + .fold().unfold(), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")).constant(1), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")).sample(2), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")) + .group().by(T.label), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")) + .aggregate("x"), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")).simplePath(), + g.V().has("unindexedProp", "x").outE().hasLabel(P.neq("knows")).tail(1), + g.V().has("unindexedProp", "x").outE().hasLabel(P.neq("knows")).fold().unfold(), + g.V().has("unindexedProp", "x") + .where(__.out().hasLabel(P.neq("scanExcluded")).tail(1))}) { + Assert.assertThrows(NoIndexException.class, query::toList); + } + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x") + .outE().hasLabel(P.neq("knows")).toList()); + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x") + .outE().hasLabel(P.neq("knows")).dedup().toList()); + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x") + .where(__.out().hasLabel(P.neq("scanExcluded"))).toList()); + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x") + .not(__.out().hasLabel("scanExcluded")).toList()); + Assert.assertEquals(ImmutableList.of(match.id()), + g.V().has("unindexedProp", "x") + .hasLabel(P.neq("scanExcluded")).id().toList()); + Assert.assertEquals(Long.valueOf(1L), g.V().has("unindexedProp", "x") + .hasLabel(P.neq("scanExcluded")).count().next()); + Assert.assertEquals(ImmutableList.of(match.id()), + g.V(match.id()).has("unindexedProp", "x") + .hasLabel(P.neq("scanExcluded")).id().toList()); + long old = Query.defaultCapacity(1L); + try { + if ("rocksdb".equals(graph.backend())) { + // RocksDB's iterator counts candidates, not just final matches. + Assert.assertThrows(LimitExceedException.class, + () -> g.V().has("unindexedProp", "x") + .hasLabel(P.neq("scanExcluded")).toList()); + } else if ("memory".equals(graph.backend())) { + // The test-only memory backend does not enforce scan capacity. + Assert.assertEquals(ImmutableList.of(match.id()), + g.V().has("unindexedProp", "x") + .hasLabel(P.neq("scanExcluded")).id().toList()); + } + } finally { + Query.defaultCapacity(old); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testUnboundLocalSearchUsesGraphAnalyzer() { + HugeGraph graph = graph(); + graph.schema().propertyKey("body").asText().create(); + graph.schema().vertexLabel("runtimeDoc").properties("body").useAutomaticId().create(); + Vertex alpha = graph.addVertex(T.label, "runtimeDoc", "body", "alpha"); + Vertex alphabet = graph.addVertex(T.label, "runtimeDoc", "body", "alphabet"); + this.commitTx(); + for (P predicate : new P[]{Text.contains("(alpha)"), + Text.contains("(alpha)").or(P.eq("beta")).and(P.neq("excluded"))}) { + GraphTraversal.Admin traversal = __.V().has("body", predicate) + .limit(10).hasLabel(P.neq("other")).asAdmin(); + HugeGraphStep source = new HugeGraphStep<>( + (GraphStep) traversal.getStartStep()); + traversal.removeStep(0); + traversal.addStep(0, source); + TraversalUtil.extractHasContainer(source, traversal); + HasContainer local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertTrue(local.test(alpha)); + Assert.assertFalse(local.test(alphabet)); + Assert.assertTrue(local.clone().test(alpha)); + } + } + + @Test + public void testSearchBeforeDownstreamNegativeLabel() { + HugeGraph graph = graph(); + graph.schema().propertyKey("body").asText().create(); + graph.schema().vertexLabel("doc").properties("body").useAutomaticId().create(); + graph.schema().vertexLabel("unindexedDoc").properties("body").useAutomaticId().create(); + graph.schema().indexLabel("docByBody").onV("doc").by("body").search().create(); + Vertex alpha = graph.addVertex(T.label, "doc", "body", "alpha"); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + for (String text : ImmutableList.of("(alpha)", "(alpha|beta)", "alpha", "alpha beta")) { + Assert.assertEquals(ImmutableList.of(alpha.id()), + g.V().hasLabel("doc") + .has("body", Text.contains(text)).id().toList()); + Assert.assertEquals(ImmutableList.of(alpha.id()), + g.V().hasLabel("doc") + .has("body", Text.contains(text)).limit(10) + .has(T.label, P.neq("author")).id().toList()); + } + Assert.assertEquals(ImmutableList.of(alpha.id()), + g.V().has("body", Text.contains("(alpha)").or(P.eq("beta"))) + .limit(10).has(T.label, P.neq("author")).id().toList()); + Vertex unindexed = graph.addVertex(T.label, "unindexedDoc", "body", "alpha"); + this.commitTx(); + List ids = g.V().has("body", Text.contains("(alpha|beta)")) + .limit(10).has(T.label, P.neq("author")).id().toList(); + Assert.assertEquals(2, ids.size()); + Assert.assertEquals(ImmutableSet.of(alpha.id(), unindexed.id()), ImmutableSet.copyOf(ids)); + } + + @Test + public void testNegativeLabelPreservesRangeAndAggregate() { + HugeGraph graph = graph(); + initPersonIndex(true); + graph.schema().vertexLabel("author").properties("city").nullableKeys("city").append(); + graph.addVertex(T.label, "person", "name", "a", "city", "Beijing", "age", 20); + Vertex excluded = graph.addVertex(T.label, "author", "name", "b", + "id", 1, "age", 20, "city", "Beijing"); + Vertex included = graph.addVertex(T.label, "fan", "name", "c", + "age", 20, "city", "Beijing"); + graph.addVertex(T.label, "person", "name", "d", "city", "Beijing", "age", 20); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertEquals(ImmutableList.of(included.id()), + g.V().has("city", "Beijing").order().by("name").range(1, 3) + .has(T.label, P.neq("author")).id().toList()); + GraphTraversal traversal = g.V().has("city", "Beijing") + .order().by("name").range(1, 3).aggregate("seen") + .has(T.label, P.neq("author")); + Assert.assertEquals(ImmutableList.of(included), traversal.toList()); + Collection seen = traversal.asAdmin().getSideEffects().get("seen"); + Assert.assertEquals(2, seen.size()); + Assert.assertTrue(seen.contains(excluded)); + Assert.assertTrue(seen.contains(included)); + } + + @Test + public void testPageBeforeDownstreamNegativeLabel() { + Assume.assumeTrue("Not support paging", storeFeatures().supportsQueryByPage()); + HugeGraph graph = graph(); + Set expected = new HashSet<>(); + for (int i = 0; i < 5; i++) { + expected.add(graph.addVertex(T.label, "person", "name", "page-" + i, + "city", "Beijing", "age", 20).id()); + } + // More excluded vertices than matches also exercises empty filtered + // pages whose backend cursor must still be followed. + for (int i = 0; i < 10; i++) { + graph.addVertex(T.label, "author", "id", i, "name", "excluded-" + i); + } + this.commitTx(); + assertNegativeLabelPages(graph, expected, false); + assertNegativeLabelPages(graph, expected, true); + } + + private static void assertNegativeLabelPages(HugeGraph graph, Set expected, + boolean localFilter) { + Set actual = new HashSet<>(); + String page = ""; + int pages = 0; + do { + GraphTraversal traversal = graph.traversal().V(); + if (localFilter) { + traversal.hasLabel("person").has("age", 20); + } + traversal.has("~page", page).limit(2).has(T.label, P.neq("author")); + traversal.asAdmin().applyStrategies(); + Assert.assertNotNull(TraversalUtil.firstPageStep(traversal)); + List vertices = traversal.toList(); + Assert.assertTrue(vertices.size() <= 2); + for (Vertex vertex : vertices) { + Assert.assertTrue(actual.add(vertex.id())); + } + page = TraversalUtil.page(traversal); + Assert.assertTrue(++pages <= 10); + } while (page != null && !page.isEmpty()); + Assert.assertEquals(expected, actual); + Assert.assertTrue(pages >= 3); + } + + @Test + public void testSelectedLabelsExcludeIndexedInterference() { + HugeGraph graph = graph(); + Set expected = new HashSet<>(); + for (String label : ImmutableList.of("selectedA", "selectedB", "outside")) { + graph.schema().vertexLabel(label).properties("age").useAutomaticId().create(); + graph.schema().indexLabel(label + "ByAge").onV(label).by("age").secondary().create(); + Vertex vertex = graph.addVertex(T.label, label, "age", 20); + if (!label.equals("outside")) { + expected.add(vertex.id()); + } + } + this.commitTx(); + List ids = graph.traversal().V().hasLabel("selectedA", "selectedB") + .has("age", 20).id().toList(); + Assert.assertEquals(2, ids.size()); + Assert.assertEquals(expected, new HashSet<>(ids)); + } + private void init10VerticesAndCommit() { this.init10Vertices(); this.commitTx(); @@ -9534,8 +10605,25 @@ private void init5Computers() { } private void initPageTestData() { + this.initPageTestData(false); + } + + private void initPageTestData(boolean emptyIndexFirst) { SchemaManager schema = graph().schema(); + // These global paging tests require complete source-index coverage. + // Keep their two-label fixture separate from the unrelated labels + // created by initSchema(); partial coverage has dedicated regressions. + for (VertexLabel label : schema.getVertexLabels()) { + schema.vertexLabel(label.name()).remove(); + } schema.propertyKey("lang").asText().ifNotExist().create(); + if (emptyIndexFirst) { + // Keep the empty index ahead of the populated labels so the union + // test still exercises continuation past an empty first source. + schema.vertexLabel("author").properties("name").useAutomaticId().create(); + schema.indexLabel("authorByName").onV("author") + .by("name").secondary().create(); + } schema.vertexLabel("programmer") .properties("name", "age", "city") diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexLabelCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexLabelCoreTest.java index a43731f235..e25c0aa0d4 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexLabelCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexLabelCoreTest.java @@ -20,16 +20,16 @@ import java.util.Date; import java.util.List; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.exception.ExistedException; import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.Userdata; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.task.HugeTask; import org.apache.hugegraph.task.TaskScheduler; import org.apache.hugegraph.task.TaskStatus; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/memory/MemoryConsumerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/memory/MemoryConsumerTest.java index b3a83d0e50..a059ce5848 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/memory/MemoryConsumerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/memory/MemoryConsumerTest.java @@ -26,11 +26,11 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.StandardHugeGraph; import org.apache.hugegraph.backend.cache.CachedBackendStore; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.Query; -import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.backend.BinaryId; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.dist.RegisterUtil; import org.apache.hugegraph.masterelection.GlobalMasterInfo; @@ -38,8 +38,8 @@ import org.apache.hugegraph.memory.consumer.factory.IdFactory; import org.apache.hugegraph.memory.consumer.factory.PropertyFactory; import org.apache.hugegraph.memory.consumer.impl.id.StringIdOffHeap; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeEdgeProperty; @@ -143,7 +143,7 @@ public void testBinaryId() { Id stringIdOffHeap = IdFactory.getInstance().newStringId("java"); Id idOffHeap = IdFactory.getInstance().newBinaryId(stringIdOffHeap.asBytes(), stringIdOffHeap); - Id id = new BinaryBackendEntry.BinaryId(stringIdOffHeap.asBytes(), stringIdOffHeap); + Id id = new BinaryId(stringIdOffHeap.asBytes(), stringIdOffHeap); Assert.assertNotNull(idOffHeap); Assert.assertArrayEquals(stringIdOffHeap.asBytes(), idOffHeap.asBytes()); Assert.assertEquals(id, ((OffHeapObject) idOffHeap).zeroCopyReadFromByteBuf()); @@ -180,20 +180,49 @@ public void testEdgeId() { Id edgeLabelIdOffHeap = IdFactory.getInstance().newLongId(1); Id subLabelIdOffHeap = IdFactory.getInstance().newLongId(2); Id edgeIdOffHeap = - IdFactory.getInstance().newEdgeId(java, Directions.OUT, edgeLabelIdOffHeap, + IdFactory.getInstance().newEdgeId(java.id(), Directions.OUT, edgeLabelIdOffHeap, subLabelIdOffHeap, - "test", java); - Id edgeId = new EdgeId(java, + "test", java.id()); + Id edgeId = new EdgeId(java.id(), Directions.OUT, (Id) ((OffHeapObject) edgeLabelIdOffHeap).zeroCopyReadFromByteBuf(), (Id) ((OffHeapObject) subLabelIdOffHeap).zeroCopyReadFromByteBuf(), "test", - java); + java.id()); Assert.assertNotNull(edgeIdOffHeap); // TODO: adopt equals method Assert.assertEquals(edgeId.asString(), edgeIdOffHeap.asString()); } + @Test + public void testEdgeIdFormattingUsesSharedComponentsAndCache() { + Id owner = IdFactory.getInstance().newStringId("owner>vertex"); + Id other = IdFactory.getInstance().newStringId("other`vertex"); + Id label = IdFactory.getInstance().newLongId(1); + Id subLabel = IdFactory.getInstance().newLongId(2); + String[] sortValues = {"value", "a>b`c!d:北京"}; + for (Directions direction : new Directions[]{Directions.OUT, Directions.IN}) { + for (boolean directed : new boolean[]{false, true}) { + for (String sortValue : sortValues) { + EdgeId offHeap = IdFactory.getInstance().newEdgeId( + owner, direction, label, subLabel, sortValue, other, directed); + EdgeId onHeap = new EdgeId(IdGenerator.of("owner>vertex"), direction, + IdGenerator.of(1), IdGenerator.of(2), sortValue, + IdGenerator.of("other`vertex"), directed); + OffHeapObject consumer = (OffHeapObject) offHeap; + int memoryBlocks = consumer.getAllMemoryBlock().size(); + String formatted = offHeap.asString(); + Assert.assertEquals(onHeap.asString(), formatted); + Assert.assertEquals(memoryBlocks + 1, consumer.getAllMemoryBlock().size()); + Assert.assertEquals(formatted, offHeap.asString()); + Assert.assertEquals(memoryBlocks + 1, consumer.getAllMemoryBlock().size()); + Assert.assertArrayEquals(onHeap.asBytes(), offHeap.asBytes()); + Assert.assertEquals(sortValue, offHeap.sortValues()); + } + } + } + } + @Test public void testProperty() { Id stringIdOffHeap = IdFactory.getInstance().newStringId("java"); @@ -201,12 +230,12 @@ public void testProperty() { Id edgeLabelIdOffHeap = IdFactory.getInstance().newLongId(1); Id subLabelIdOffHeap = IdFactory.getInstance().newLongId(2); - Id edgeId = new EdgeId(java, + Id edgeId = new EdgeId(java.id(), Directions.OUT, (Id) ((OffHeapObject) edgeLabelIdOffHeap).zeroCopyReadFromByteBuf(), (Id) ((OffHeapObject) subLabelIdOffHeap).zeroCopyReadFromByteBuf(), "test", - java); + java.id()); HugeEdge testEdge = new HugeEdge(graph, edgeId, EdgeLabel.NONE); PropertyKey propertyKey = new PropertyKey(null, IdFactory.getInstance().newLongId(3), "fake"); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManagerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManagerTest.java index 074753308e..40ca9ffaa5 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManagerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/meta/managers/AuthMetaManagerTest.java @@ -17,10 +17,16 @@ package org.apache.hugegraph.meta.managers; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; +import org.apache.hugegraph.auth.HugeProject; import org.apache.hugegraph.auth.HugeTarget; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.meta.MetaDriver; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.util.JsonUtil; @@ -70,6 +76,119 @@ public void testDeleteTargetRejectsMismatchBeforeMutation() { Mockito.verify(driver, Mockito.never()).delete(Mockito.anyString()); } + @Test + @SuppressWarnings("unchecked") + public void testProjectGraphsJsonRoundTrip() { + HugeProject project = project(); + Map map = JsonUtil.fromJson( + JsonUtil.toJson(project.asMap()), Map.class); + + Assert.assertTrue(map.get("project_graphs") instanceof List); + HugeProject restored = HugeProject.fromMap(map); + Assert.assertEquals(project.id(), restored.id()); + Assert.assertEquals(project.graphs(), restored.graphs()); + Assert.assertEquals(project.adminGroupId(), restored.adminGroupId()); + Assert.assertEquals(project.opGroupId(), restored.opGroupId()); + Assert.assertEquals(project.targetId(), restored.targetId()); + Assert.assertEquals(project.description(), restored.description()); + } + + @Test + public void testProjectGraphsAcceptsSetAndDeduplicatesList() { + Map map = project().asMap(); + Set graphs = new HashSet<>(Arrays.asList("graph1", "graph2")); + map.put("project_graphs", graphs); + Assert.assertEquals(graphs, HugeProject.fromMap(map).graphs()); + + map.put("project_graphs", Arrays.asList("graph1", "graph2", "graph1")); + Assert.assertEquals(graphs, HugeProject.fromMap(map).graphs()); + map.put("project_graphs", Collections.emptyList()); + Assert.assertTrue(HugeProject.fromMap(map).graphs().isEmpty()); + map.put("project_graphs", null); + Assert.assertTrue(HugeProject.fromMap(map).graphs().isEmpty()); + map.remove("project_graphs"); + Assert.assertTrue(HugeProject.fromMap(map).graphs().isEmpty()); + } + + @Test + public void testProjectGraphsRejectsInvalidTypes() { + Map map = project().asMap(); + for (Object value : Arrays.asList("graph1", Collections.emptyMap(), + Arrays.asList("graph1", 1), + Arrays.asList("graph1", null))) { + map.put("project_graphs", value); + Assert.assertThrows(IllegalArgumentException.class, () -> { + HugeProject.fromMap(map); + }); + } + } + + @Test + public void testProjectMetadataReadAndMutationPaths() throws Exception { + HugeProject project = project(); + String json = JsonUtil.toJson(project.asMap()); + String key = "HUGEGRAPH/cluster/GRAPHSPACE/SPACE_A/AUTH/PROJECT/project"; + MetaDriver driver = Mockito.mock(MetaDriver.class); + Mockito.when(driver.get(key)).thenReturn(json); + Mockito.when(driver.scanWithPrefix(Mockito.anyString())) + .thenReturn(Collections.singletonMap(key, json)); + AuthMetaManager manager = new AuthMetaManager(driver, "cluster"); + + Assert.assertEquals(project.graphs(), + manager.getProject("SPACE_A", project.id()).graphs()); + Assert.assertEquals(project.graphs(), + manager.listAllProjects("SPACE_A", -1).get(0).graphs()); + + project.graphs(Collections.singleton("graph3")); + HugeProject updated = manager.updateProject("SPACE_A", project); + Assert.assertEquals(project.graphs(), updated.graphs()); + Mockito.verify(driver).put(key, JsonUtil.toJson(updated.asMap())); + + HugeProject deleted = manager.deleteProject("SPACE_A", project.id()); + Assert.assertEquals(new HashSet<>(Arrays.asList("graph1", "graph2")), + deleted.graphs()); + Mockito.verify(driver).delete(key); + } + + @Test + public void testClearGraphAuthKeepsGraphsNamedLikeAuth() { + String space = "HUGEGRAPH/cluster/GRAPHSPACE/SPACE_A/"; + // Graph keys sit beside AUTH under the graphspace, so a graph may be + // named AUTH or AUTHx. Its keys and those of other graphspaces stay + Set kept = new HashSet<>(Arrays.asList( + space + "AUTH/SCHEMA/PROPERTY_KEY/NAME/name", + space + "AUTH/TASK/1/TASK_LOCK", + space + "AUTHx/SCHEMA/PROPERTY_KEY/NAME/name", + space + "AUTHx/TASK/1/TASK_LOCK", + "HUGEGRAPH/cluster/GRAPHSPACE/SPACE_AB/AUTH/ROLE/role")); + Set keys = new HashSet<>(kept); + for (String type : Arrays.asList("ROLE", "TARGET", "BELONG", + "ACCESS", "PROJECT")) { + keys.add(space + "AUTH/" + type + "/name"); + } + MetaDriver driver = Mockito.mock(MetaDriver.class); + Mockito.doAnswer(invocation -> { + String prefix = invocation.getArgument(0); + keys.removeIf(key -> key.startsWith(prefix)); + return null; + }).when(driver).deleteWithPrefix(Mockito.anyString()); + AuthMetaManager manager = new AuthMetaManager(driver, "cluster"); + + manager.clearGraphAuth("SPACE_A"); + + Assert.assertEquals(kept, keys); + } + + private static HugeProject project() { + HugeProject project = new HugeProject("project", "description"); + project.adminGroupId(IdGenerator.of("admin-group")); + project.opGroupId(IdGenerator.of("op-group")); + project.targetId(IdGenerator.of("target")); + project.creator("admin"); + project.graphs(new HashSet<>(Arrays.asList("graph1", "graph2"))); + return project; + } + private static HugeTarget target(String graphSpace) { HugeTarget target = new HugeTarget("target", "hugegraph", "url"); target.graphSpace(graphSpace); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessorTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessorTest.java new file mode 100644 index 0000000000..e98d4fbaf1 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessorTest.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.hugegraph.opencypher; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hugegraph.testutil.Assert; +import org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutor; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; +import org.apache.tinkerpop.gremlin.server.Context; +import org.apache.tinkerpop.gremlin.server.GraphManager; +import org.apache.tinkerpop.gremlin.server.Settings; +import org.apache.tinkerpop.gremlin.server.handler.StateKey; +import org.apache.tinkerpop.gremlin.server.op.AbstractEvalOpProcessor; +import org.apache.tinkerpop.gremlin.server.op.OpProcessorException; +import org.apache.tinkerpop.gremlin.util.Tokens; +import org.apache.tinkerpop.gremlin.util.message.RequestMessage; +import org.apache.tinkerpop.gremlin.util.message.ResponseMessage; +import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode; +import org.junit.Test; +import org.opencypher.gremlin.traversal.ParameterNormalizer; +import org.mockito.Mockito; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; + +public class CypherOpProcessorTest { + + private final CypherOpProcessor processor = new CypherOpProcessor(); + + @Test + public void testAcceptCypherParameterNamesAndNullValues() throws Exception { + Map bindings = new HashMap<>(); + bindings.put("value", null); + bindings.put("id", 1); + bindings.put("label", "person"); + Assert.assertFalse(this.processor.validateEvalMessage(request("RETURN $value", bindings)).isPresent()); + Assert.assertFalse(this.processor.validateEvalMessage(RequestMessage.build(Tokens.OPS_EVAL) + .addArg(Tokens.ARGS_GREMLIN, "RETURN 1").create()).isPresent()); + } + + @Test + public void testRejectInvalidQuery() { + assertInvalid(request(1, Collections.emptyMap())); + assertInvalid(request(" \n", Collections.emptyMap())); + } + + @Test + public void testRejectInvalidBindingShapeAndKeys() { + assertInvalid(request("RETURN 1", Collections.emptyList())); + assertInvalid(request("RETURN 1", Collections.singletonMap(1, "value"))); + assertInvalid(request("RETURN 1", Collections.singletonMap(null, "value"))); + } + + @Test + public void testRejectTranslatorNullSentinelInBindings() { + Object reserved = ParameterNormalizer.normalize(Collections.singletonMap("value", null)).get("value"); + Assert.assertEquals(" cypher.null", reserved); + assertInvalid(request("RETURN $value", Collections.singletonMap("value", reserved))); + assertInvalid(request("RETURN $value", Collections.singletonMap("value", + Collections.singleton(reserved)))); + assertInvalid(request("RETURN $value", Collections.singletonMap("value", + Collections.singletonMap("nested", reserved)))); + assertInvalid(request("RETURN $value", Collections.singletonMap("value", + Arrays.asList(null, Collections.singletonMap("nested", Collections.singletonList(reserved)))))); + } + + @Test + public void testAcceptNullAndStringsNearTranslatorSentinel() throws Exception { + Map bindings = Collections.singletonMap("value", + Arrays.asList(null, "cypher.null", " cypher.null", " cypher.null ", + Collections.singletonMap("nested", null))); + Assert.assertFalse(this.processor.validateEvalMessage(request("RETURN $value", bindings)).isPresent()); + } + + @Test + public void testEnforceParameterCountBoundary() throws Exception { + Map bindings = new HashMap<>(); + for (int i = 0; i < AbstractEvalOpProcessor.DEFAULT_MAX_PARAMETERS; i++) { + bindings.put("parameter" + i, i); + } + Assert.assertFalse(this.processor.validateEvalMessage(request("RETURN 1", bindings)).isPresent()); + bindings.put("oneTooMany", 1); + assertInvalid(request("RETURN 1", bindings)); + } + + @Test + public void testRollbackOnTraversalError() throws Exception { + assertRollbackOnTraversalError(false); + } + + @Test + public void testRollbackFailureIsSuppressedOnTraversalError() throws Exception { + assertRollbackOnTraversalError(true); + } + + private void assertRollbackOnTraversalError(boolean failRollback) throws Exception { + Context context = Mockito.mock(Context.class); + GraphManager manager = Mockito.mock(GraphManager.class); + GremlinExecutor gremlinExecutor = Mockito.mock(GremlinExecutor.class); + AtomicReference> submitted = new AtomicReference<>(); + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>()) { + @Override + public void execute(Runnable command) { + submitted.set((FutureTask) command); + super.execute(command); + } + }; + ChannelHandlerContext channelContext = Mockito.mock(ChannelHandlerContext.class); + EmbeddedChannel channel = new EmbeddedChannel(); + channel.attr(StateKey.USE_BINARY).set(false); + Settings settings = new Settings(); + settings.evaluationTimeout = 0; + settings.strictTransactionManagement = false; + Mockito.when(context.getRequestMessage()).thenReturn(request("RETURN 1", Collections.emptyMap())); + Mockito.when(context.getSettings()).thenReturn(settings); + Mockito.when(context.getGraphManager()).thenReturn(manager); + Mockito.when(context.getGremlinExecutor()).thenReturn(gremlinExecutor); + Mockito.when(gremlinExecutor.getExecutorService()).thenReturn(executor); + Mockito.when(context.getChannelHandlerContext()).thenReturn(channelContext); + Mockito.when(channelContext.channel()).thenReturn(channel); + + AssertionError failure = new AssertionError("fatal traversal failure"); + IllegalStateException rollbackFailure = new IllegalStateException("rollback failure"); + CountDownLatch responded = new CountDownLatch(1); + AtomicReference response = new AtomicReference<>(); + Mockito.doAnswer(invocation -> { + response.set(invocation.getArgument(0)); + responded.countDown(); + return null; + }).when(context).writeAndFlush(Mockito.any(ResponseMessage.class)); + AtomicReference iterationThread = new AtomicReference<>(); + AtomicReference rollbackThread = new AtomicReference<>(); + Iterator iterator = Mockito.mock(Iterator.class); + Mockito.when(iterator.hasNext()).thenAnswer(invocation -> { + iterationThread.set(Thread.currentThread()); + throw failure; + }); + Mockito.doAnswer(invocation -> { + rollbackThread.set(Thread.currentThread()); + if (failRollback) { + throw rollbackFailure; + } + return null; + }).when(manager).rollbackAll(); + GraphTraversal source = Mockito.mock(GraphTraversal.class); + Method handleTraversal = CypherOpProcessor.class.getDeclaredMethod( + "handleTraversal", Context.class, Iterator.class, GraphTraversal.class); + handleTraversal.setAccessible(true); + try { + handleTraversal.invoke(this.processor, context, iterator, source); + Assert.assertTrue("Fatal errors must send a terminal response", + responded.await(5, TimeUnit.SECONDS)); + FutureTask task = submitted.get(); + ExecutionException result = Assert.assertThrows(ExecutionException.class, + () -> task.get(5, TimeUnit.SECONDS)); + Assert.assertSame(failure, result.getCause()); + Assert.assertNotNull(iterationThread.get()); + Assert.assertNotSame(Thread.currentThread(), iterationThread.get()); + Assert.assertSame(iterationThread.get(), rollbackThread.get()); + Mockito.verify(manager).rollbackAll(); + Mockito.verify(manager, Mockito.never()).commitAll(); + Mockito.verify(source).close(); + Mockito.verify(context).writeAndFlush(Mockito.any(ResponseMessage.class)); + Assert.assertEquals(ResponseStatusCode.SERVER_ERROR, response.get().getStatus().getCode()); + Assert.assertEquals(failure.getMessage(), response.get().getStatus().getMessage()); + Assert.assertNull(response.get().getResult().getData()); + Assert.assertEquals(failRollback ? 1 : 0, failure.getSuppressed().length); + if (failRollback) { + Assert.assertSame(rollbackFailure, failure.getSuppressed()[0]); + } + } finally { + executor.shutdownNow(); + Assert.assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + channel.finishAndReleaseAll(); + } + } + + private void assertInvalid(RequestMessage message) { + OpProcessorException error = Assert.assertThrows(OpProcessorException.class, + () -> this.processor.validateEvalMessage(message)); + Assert.assertEquals(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS, + error.getResponseMessage().getStatus().getCode()); + } + + private static RequestMessage request(Object query, Object bindings) { + return RequestMessage.build(Tokens.OPS_EVAL) + .addArg(Tokens.ARGS_GREMLIN, query) + .addArg(Tokens.ARGS_BINDINGS, bindings).create(); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/store/client/OrderedScanSecurityTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/store/client/OrderedScanSecurityTest.java index 6d81e66487..c27e04b359 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/store/client/OrderedScanSecurityTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/store/client/OrderedScanSecurityTest.java @@ -81,6 +81,9 @@ private static void assertScanUnderSandbox(ExecutorService executor) throws Exce String factoryScript = "org.apache.hugegraph.store.client.util.ExecutorPool." + "newThreadFactory('untrusted').newThread({} as Runnable)"; engine.eval(factoryScript); + // Load Groovy's Thread metadata before restricting filesystem access. + // The assertion below must exercise thread-group access, not class loading. + engine.eval("new Thread()"); SecurityManager previous = System.getSecurityManager(); String name = Thread.currentThread().getName(); Thread.currentThread().setName("gremlin-server-exec-ordered-scan-test"); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/task/StandardTaskSchedulerTxTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/task/StandardTaskSchedulerTxTest.java new file mode 100644 index 0000000000..6e741e28ab --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/task/StandardTaskSchedulerTxTest.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.hugegraph.task; + +import java.util.Collections; + +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.core.BaseCoreTest; +import org.junit.Assert; +import org.junit.Test; + +public class StandardTaskSchedulerTxTest extends BaseCoreTest { + + @Test + public void testTaskQueryDoesNotOpenUpperGraphTransaction() { + HugeGraph graph = this.graph(); + TaskScheduler scheduler = graph.taskScheduler(); + HugeTask task = new HugeTask<>(IdGenerator.of(9999999L), null, + new TaskAndResultSchedulerTest.EmptyCallable()); + task.type("test"); + task.name("transaction-lifecycle-query"); + boolean saved = false; + try { + // The task worker must also be quiescent after graph startup. + Assert.assertFalse(scheduler.call(() -> graph.tx().isOpen())); + + scheduler.save(task); + saved = true; + scheduler.tasks(TaskStatus.NEW, 1L, null).hasNext(); + Assert.assertFalse(scheduler.call(() -> graph.tx().isOpen())); + + Assert.assertTrue(scheduler.tasks(Collections.singletonList(task.id())).hasNext()); + Assert.assertFalse(scheduler.call(() -> graph.tx().isOpen())); + } finally { + try { + if (saved) { + scheduler.delete(task.id(), true); + } + } finally { + scheduler.call(() -> { + if (graph.tx().isOpen()) { + graph.tx().rollback(); + } + return null; + }); + } + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/task/TaskAndResultSchedulerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/task/TaskAndResultSchedulerTest.java index 15fabf1302..d45b6f5137 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/task/TaskAndResultSchedulerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/task/TaskAndResultSchedulerTest.java @@ -24,10 +24,10 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.atomic.AtomicInteger; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.core.BaseCoreTest; import org.apache.hugegraph.structure.HugeVertex; @@ -209,7 +209,6 @@ private static class TestDistributedTaskScheduler private final ExecutorService olapTaskExecutor; private final ExecutorService gremlinTaskExecutor; private final ExecutorService ephemeralTaskExecutor; - private final ExecutorService serverInfoDbExecutor; private final AtomicInteger resultReadCount; private volatile boolean forbidResultRead; private volatile boolean failNextTaskResultDelete; @@ -220,7 +219,6 @@ private static class TestDistributedTaskScheduler Executors.newSingleThreadExecutor(), Executors.newSingleThreadExecutor(), Executors.newSingleThreadExecutor(), - Executors.newSingleThreadExecutor(), Executors.newSingleThreadExecutor()); } @@ -231,18 +229,15 @@ private TestDistributedTaskScheduler( ExecutorService schemaTaskExecutor, ExecutorService olapTaskExecutor, ExecutorService gremlinTaskExecutor, - ExecutorService ephemeralTaskExecutor, - ExecutorService serverInfoDbExecutor) { + ExecutorService ephemeralTaskExecutor) { super(graph, schedulerExecutor, taskDbExecutor, schemaTaskExecutor, - olapTaskExecutor, gremlinTaskExecutor, ephemeralTaskExecutor, - serverInfoDbExecutor); + olapTaskExecutor, gremlinTaskExecutor, ephemeralTaskExecutor); this.schedulerExecutor = schedulerExecutor; this.taskDbExecutor = taskDbExecutor; this.schemaTaskExecutor = schemaTaskExecutor; this.olapTaskExecutor = olapTaskExecutor; this.gremlinTaskExecutor = gremlinTaskExecutor; this.ephemeralTaskExecutor = ephemeralTaskExecutor; - this.serverInfoDbExecutor = serverInfoDbExecutor; this.resultReadCount = new AtomicInteger(); this.forbidResultRead = false; this.failNextTaskResultDelete = false; @@ -327,7 +322,6 @@ public void closeAndShutdown() { this.olapTaskExecutor.shutdownNow(); this.gremlinTaskExecutor.shutdownNow(); this.ephemeralTaskExecutor.shutdownNow(); - this.serverInfoDbExecutor.shutdownNow(); } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/testutil/Utils.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/testutil/Utils.java index e081423b8e..5fd9928561 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/testutil/Utils.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/testutil/Utils.java @@ -24,10 +24,10 @@ import org.apache.commons.configuration2.PropertiesConfiguration; import org.apache.commons.configuration2.builder.fluent.Configurations; import org.apache.commons.configuration2.ex.ConfigurationException; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.util.DateUtil; import org.apache.hugegraph.util.E; import org.apache.tinkerpop.gremlin.structure.Edge; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphFeatureTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphFeatureTest.java new file mode 100644 index 0000000000..d3f96d1307 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphFeatureTest.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.hugegraph.tinkerpop; + +import org.apache.hugegraph.dist.RegisterUtil; +import org.apache.tinkerpop.gremlin.features.AbstractGuiceFactory; +import org.apache.tinkerpop.gremlin.features.World; +import org.junit.runner.RunWith; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Stage; + +import io.cucumber.guice.CucumberModules; +import io.cucumber.junit.Cucumber; +import io.cucumber.junit.CucumberOptions; + +@RunWith(Cucumber.class) +@CucumberOptions( + tags = HugeGraphFeatureTest.TAGS, + name = HugeGraphFeatureTest.NAMES, + glue = {"org.apache.tinkerpop.gremlin.features"}, + objectFactory = HugeGraphFeatureTest.HugeGraphGuiceFactory.class, + features = { + "classpath:/org/apache/tinkerpop/gremlin/test/features" + }, + plugin = { + "progress", + "junit:target/cucumber-tp38.xml", + "org.apache.hugegraph.tinkerpop." + + "HugeGraphScenarioCountPlugin" + }) +public class HugeGraphFeatureTest { + + public static final String NAMES = + "^g_(?!mergeEXlabel_knows_out_marko_in_vadas_weight_05X_" + + "exists$)(?!V_hasXperson_name_marko_X_mergeEXlabel_knowsX_" + + "optionXonCreate_created_YX_optionXonMatch_created_NX_" + + "exists_updated$)" + + // HugeGraph rejects limit queries while graph initializer data + // is still uncommitted, before mergeE can validate its endpoints. + "(?!V_limitX1X_mergeEXnullvarX$).*"; + + public static final String TAGS = + "(@StepAsString or @StepConcat or @StepFormat or " + + "@StepLength or @StepSplit or @StepSubstring or " + + "@StepReplace or @StepReverse or @StepToLower or " + + "@StepToUpper or @StepTrim or @StepLTrim or @StepRTrim or " + + "@StepCombine or @StepMerge or @StepIntersect or " + + "@StepDifference or @StepDisjunct or @StepConjoin or " + + "@StepProduct or @StepAll or @StepAny or @StepAsDate or " + + "@StepDateAdd or @StepDateDiff or @StepMergeV or " + + "@StepMergeE or @StepFail) and " + + "not @RemoteOnly and not @GraphComputerOnly and " + + "not @AllowNullPropertyValues and not @MetaProperties and " + + "not @MultiProperties and " + + "not @UserSuppliedVertexIds and not @UserSuppliedEdgeIds and " + + "not @UserSuppliedVertexPropertyIds and " + + "not @InsertionOrderingRequired"; + + public static class HugeGraphGuiceFactory extends AbstractGuiceFactory { + + public HugeGraphGuiceFactory() { + super(createInjector()); + } + + private static Injector createInjector() { + RegisterUtil.registerBackends(); + return Guice.createInjector(Stage.PRODUCTION, + CucumberModules.createScenarioModule(), + new ServiceModule()); + } + } + + public static final class ServiceModule extends AbstractModule { + + @Override + protected void configure() { + bind(World.class).to(HugeGraphWorld.class); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderContext.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderContext.java new file mode 100644 index 0000000000..794d7fc5e2 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderContext.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.hugegraph.tinkerpop; + +import java.io.IOException; + +final class HugeGraphProviderContext { + + private ProcessTestGraphProvider provider; + + synchronized ProcessTestGraphProvider provider() { + if (this.provider == null) { + try { + this.provider = new ProcessTestGraphProvider(); + } catch (IOException e) { + throw new IllegalStateException( + "Failed to create graph provider", e); + } + } + return this.provider; + } + + synchronized void clear() { + if (this.provider == null) { + return; + } + + ProcessTestGraphProvider provider = this.provider; + this.provider = null; + provider.clear(); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderLifecycleTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderLifecycleTest.java new file mode 100644 index 0000000000..33d781bc9f --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphProviderLifecycleTest.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.hugegraph.tinkerpop; + +import java.util.Map; + +import org.apache.commons.configuration2.Configuration; +import org.apache.commons.configuration2.MapConfiguration; +import org.apache.hugegraph.dist.RegisterUtil; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Utils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class HugeGraphProviderLifecycleTest { + + @Rule + public TemporaryFolder directory = new TemporaryFolder(); + + @Test + public void testProviderContextLifecycle() throws Exception { + String backend = Utils.getConf().getString("backend"); + RegisterUtil.registerBackends(); + HugeGraphProviderContext context = new HugeGraphProviderContext(); + ProcessTestGraphProvider provider = context.provider(); + TestGraph graph = null; + try { + Assert.assertSame(provider, context.provider()); + + Map config = provider.getBaseConfiguration( + "provider_context", this.getClass(), + "testProviderContextLifecycle", null); + if ("rocksdb".equals(backend)) { + String path = this.directory.getRoot().getAbsolutePath(); + config.put("rocksdb.data_path", path); + config.put("rocksdb.wal_path", path); + config.remove("rocksdb.data_disks"); + } + Configuration configuration = new MapConfiguration(config); + graph = (TestGraph) provider.openTestGraph(configuration); + + Assert.assertEquals(backend, graph.hugegraph().backend()); + Assert.assertFalse(graph.closed()); + + provider.clear(graph, configuration); + Assert.assertEquals("rocksdb".equals(backend), graph.closed()); + + graph = (TestGraph) provider.openTestGraph(configuration); + Assert.assertFalse(graph.closed()); + + context.clear(); + Assert.assertTrue(graph.closed()); + + context.clear(); + ProcessTestGraphProvider nextProvider = context.provider(); + Assert.assertNotSame(provider, nextProvider); + graph = (TestGraph) nextProvider.openTestGraph(configuration); + Assert.assertEquals(backend, graph.hugegraph().backend()); + Assert.assertFalse(graph.closed()); + } finally { + context.clear(); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphScenarioCountPlugin.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphScenarioCountPlugin.java new file mode 100644 index 0000000000..4503f60b31 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphScenarioCountPlugin.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.hugegraph.tinkerpop; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.cucumber.plugin.ConcurrentEventListener; +import io.cucumber.plugin.event.EventPublisher; +import io.cucumber.plugin.event.TestCaseStarted; +import io.cucumber.plugin.event.TestRunFinished; + +public final class HugeGraphScenarioCountPlugin + implements ConcurrentEventListener { + + /* + * This is the exact number selected by TAGS and NAMES for TinkerPop 3.8.1. + * Update it together with an intentional filter or TinkerPop change. + */ + private static final int EXPECTED_SCENARIOS = 361; + + private final AtomicInteger scenarioCount = new AtomicInteger(); + + @Override + public void setEventPublisher(EventPublisher publisher) { + publisher.registerHandlerFor(TestCaseStarted.class, + event -> this.scenarioCount.incrementAndGet()); + publisher.registerHandlerFor(TestRunFinished.class, + event -> this.finishRun()); + } + + private void finishRun() { + try { + assertScenariosExecuted(this.scenarioCount.get()); + } finally { + HugeGraphWorld.clearProvider(); + } + } + + static void assertScenariosExecuted(int scenarioCount) { + if (scenarioCount != EXPECTED_SCENARIOS) { + throw new AssertionError( + scenarioCount + " TinkerPop Gherkin scenarios were " + + "executed, expected exactly " + EXPECTED_SCENARIOS + + " (check the TAGS/NAMES filters and update the expected " + + "count for intentional changes)"); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphTestInfrastructureTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphTestInfrastructureTest.java new file mode 100644 index 0000000000..070bcae9a1 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphTestInfrastructureTest.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.hugegraph.tinkerpop; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.schema.SchemaManager; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class HugeGraphTestInfrastructureTest { + + @Test + public void testProviderContextLifecycle() { + HugeGraphProviderContext context = new HugeGraphProviderContext(); + ProcessTestGraphProvider provider = context.provider(); + try { + Assert.assertSame(provider, context.provider()); + + context.clear(); + context.clear(); + + Assert.assertNotSame(provider, context.provider()); + } finally { + context.clear(); + } + } + + @Test + public void testExactScenarioCount() { + HugeGraphScenarioCountPlugin.assertScenariosExecuted(361); + + Assert.assertThrows(AssertionError.class, () -> { + HugeGraphScenarioCountPlugin.assertScenariosExecuted(360); + }, e -> { + Assert.assertContains("expected exactly 361", e.getMessage()); + }); + Assert.assertThrows(AssertionError.class, () -> { + HugeGraphScenarioCountPlugin.assertScenariosExecuted(362); + }, e -> { + Assert.assertContains("expected exactly 361", e.getMessage()); + }); + } + + @Test + public void testScenarioNameFilterExcludesUnsupportedLimitMerge() { + Assert.assertFalse("g_V_limitX1X_mergeEXnullvarX" + .matches(HugeGraphFeatureTest.NAMES)); + Assert.assertTrue("g_mergeEXnullvarX" + .matches(HugeGraphFeatureTest.NAMES)); + } + + @Test + public void testHStoreCleanupTruncatesDataBeforeClearingSchema() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + SchemaManager schema = Mockito.mock(SchemaManager.class); + PropertyKey propertyKey = Mockito.mock(PropertyKey.class); + Mockito.when(graph.schema()).thenReturn(schema); + Mockito.when(schema.getPropertyKeys()) + .thenReturn(Collections.singletonList(propertyKey)); + Mockito.when(graph.backend()).thenReturn("hstore"); + + CleanupTestGraph testGraph = new CleanupTestGraph(graph); + testGraph.clearAll(""); + + Assert.assertTrue(testGraph.backendTruncated); + Assert.assertTrue(testGraph.schemaCleared); + Assert.assertEquals(Arrays.asList("truncate", "schema"), + testGraph.cleanupSteps); + } + + @Test + public void testHStoreLoadCleanupDoesNotTruncateBackend() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.backend()).thenReturn("hstore"); + + CleanupTestGraph testGraph = new CleanupTestGraph(graph); + testGraph.clearForLoad(); + + Assert.assertFalse(testGraph.backendTruncated); + Assert.assertTrue(testGraph.schemaCleared); + Assert.assertEquals(Collections.singletonList("schema"), + testGraph.cleanupSteps); + } + + @Test + public void testHStoreCleanupDoesNotSkipSchemaWithoutPropertyKeys() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + SchemaManager schema = Mockito.mock(SchemaManager.class); + VertexLabel vertexLabel = Mockito.mock(VertexLabel.class); + Mockito.when(graph.schema()).thenReturn(schema); + Mockito.when(schema.getPropertyKeys()) + .thenReturn(Collections.emptyList()); + Mockito.when(schema.getVertexLabels()) + .thenReturn(Collections.singletonList(vertexLabel)); + Mockito.when(graph.backend()).thenReturn("hstore"); + + CleanupTestGraph testGraph = new CleanupTestGraph(graph); + testGraph.clearAll(""); + + Assert.assertTrue(testGraph.backendTruncated); + Assert.assertTrue(testGraph.schemaCleared); + } + + private static class CleanupTestGraph extends TestGraph { + + private boolean backendTruncated; + private boolean schemaCleared; + private final List cleanupSteps; + + private CleanupTestGraph(HugeGraph graph) { + super(graph); + this.cleanupSteps = new ArrayList<>(); + } + + @Override + protected void truncateBackend() { + this.backendTruncated = true; + this.cleanupSteps.add("truncate"); + } + + @Override + protected void clearSchema() { + this.schemaCleared = true; + this.cleanupSteps.add("schema"); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphWorld.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphWorld.java new file mode 100644 index 0000000000..1b43187d71 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/HugeGraphWorld.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.hugegraph.tinkerpop; + +import java.lang.annotation.Annotation; +import java.util.Locale; +import java.util.Map; + +import org.apache.commons.configuration2.Configuration; +import org.apache.commons.configuration2.MapConfiguration; +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.schema.SchemaManager; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.IdStrategy; +import org.apache.tinkerpop.gremlin.LoadGraphWith; +import org.apache.tinkerpop.gremlin.features.World; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.Element; +import org.apache.tinkerpop.gremlin.structure.Graph; + +import io.cucumber.java.Scenario; + +public class HugeGraphWorld implements World { + + private static final HugeGraphProviderContext PROVIDER_CONTEXT = + new HugeGraphProviderContext(); + + private final ProcessTestGraphProvider provider; + private Scenario scenario; + private Graph graph; + private Configuration configuration; + + public HugeGraphWorld() { + this.provider = PROVIDER_CONTEXT.provider(); + } + + static void clearProvider() { + PROVIDER_CONTEXT.clear(); + } + + @Override + public void beforeEachScenario(Scenario scenario) { + this.scenario = scenario; + } + + @Override + public GraphTraversalSource getGraphTraversalSource( + LoadGraphWith.GraphData graphData) { + if (this.scenario == null) { + throw new IllegalStateException("Scenario has not been initialized"); + } + if (this.graph != null) { + this.clearGraph(); + } + + Map config = this.provider.getBaseConfiguration( + graphName(graphData), HugeGraphFeatureTest.class, + this.scenario.getName(), graphData); + this.configuration = new MapConfiguration(config); + this.graph = this.provider.openTestGraph(this.configuration); + this.prepareGraph(graphData); + return this.provider.traversal(this.graph); + } + + @Override + public void afterEachScenario() { + this.clearGraph(); + } + + @Override + public String convertIdToScript(Object id, + Class type) { + return this.provider.convertId(id, type); + } + + private void clearGraph() { + if (this.graph == null) { + return; + } + + try { + this.provider.clear(this.graph, this.configuration); + } catch (Exception e) { + throw new IllegalStateException("Failed to clear test graph", e); + } finally { + this.graph = null; + this.configuration = null; + } + } + + private void prepareGraph(LoadGraphWith.GraphData graphData) { + TestGraph testGraph = (TestGraph) this.graph; + if (graphData == null) { + testGraph.clearAll(""); + testGraph.initModernSchema(IdStrategy.AUTOMATIC); + this.prepareGherkinSchema(testGraph); + testGraph.tx().commit(); + testGraph.autoPerson(true); + return; + } + + this.provider.loadGraphData(testGraph, new GraphDataLoader(graphData), + HugeGraphFeatureTest.class, + this.scenario.getName()); + this.prepareGherkinSchema(testGraph); + testGraph.tx().commit(); + } + + private void prepareGherkinSchema(TestGraph testGraph) { + HugeGraph graph = testGraph.hugegraph(); + SchemaManager schema = graph.schema(); + schema.propertyKey("birthday").dataType(DataType.OBJECT) + .ifNotExist().create(); + schema.propertyKey("created").ifNotExist().create(); + schema.propertyKey("matched").ifNotExist().create(); + schema.vertexLabel("a").useAutomaticId().ifNotExist().create(); + schema.vertexLabel("b").useAutomaticId().ifNotExist().create(); + schema.vertexLabel("prefix_person").useAutomaticId() + .ifNotExist().create(); + this.prepareVertexLabel(graph, schema, "person"); + this.prepareVertexLabel(graph, schema, "software"); + this.prepareVertexLabel(graph, schema, TestGraph.DEFAULT_VL); + if (graph.existsVertexLabel("person")) { + schema.vertexLabel("person").properties("birthday") + .nullableKeys("birthday").append(); + } + this.prepareEdgeLabel(graph, schema, "knows"); + this.prepareEdgeLabel(graph, schema, "created"); + if (graph.existsVertexLabel("person")) { + schema.edgeLabel("self").link("person", "person") + .properties("weight", "created", "matched") + .nullableKeys("weight", "created", "matched") + .ifNotExist().create(); + this.prepareEdgeLabel(graph, schema, "self"); + } + } + + private void prepareVertexLabel(HugeGraph graph, SchemaManager schema, + String label) { + if (!graph.existsVertexLabel(label)) { + return; + } + schema.vertexLabel(label).properties("created", "matched") + .nullableKeys("created", "matched").append(); + schema.indexLabel(label + "ByCreated").onV(label).by("created") + .secondary().ifNotExist().create(); + schema.indexLabel(label + "ByMatched").onV(label).by("matched") + .secondary().ifNotExist().create(); + } + + private void prepareEdgeLabel(HugeGraph graph, SchemaManager schema, + String label) { + if (!graph.existsEdgeLabel(label)) { + return; + } + schema.edgeLabel(label).properties("created", "matched") + .nullableKeys("created", "matched").append(); + schema.indexLabel(label + "ByCreated").onE(label).by("created") + .secondary().ifNotExist().create(); + schema.indexLabel(label + "ByMatched").onE(label).by("matched") + .secondary().ifNotExist().create(); + } + + private static String graphName(LoadGraphWith.GraphData graphData) { + if (graphData == null) { + return "gherkin_empty_standard"; + } + return "gherkin_" + graphData.name().toLowerCase(Locale.ROOT) + + "_standard"; + } + + private static final class GraphDataLoader implements LoadGraphWith { + + private final GraphData graphData; + + private GraphDataLoader(GraphData graphData) { + this.graphData = graphData; + } + + @Override + public GraphData value() { + return this.graphData; + } + + @Override + public Class annotationType() { + return LoadGraphWith.class; + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/ProcessBasicSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/ProcessBasicSuite.java index e0fcba9832..b289912f00 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/ProcessBasicSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/ProcessBasicSuite.java @@ -25,7 +25,10 @@ import org.apache.tinkerpop.gremlin.process.traversal.CoreTraversalTest; import org.apache.tinkerpop.gremlin.process.traversal.TraversalEngine; import org.apache.tinkerpop.gremlin.process.traversal.TraversalInterruptionTest; +import org.apache.tinkerpop.gremlin.process.traversal.step.ComparabilitySemanticsTest; import org.apache.tinkerpop.gremlin.process.traversal.step.ComplexTest; +import org.apache.tinkerpop.gremlin.process.traversal.step.LambdaStepTest; +import org.apache.tinkerpop.gremlin.process.traversal.step.OrderabilityTest; import org.apache.tinkerpop.gremlin.process.traversal.step.branch.BranchTest; import org.apache.tinkerpop.gremlin.process.traversal.step.branch.ChooseTest; import org.apache.tinkerpop.gremlin.process.traversal.step.branch.LocalTest; @@ -51,6 +54,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.map.CoalesceTest; import org.apache.tinkerpop.gremlin.process.traversal.step.map.ConstantTest; import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountTest; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.ElementMapTest; import org.apache.tinkerpop.gremlin.process.traversal.step.map.FlatMapTest; import org.apache.tinkerpop.gremlin.process.traversal.step.map.FoldTest; import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphTest; @@ -61,6 +65,8 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.map.MathTest; import org.apache.tinkerpop.gremlin.process.traversal.step.map.MaxTest; import org.apache.tinkerpop.gremlin.process.traversal.step.map.MeanTest; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeVertexTest; import org.apache.tinkerpop.gremlin.process.traversal.step.map.MinTest; import org.apache.tinkerpop.gremlin.process.traversal.step.map.OrderTest; import org.apache.tinkerpop.gremlin.process.traversal.step.map.PathTest; @@ -81,12 +87,12 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.SackTest; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.SideEffectCapTest; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.SideEffectTest; -import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.StoreTest; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.SubgraphTest; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.TreeTest; import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategyProcessTest; import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest; import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest; +import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SeedStrategyProcessTest; import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategyProcessTest; import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.TranslationStrategyProcessTest; import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.EarlyLimitStrategyProcessTest; @@ -105,9 +111,13 @@ public class ProcessBasicSuite extends AbstractGremlinSuite { /** * This list of tests in the suite that will be executed - * as part of this suite. + * as part of this suite. It is synchronized with TinkerPop 3.8.1's + * official ProcessStandardSuite; HugeGraphWriteTest + * intentionally replaces WriteTest.Traversals. */ private static final Class[] ALL_TESTS = new Class[]{ + LambdaStepTest.Traversals.class, + // branch BranchTest.Traversals.class, ChooseTest.Traversals.class, @@ -138,6 +148,7 @@ public class ProcessBasicSuite extends AbstractGremlinSuite { CoalesceTest.Traversals.class, ConstantTest.Traversals.class, CountTest.Traversals.class, + ElementMapTest.Traversals.class, FlatMapTest.Traversals.class, FoldTest.Traversals.class, GraphTest.Traversals.class, @@ -149,6 +160,8 @@ public class ProcessBasicSuite extends AbstractGremlinSuite { MathTest.Traversals.class, MaxTest.Traversals.class, MeanTest.Traversals.class, + MergeEdgeTest.Traversals.class, + MergeVertexTest.Traversals.class, MinTest.Traversals.class, SumTest.Traversals.class, OrderTest.Traversals.class, @@ -161,7 +174,7 @@ public class ProcessBasicSuite extends AbstractGremlinSuite { VertexTest.Traversals.class, UnfoldTest.Traversals.class, ValueMapTest.Traversals.class, - // Override WriteTest.Traversals.class + // Intentionally replace WriteTest.Traversals.class HugeGraphWriteTest.class, // sideEffect @@ -173,7 +186,6 @@ public class ProcessBasicSuite extends AbstractGremlinSuite { SackTest.Traversals.class, SideEffectCapTest.Traversals.class, SideEffectTest.Traversals.class, - StoreTest.Traversals.class, SubgraphTest.Traversals.class, TreeTest.Traversals.class, @@ -190,11 +202,16 @@ public class ProcessBasicSuite extends AbstractGremlinSuite { EventStrategyProcessTest.class, ReadOnlyStrategyProcessTest.class, PartitionStrategyProcessTest.class, + SeedStrategyProcessTest.class, SubgraphStrategyProcessTest.class, // optimizations IncidentToAdjacentStrategyProcessTest.class, - EarlyLimitStrategyProcessTest.class + EarlyLimitStrategyProcessTest.class, + + // semantics + OrderabilityTest.Traversals.class, + ComparabilitySemanticsTest.class }; /** @@ -202,6 +219,8 @@ public class ProcessBasicSuite extends AbstractGremlinSuite { * Gremlin flavors should implement to be compliant with Gremlin. */ private static final Class[] TESTS_TO_ENFORCE = new Class[]{ + LambdaStepTest.class, + // branch BranchTest.class, ChooseTest.class, @@ -232,6 +251,7 @@ public class ProcessBasicSuite extends AbstractGremlinSuite { CoalesceTest.class, ConstantTest.class, CountTest.class, + ElementMapTest.class, FlatMapTest.class, FoldTest.class, LoopsTest.class, @@ -261,7 +281,6 @@ public class ProcessBasicSuite extends AbstractGremlinSuite { SackTest.class, SideEffectCapTest.class, SideEffectTest.class, - StoreTest.class, SubgraphTest.class, TreeTest.class }; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/StructureBasicSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/StructureBasicSuite.java index 593e89359e..e6606bb10a 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/StructureBasicSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/StructureBasicSuite.java @@ -30,6 +30,7 @@ import org.apache.tinkerpop.gremlin.structure.GraphTest; import org.apache.tinkerpop.gremlin.structure.PropertyTest; import org.apache.tinkerpop.gremlin.structure.SerializationTest; +import org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest; import org.apache.tinkerpop.gremlin.structure.TransactionTest; import org.apache.tinkerpop.gremlin.structure.VariablesTest; import org.apache.tinkerpop.gremlin.structure.VertexPropertyTest; @@ -64,7 +65,8 @@ public class StructureBasicSuite extends AbstractGremlinSuite { /** * This list of tests in the suite that will be executed. * Gremlin developers should add to this list - * as needed to enforce tests upon implementations. + * as needed to enforce tests upon implementations. This list is synchronized + * with TinkerPop 3.8.1's official StructureStandardSuite. */ private static final Class[] ALL_TESTS = new Class[]{ CommunityGeneratorTest.class, @@ -94,6 +96,7 @@ public class StructureBasicSuite extends AbstractGremlinSuite { SerializationTest.class, StarGraphTest.class, TransactionTest.class, + TransactionMultiThreadedTest.class, VertexTest.class }; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java index 53dc1fe9ac..75a02f5469 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java @@ -30,7 +30,7 @@ import org.apache.hugegraph.io.HugeGraphSONModule; import org.apache.hugegraph.masterelection.GlobalMasterInfo; import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.task.TaskScheduler; import org.apache.hugegraph.testutil.Whitebox; @@ -55,6 +55,8 @@ public class TestGraph implements Graph { public static final Set TRUNCATE_BACKENDS = ImmutableSet.of("rocksdb"); + private static final String HSTORE_BACKEND = "hstore"; + private static volatile int id = 666; private HugeGraph graph; @@ -95,24 +97,53 @@ protected void clearBackend() { @Watched protected void clearAll(String testClass) { - List pks = this.graph.schema().getPropertyKeys(); - if (pks.isEmpty()) { - // No need to clear if there is no PKs(that's no schema and data) + if (!this.hasSchema() && + !testClass.endsWith("VariableAsMapTest")) { + // No need to clear if there is no schema, data, or variables return; } - if (TRUNCATE_BACKENDS.contains(this.graph.backend())) { + String backend = this.graph.backend(); + if (HSTORE_BACKEND.equals(backend)) { + // HStore keeps schema in PD, outside the truncated data store + this.truncateBackend(); + this.clearSchemaAndVariables(testClass); + } else if (TRUNCATE_BACKENDS.contains(backend)) { // Delete all data by truncating tables this.truncateBackend(); } else { - // Clear schema (also include data) + this.clearSchemaAndVariables(testClass); + } + } + + @Watched + protected void clearForLoad() { + if (HSTORE_BACKEND.equals(this.graph.backend())) { + // An auxiliary graph can be loaded while its source remains open. + // Truncating it makes the source invisible to HStore scans. + // Only the bootstrap schema needs to be removed at this point. this.clearSchema(); + } else { + this.clearAll(""); + } + } - // Clear variables if needed (would not clear when clearing schema) - if (testClass.endsWith("VariableAsMapTest")) { - this.clearVariables(); - this.tx().commit(); - } + private boolean hasSchema() { + SchemaManager schema = this.graph.schema(); + return !schema.getPropertyKeys().isEmpty() || + !schema.getVertexLabels().isEmpty() || + !schema.getEdgeLabels().isEmpty() || + !schema.getIndexLabels().isEmpty(); + } + + private void clearSchemaAndVariables(String testClass) { + // Clear schema (also include data) + this.clearSchema(); + + // Clear variables if needed (would not clear when clearing schema) + if (testClass.endsWith("VariableAsMapTest")) { + this.clearVariables(); + this.tx().commit(); } } @@ -430,6 +461,7 @@ public void initModernSchema(IdStrategy idStrategy) { SchemaManager schema = this.graph.schema(); schema.propertyKey("weight").asDouble().ifNotExist().create(); + schema.propertyKey("a").asInt().ifNotExist().create(); schema.propertyKey("name").ifNotExist().create(); schema.propertyKey("lang").ifNotExist().create(); schema.propertyKey("age").asInt().ifNotExist().create(); @@ -499,12 +531,12 @@ public void initModernSchema(IdStrategy idStrategy) { } schema.edgeLabel("knows").link("person", "person") - .properties("weight", "year") - .nullableKeys("weight", "year") + .properties("weight", "year", "a") + .nullableKeys("weight", "year", "a") .ifNotExist().create(); schema.edgeLabel("created").link("person", "software") - .properties("weight") - .nullableKeys("weight") + .properties("weight", "a") + .nullableKeys("weight", "a") .ifNotExist().create(); schema.edgeLabel("codeveloper").link("person", "person") .properties("year") @@ -533,6 +565,10 @@ public void initModernSchema(IdStrategy idStrategy) { .ifNotExist().create(); schema.indexLabel("dogByName").onV("dog").by("name") .ifNotExist().create(); + schema.indexLabel("animalByName").onV("animal").by("name") + .ifNotExist().create(); + schema.indexLabel("animalByAge").onV("animal").by("age").range() + .ifNotExist().create(); schema.indexLabel("vertexByName").onV("vertex").by("name") .ifNotExist().create(); schema.indexLabel("vertexByAge").onV("vertex").by("age").range() @@ -541,6 +577,8 @@ public void initModernSchema(IdStrategy idStrategy) { .ifNotExist().create(); schema.indexLabel("createdByWeight").onE("created").by("weight") .range().ifNotExist().create(); + schema.indexLabel("createdByEdgeWeight").onE("createdBy").by("weight") + .range().ifNotExist().create(); schema.indexLabel("personByNameAge").onV("person").by("name", "age") .ifNotExist().create(); schema.indexLabel("vertexByP").onV("vertex").by("p") @@ -591,9 +629,15 @@ public void initClassicSchema(IdStrategy idStrategy) { @Watched public void initBasicSchema(IdStrategy idStrategy, String defaultVL) { + this.initBasicSchema(idStrategy, defaultVL, defaultVL); + } + + @Watched + public void initBasicSchema(IdStrategy idStrategy, String defaultVL, + String selfVL) { this.initBasicPropertyKey(); this.initBasicVertexLabelV(idStrategy, defaultVL); - this.initBasicVertexLabelAndEdgeLabelExceptV(defaultVL); + this.initBasicVertexLabelAndEdgeLabelExceptV(defaultVL, selfVL); } @Watched @@ -603,7 +647,9 @@ private void initBasicPropertyKey() { schema.propertyKey("__id").ifNotExist().create(); schema.propertyKey("oid").asInt().ifNotExist().create(); schema.propertyKey("communityIndex").asInt().ifNotExist().create(); - schema.propertyKey("test").ifNotExist().create(); + if (!this.graph.existsPropertyKey("test")) { + schema.propertyKey("test").ifNotExist().create(); + } schema.propertyKey("testing").ifNotExist().create(); schema.propertyKey("data").ifNotExist().create(); schema.propertyKey("name").ifNotExist().create(); @@ -748,7 +794,8 @@ private void initBasicVertexLabelV(IdStrategy idStrategy, String defaultVL) { } @Watched - private void initBasicVertexLabelAndEdgeLabelExceptV(String defaultVL) { + private void initBasicVertexLabelAndEdgeLabelExceptV(String defaultVL, + String selfVL) { SchemaManager schema = this.graph.schema(); if (!"person".equals(defaultVL)) { @@ -770,7 +817,7 @@ private void initBasicVertexLabelAndEdgeLabelExceptV(String defaultVL) { .nullableKeys("test") .ifNotExist().create(); - schema.edgeLabel("self").link(defaultVL, defaultVL) + schema.edgeLabel("self").link(selfVL, selfVL) .properties("__id", "test", "name", "some", "acl", "weight", "here", "to-change", "dropped", "not-dropped", "new", "to-drop", "short", "long") @@ -778,6 +825,8 @@ private void initBasicVertexLabelAndEdgeLabelExceptV(String defaultVL) { "here", "to-change", "dropped", "not-dropped", "new", "to-drop", "short", "long") .ifNotExist().create(); + schema.edgeLabel("self-but-different").link(selfVL, selfVL) + .ifNotExist().create(); schema.edgeLabel("aTOa").link(defaultVL, defaultVL) .properties("gremlin.partitionGraphStrategy.partition") .nullableKeys("gremlin.partitionGraphStrategy.partition") @@ -855,6 +904,16 @@ private void initBasicVertexLabelAndEdgeLabelExceptV(String defaultVL) { schema.indexLabel("selfByName").onE("self").by("name") .ifNotExist().create(); + if (!"person".equals(defaultVL)) { + schema.indexLabel("personByName").onV("person").by("name") + .ifNotExist().create(); + } + schema.indexLabel("softwareByName").onV("software").by("name") + .ifNotExist().create(); + schema.indexLabel("friendByName").onE("friend").by("name") + .ifNotExist().create(); + schema.indexLabel("lByName").onE("l").by("name") + .ifNotExist().create(); schema.indexLabel("selfBy__id").onE("self").by("__id") .ifNotExist().create(); schema.indexLabel("selfBySome").onE("self").by("some") @@ -869,12 +928,21 @@ private void initBasicVertexLabelAndEdgeLabelExceptV(String defaultVL) { schema.indexLabel("aTOaByGremlinPartition").onE("aTOa") .by("gremlin.partitionGraphStrategy.partition") .ifNotExist().create(); + schema.indexLabel("aTObByGremlinPartition").onE("aTOb") + .by("gremlin.partitionGraphStrategy.partition") + .ifNotExist().create(); schema.indexLabel("aTOcByGremlinPartition").onE("aTOc") .by("gremlin.partitionGraphStrategy.partition") .ifNotExist().create(); schema.indexLabel("bTOcByGremlinPartition").onE("bTOc") .by("gremlin.partitionGraphStrategy.partition") .ifNotExist().create(); + schema.indexLabel("connectsToByGremlinPartition").onE("connectsTo") + .by("gremlin.partitionGraphStrategy.partition") + .ifNotExist().create(); + schema.indexLabel("relatesToByGremlinPartition").onE("relatesTo") + .by("gremlin.partitionGraphStrategy.partition") + .ifNotExist().create(); schema.edgeLabel("blah1").link(defaultVL, defaultVL) .ifNotExist().create(); schema.edgeLabel("blah2").link(defaultVL, defaultVL) @@ -942,5 +1010,8 @@ public void initSinkSchema() { schema.indexLabel("loopsByName").onV("loops") .secondary().by("name") .ifNotExist().create(); + schema.indexLabel("messageByName").onV("message") + .secondary().by("name") + .ifNotExist().create(); } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraphProvider.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraphProvider.java index c257e8bd1c..45c93c2774 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraphProvider.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraphProvider.java @@ -30,6 +30,7 @@ import org.apache.commons.configuration2.Configuration; import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.commons.text.StringEscapeUtils; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.perf.PerfUtil.Watched; @@ -47,11 +48,13 @@ import org.apache.tinkerpop.gremlin.FeatureRequirements; import org.apache.tinkerpop.gremlin.LoadGraphWith; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest; import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.LazyBarrierStrategy; import org.apache.tinkerpop.gremlin.structure.Element; import org.apache.tinkerpop.gremlin.structure.Graph; import org.apache.tinkerpop.gremlin.structure.Graph.Features.VertexPropertyFeatures; import org.apache.tinkerpop.gremlin.structure.Transaction; +import org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest; import org.junit.Assert; import org.junit.Assume; import org.slf4j.Logger; @@ -85,10 +88,19 @@ public class TestGraphProvider extends AbstractGraphProvider { private static final String GREMLIN_GRAPH_KEY = "gremlin.graph"; private static final String GREMLIN_GRAPH_VALUE = "org.apache.hugegraph.tinkerpop.TestGraphFactory"; + private static final String BACKEND = "backend"; + private static final String BACKEND_ROCKSDB = "rocksdb"; + private static final String ROCKSDB_DATA_PATH = "rocksdb.data_path"; + private static final String ROCKSDB_WAL_PATH = "rocksdb.wal_path"; + private static final String ROCKSDB_DATA_DISKS = "rocksdb.data_disks"; + private static final String TEST_PATH_SEPARATOR = "/"; + private static final int MAX_ROCKSDB_PATH_SUFFIX_PREFIX_LENGTH = 80; private static final String AKEY_CLASS_PREFIX = "org.apache.tinkerpop.gremlin.structure." + "PropertyTest.PropertyFeatureSupportTest"; + private static final String SUPPORTS_PREFIX = "supports"; + private static final String FEATURE_VALUES_SUFFIX = "Values"; private static final String IO_CLASS_PREFIX = "org.apache.tinkerpop.gremlin.structure.io.IoGraphTest"; private static final String IO_TEST_PREFIX = @@ -182,8 +194,17 @@ public Map getBaseConfiguration( confMap.put(key, config.getProperty(key)); } String storePrefix = config.getString(CoreOptions.STORE.name()); - confMap.put(CoreOptions.STORE.name(), - storePrefix + "_" + this.suite + "_" + graphName); + String store = storePrefix + "_" + this.suite + "_" + graphName; + if (isTransactionMultiThreadedPropertyTest(testClass, testMethod)) { + store += "_txprop"; + } else if (isMergeEdgeSelfTest(testClass, testMethod)) { + store += "_meself"; + } + confMap.put(CoreOptions.STORE.name(), store); + if (isRocksDBBackend(config)) { + this.isolateRocksDBPaths(confMap, graphName, testClass, + testMethod); + } confMap.put(GREMLIN_GRAPH_KEY, GREMLIN_GRAPH_VALUE); confMap.put(TEST_CLASS, testClass); confMap.put(TEST_METHOD, testMethod); @@ -193,6 +214,90 @@ public Map getBaseConfiguration( return confMap; } + private void isolateRocksDBPaths(Map confMap, + String graphName, Class testClass, + String testMethod) { + String testClassName = testClass.getName(); + String rawSuffix = this.suite + "_" + graphName + "_" + + testClassName + "_" + testMethod; + String prefix = sanitizePathPart(this.suite + "_" + graphName + "_" + + testClass.getSimpleName() + "_" + + testMethod); + if (prefix.length() > MAX_ROCKSDB_PATH_SUFFIX_PREFIX_LENGTH) { + prefix = prefix.substring(0, + MAX_ROCKSDB_PATH_SUFFIX_PREFIX_LENGTH); + } + String pathSuffix = prefix + "_" + shortHash(rawSuffix); + isolatePath(confMap, ROCKSDB_DATA_PATH, pathSuffix); + isolatePath(confMap, ROCKSDB_WAL_PATH, pathSuffix); + + Object dataDisks = confMap.get(ROCKSDB_DATA_DISKS); + if (dataDisks != null) { + confMap.put(ROCKSDB_DATA_DISKS, + isolateDataDisks(dataDisks, pathSuffix)); + } + } + + private static void isolatePath(Map confMap, String key, + String pathSuffix) { + Object path = confMap.get(key); + if (path == null) { + return; + } + confMap.put(key, appendPath(path.toString(), pathSuffix)); + } + + private static String isolateDataDisks(Object dataDisks, + String pathSuffix) { + String value = dataDisks.toString().trim(); + if (value.isEmpty()) { + return value; + } + + boolean wrapped = value.startsWith("[") && value.endsWith("]"); + String body = wrapped ? value.substring(1, value.length() - 1) : value; + String[] entries = body.split(","); + StringBuilder builder = new StringBuilder(); + for (String entry : entries) { + String item = entry.trim(); + int index = item.indexOf(':'); + if (index < 0) { + return value; + } + String table = item.substring(0, index).trim(); + String path = item.substring(index + 1).trim(); + if (table.isEmpty() || path.isEmpty()) { + return value; + } + if (builder.length() > 0) { + builder.append(','); + } + builder.append(table).append(':') + .append(appendPath(path, pathSuffix)); + } + return wrapped ? "[" + builder + "]" : builder.toString(); + } + + private static String appendPath(String path, String suffix) { + if (path.endsWith("/") || path.endsWith("\\")) { + return path + suffix; + } + return path + TEST_PATH_SEPARATOR + suffix; + } + + private static String sanitizePathPart(String value) { + return value.replaceAll("[^A-Za-z0-9._-]", "_"); + } + + private static String shortHash(String value) { + return Integer.toHexString(value.hashCode()); + } + + private static boolean isRocksDBBackend(Configuration config) { + return config != null && + BACKEND_ROCKSDB.equals(config.getString(BACKEND, "")); + } + private static boolean customizedId(Class test, String testMethod) { Method method; try { @@ -215,10 +320,41 @@ private static boolean customizedId(Class test, String testMethod) { return false; } + private static boolean isTransactionMultiThreadedPropertyTest( + Class testClass, String testMethod) { + return testClass == TransactionMultiThreadedTest.class && + testMethod.equals("shouldChangeVertexProperty"); + } + + private static boolean isMergeEdgeSelfTest(Class testClass, + String testMethod) { + return testClass == MergeEdgeTest.Traversals.class && + testMethod.equals("g_V_mergeEXlabel_self_weight_05X"); + } + private static String getAKeyType(Class clazz, String method) { if (clazz.getCanonicalName().startsWith(AKEY_CLASS_PREFIX)) { - return method.substring(method.indexOf('[') + 9, - method.indexOf('(') - 6); + String feature = method; + int featureStart = method.indexOf('['); + int featureEnd = method.indexOf(']'); + if (featureStart >= 0 && featureEnd > featureStart) { + feature = method.substring(featureStart + 1, featureEnd); + } + + if (!feature.startsWith(SUPPORTS_PREFIX)) { + return null; + } + feature = feature.substring(SUPPORTS_PREFIX.length()); + + int valueStart = feature.indexOf('('); + if (valueStart >= 0) { + feature = feature.substring(0, valueStart); + } + if (!feature.endsWith(FEATURE_VALUES_SUFFIX)) { + return null; + } + return feature.substring(0, feature.length() - + FEATURE_VALUES_SUFFIX.length()); } return null; } @@ -292,8 +428,15 @@ public Graph openTestGraph(final Configuration config) { testGraph.initPropertyKey("long", "Long"); } + if (isTransactionMultiThreadedPropertyTest(testClass, testMethod)) { + testGraph.initPropertyKey("test", "Integer"); + } + // Basic schema is initiated by default once a graph is open - testGraph.initBasicSchema(idStrategy(config), TestGraph.DEFAULT_VL); + String selfVL = isMergeEdgeSelfTest(testClass, testMethod) ? + "person" : TestGraph.DEFAULT_VL; + testGraph.initBasicSchema(idStrategy(config), TestGraph.DEFAULT_VL, + selfVL); if (testClass.getName().equals( "org.apache.tinkerpop.gremlin.process.traversal.step.map.ReadTest$Traversals")) { testGraph.initEdgeLabelPersonKnowsPerson(); @@ -330,6 +473,10 @@ public void clear(Graph graph, Configuration config) throws Exception { String graphName = config.getString(CoreOptions.STORE.name()); if (!testGraph.initedBackend()) { testGraph.close(); + if (this.graphs.get(graphName) == testGraph) { + this.graphs.remove(graphName); + } + return; } if (testGraph.closed()) { if (this.graphs.get(graphName) == testGraph) { @@ -349,6 +496,13 @@ public void clear(Graph graph, Configuration config) throws Exception { Class testClass = (Class) config.getProperty(TEST_CLASS); testGraph.clearAll(testClass.getCanonicalName()); + if (isRocksDBBackend(config)) { + testGraph.close(); + if (this.graphs.get(graphName) == testGraph) { + this.graphs.remove(graphName); + } + } + LOG.debug("Clear graph '{}'", graphName); } @@ -389,7 +543,7 @@ public void loadGraphData(final Graph graph, TestGraph testGraph = (TestGraph) graph; // Clear basic schema initiated in openTestGraph - testGraph.clearAll(""); + testGraph.clearForLoad(); if (testGraph.loadedGraph() == null) { testGraph.loadedGraph(REGULAR_LOAD); @@ -441,6 +595,10 @@ public GraphTraversalSource traversal(Graph graph) { @Override public String convertId(Object id, Class c) { - return id.toString(); + if (id instanceof Number) { + return id.toString(); + } + return String.format("\"%s\"", StringEscapeUtils.escapeJava( + id.toString())); } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java index c79db5056f..002ebd55ea 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java @@ -17,22 +17,42 @@ package org.apache.hugegraph.traversal.optimize; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashSet; +import java.util.List; import java.util.Set; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; import org.apache.hugegraph.exception.NotFoundException; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.BaseVertex; +import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.tinkerpop.gremlin.process.traversal.GType; +import org.apache.tinkerpop.gremlin.process.traversal.NotP; import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.Scope; import org.apache.tinkerpop.gremlin.process.traversal.Step; +import org.apache.tinkerpop.gremlin.process.traversal.TextP; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy; +import org.apache.tinkerpop.gremlin.process.traversal.Traverser; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder; import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.AndStep; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep; @@ -41,9 +61,12 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.map.VertexStep; import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer; import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.InlineFilterStrategy; +import org.apache.tinkerpop.gremlin.process.traversal.util.ConnectiveP; import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.structure.VertexProperty; +import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph; import org.junit.Test; import org.mockito.Mockito; @@ -57,6 +80,62 @@ public void testCanExtractHasContainerWithoutGraph() { null, new HasContainer("~id", P.eq("1")))); Assert.assertFalse(TraversalUtil.canExtractHasContainer( null, new HasContainer("name", P.eq("marko")))); + Assert.assertFalse(TraversalUtil.canExtractHasContainer( + null, new HasContainer(null, P.eq("marko")))); + Assert.assertFalse(TraversalUtil.canExtractHasContainer( + null, new HasContainer(T.label.getAccessor(), P.eq(null)))); + Assert.assertFalse(TraversalUtil.canExtractHasContainer( + null, new HasContainer(T.label.getAccessor(), + P.within(null, "person")))); + } + + @Test + public void testCanExtractHasContainerKeepsTypePredicatesLocal() { + Assert.assertFalse(TraversalUtil.canExtractHasContainer( + null, new HasContainer(T.label.getAccessor(), P.typeOf(GType.STRING)))); + Assert.assertFalse(TraversalUtil.canExtractHasContainer( + null, new HasContainer(T.label.getAccessor(), + P.typeOf(GType.STRING).and(P.eq("person"))))); + Assert.assertFalse(TraversalUtil.canExtractHasContainer( + null, new HasContainer(T.label.getAccessor(), + P.typeOf(GType.STRING).or(P.eq("person"))))); + Assert.assertFalse(TraversalUtil.canExtractHasContainer( + null, new HasContainer(T.label.getAccessor(), P.not(P.typeOf(GType.STRING))))); + Assert.assertFalse(TraversalUtil.canExtractHasContainer( + null, new HasContainer(T.label.getAccessor(), P.test((a, b) -> true, "person")))); + } + + @Test + public void testCanExtractHasContainerWithNullPredicate() { + Assert.assertFalse(TraversalUtil.canExtractHasContainer( + null, new HasContainer("name", null))); + } + + @Test + public void testExtractHasContainerKeepsNullKeyLocal() { + Traversal.Admin traversal = __.V() + .has((String) null, + "test-null-key") + .asAdmin(); + HugeGraphStep newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal)); + } + + @Test + public void testExtractHasContainerKeepsMixedNullLabelLocal() { + Traversal.Admin traversal = __.V() + .hasLabel(null, "person") + .asAdmin(); + HugeGraphStep newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); } @Test @@ -89,6 +168,32 @@ public void testCanExtractHasContainerWithNonTextProperty() { graph, new HasContainer("age", P.eq(1)))); } + @Test + public void testCanExtractHasContainerKeepsNegatedComparePredicateLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + + Assert.assertFalse(TraversalUtil.canExtractHasContainer( + graph, new HasContainer("age", P.not(P.lte(10))))); + } + + @Test + public void testExtractHasContainerKeepsNestedNegatedPredicateLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + + Traversal.Admin traversal = traversal( + __.V().has("age", P.gt(18).and(P.not(P.lte(65)))), graph); + HugeGraphStep newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "age")); + } + @Test public void testCanExtractHasContainerWithTextRangePredicate() { HugeGraph graph = Mockito.mock(HugeGraph.class); @@ -122,6 +227,31 @@ public void testExtractHasContainerKeepsTextRangeGraphHasStep() { Assert.assertTrue(hasStepExists(traversal)); } + @Test + public void testExtractHasContainerKeepsMixedTextGraphHasStepLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + PropertyKey name = propertyKey(2L, "name", DataType.TEXT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + Mockito.when(graph.propertyKey("name")).thenReturn(name); + + Traversal.Admin traversal = traversal( + __.V().has("person", "name", TextP.containing("ar")), + graph); + HasStep hasStep = (HasStep) traversal.getEndStep(); + hasStep.addHasContainer(new HasContainer("age", P.eq(29))); + HugeGraphStep newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertFalse(hasContainer(newStep, T.label.getAccessor())); + Assert.assertFalse(hasContainer(newStep, "age")); + Assert.assertFalse(hasContainer(newStep, "name")); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); + Assert.assertTrue(hasStepExists(traversal, "age")); + Assert.assertTrue(hasStepExists(traversal, "name")); + } + @Test public void testExtractHasContainerKeepsTextRangeWithoutGraph() { Traversal.Admin traversal = __.V() @@ -165,6 +295,71 @@ public void testExtractHasContainerExtractsPositiveLabelOnlyOrStep() { Assert.assertFalse(stepExists(traversal, OrStep.class)); } + @Test + public void testExtractHasContainerKeepsUnsafeLabelAfterPositiveLabelOr() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + PropertyKey city = propertyKey(2L, "city", DataType.TEXT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + Mockito.when(graph.propertyKey("city")).thenReturn(city); + Mockito.when(graph.vertexLabels()).thenReturn(Collections.emptyList()); + + Traversal.Admin traversal = traversal( + __.V().has("age", P.gt(1)) + .or(__.hasLabel("person"), __.hasLabel("software")) + .has(T.label, P.neq("author")) + .has("city", "Beijing"), graph); + HugeGraphStep newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "age")); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); + Assert.assertTrue(hasStepExists(traversal, "city")); + Assert.assertTrue(stepExists(traversal, OrStep.class)); + } + + @Test + public void testExtractHasContainerKeepsMixedKeyOrUnsafeLabelLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey city = propertyKey(2L, "city", DataType.TEXT); + PropertyKey status = propertyKey(3L, "status", DataType.TEXT); + Mockito.when(graph.propertyKey("city")).thenReturn(city); + Mockito.when(graph.propertyKey("status")).thenReturn(status); + Mockito.when(graph.vertexLabels()).thenReturn(Collections.emptyList()); + + Traversal.Admin traversal = traversal( + __.V().has("city", "Beijing") + .or(__.has(T.label, P.neq("author")), + __.has("status", "active")), graph); + HugeGraphStep newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "city")); + Assert.assertTrue(stepExists(traversal, OrStep.class)); + } + + @Test + public void testExtractHasContainerKeepsGlobalChildUnsafeLabelLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey city = propertyKey(2L, "city", DataType.TEXT); + Mockito.when(graph.propertyKey("city")).thenReturn(city); + + Traversal.Admin traversal = traversal( + __.V().has("city", "Beijing") + .union(__.has(T.label, P.neq("author")), + __.identity()), graph); + HugeGraphStep newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "city")); + } + @Test public void testExtractHasContainerKeepsUnsupportedOrLabelLocal() { Traversal.Admin traversal = __.V() @@ -263,6 +458,33 @@ public void testExtractHasContainerRemovesSafeGraphHasStep() { Assert.assertFalse(hasStepExists(traversal)); } + @Test + public void testExtractHasContainerKeepsGraphChainWithUnsafeLabel() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + + Traversal.Admin traversal = traversal( + __.V().has(T.label, P.neq("author")).barrier() + .has("age", 18), graph); + HugeGraphStep newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertEquals(2, countHasSteps(traversal)); + + traversal = traversal( + __.V().has("age", 18).barrier() + .has(T.label, P.neq("author")), graph); + newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertEquals(2, countHasSteps(traversal)); + } + @Test public void testExtractHasContainerKeepsTextRangeVertexHasStep() { HugeGraph graph = Mockito.mock(HugeGraph.class); @@ -280,6 +502,31 @@ public void testExtractHasContainerKeepsTextRangeVertexHasStep() { Assert.assertTrue(hasStepExists(traversal)); } + @Test + public void testExtractHasContainerKeepsMixedTextVertexHasStepLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + PropertyKey name = propertyKey(2L, "name", DataType.TEXT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + Mockito.when(graph.propertyKey("name")).thenReturn(name); + + Traversal.Admin traversal = traversal( + __.V().out().has("person", "name", TextP.containing("ar")), + graph); + HasStep hasStep = (HasStep) traversal.getEndStep(); + hasStep.addHasContainer(new HasContainer("age", P.eq(29))); + HugeVertexStep newStep = replaceVertexStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertFalse(hasContainer(newStep, T.label.getAccessor())); + Assert.assertFalse(hasContainer(newStep, "age")); + Assert.assertFalse(hasContainer(newStep, "name")); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); + Assert.assertTrue(hasStepExists(traversal, "age")); + Assert.assertTrue(hasStepExists(traversal, "name")); + } + @Test public void testExtractHasContainerRemovesSafeVertexHasStep() { HugeGraph graph = Mockito.mock(HugeGraph.class); @@ -296,6 +543,107 @@ public void testExtractHasContainerRemovesSafeVertexHasStep() { Assert.assertFalse(hasStepExists(traversal)); } + @Test + public void testExtractHasContainerKeepsVertexChainWithUnsafeLabel() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + + Traversal.Admin traversal = traversal( + __.V().out().has(T.label, P.neq("author")).barrier() + .has("age", 18), graph); + HugeVertexStep newStep = replaceVertexStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertEquals(2, countHasSteps(traversal)); + + traversal = traversal( + __.V().out().has("age", 18).barrier() + .has(T.label, P.neq("author")), graph); + newStep = replaceVertexStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertEquals(2, countHasSteps(traversal)); + } + + @Test + public void testExtractHasContainerKeepsVertexMixedKeyOrUnsafeLabelLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + PropertyKey city = propertyKey(2L, "city", DataType.TEXT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + Mockito.when(graph.propertyKey("city")).thenReturn(city); + + Traversal.Admin traversal = traversal( + __.V().out().has("age", 18) + .or(__.has(T.label, P.neq("author")), + __.has("city", "Beijing")), graph); + HugeVertexStep newStep = replaceVertexStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "age")); + Assert.assertTrue(stepExists(traversal, OrStep.class)); + } + + @Test + public void testExtractHasContainerKeepsTrailingUnsafeVertexLabelLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + + Traversal.Admin traversal = traversal( + __.V().out().has("age", P.gt(18)) + .or(__.hasLabel("person"), __.hasLabel("software")) + .has(T.label, P.neq("author")), graph); + HugeVertexStep newStep = replaceVertexStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "age")); + Assert.assertTrue(stepExists(traversal, OrStep.class)); + } + + @Test + public void testLabelContainersWithImmutableList() { + assertNullHostileLabelCollection(List.of("person")); + } + + @Test + public void testLabelContainersWithImmutableSet() { + assertNullHostileLabelCollection(Set.of("person")); + } + + private static void assertNullHostileLabelCollection(Collection labels) { + HasContainer within = new HasContainer(T.label.getAccessor(), P.within(labels)); + Assert.assertTrue(TraversalUtil.canExtractHasContainer(null, within)); + Assert.assertTrue(TraversalUtil.isPositiveLabelContainer(within)); + HasContainer without = new HasContainer(T.label.getAccessor(), P.without(labels)); + Assert.assertTrue(TraversalUtil.canExtractHasContainer(null, without)); + Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(without)); + } + + @Test + public void testLabelContainersWithMutableNullValuesStayLocal() { + Collection list = new ArrayList<>(List.of("person")); + list.add(null); + Collection set = new HashSet<>(list); + for (Collection labels : List.of(list, set)) { + HasContainer within = new HasContainer(T.label.getAccessor(), P.within(labels)); + Assert.assertFalse(TraversalUtil.canExtractHasContainer(null, within)); + Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(within)); + HasContainer without = new HasContainer(T.label.getAccessor(), P.without(labels)); + Assert.assertFalse(TraversalUtil.canExtractHasContainer(null, without)); + Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(without)); + } + } + @Test public void testIsPositiveLabelContainer() { Assert.assertTrue(TraversalUtil.isPositiveLabelContainer( @@ -314,6 +662,682 @@ public void testIsPositiveLabelContainer() { Assert.assertFalse(TraversalUtil.isPositiveLabelContainer( new HasContainer(T.label.getAccessor(), P.within(Collections.emptyList())))); + Assert.assertFalse(TraversalUtil.isPositiveLabelContainer( + new HasContainer(T.label.getAccessor(), P.eq(null)))); + Assert.assertFalse(TraversalUtil.isPositiveLabelContainer( + new HasContainer(T.label.getAccessor(), + P.within(null, "person")))); + } + + @Test + public void testPageIsConsumedBeforeUnsafeLabel() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Traversal.Admin traversal = traversal( + __.V().has("~page", "").limit(2).has(T.label, P.neq("author")), graph); + HugeGraphStep graphStep = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(graphStep, traversal); + Assert.assertTrue(graphStep.queryInfo().paging()); + Assert.assertFalse(hasStepExists(traversal, "~page")); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); + + traversal = traversal( + __.V().outE().has("~page", "").limit(2).has(T.label, P.neq("knows")), graph); + HugeVertexStep vertexStep = replaceVertexStep(traversal); + TraversalUtil.extractHasContainer(vertexStep, traversal); + Assert.assertTrue(vertexStep.queryInfo().paging()); + Assert.assertFalse(hasStepExists(traversal, "~page")); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); + } + + @Test + public void testLocalSearchWithoutGraphUsesRuntimeAnalyzer() throws Exception { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.searchPredicate("(alpha)")).thenReturn("alpha"::equals); + HugeGraph other = Mockito.mock(HugeGraph.class); + Mockito.when(other.searchPredicate("(alpha)")).thenReturn("different"::equals); + for (P search : new P[]{Text.contains("(alpha)"), + Text.contains("(alpha)").or(P.eq("beta")).and(P.neq("excluded"))}) { + for (GraphTraversal chain : new GraphTraversal[]{ + __.V().has("body", search).limit(10).hasLabel(P.neq("author")), + __.outE().has("body", search).limit(10).hasLabel(P.neq("knows"))}) { + Traversal.Admin traversal = chain.asAdmin(); + Step source; + if (traversal.getStartStep() instanceof GraphStep) { + HugeGraphStep step = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(step, traversal); + source = step; + } else { + HugeVertexStep step = replaceVertexStep(traversal); + TraversalUtil.extractHasContainer(step, traversal); + source = step; + } + Assert.assertTrue(((QueryHolder) source).getHasContainers().isEmpty()); + HasContainer local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertEquals(search, local.getPredicate()); + Assert.assertFalse(Text.contains("(alpha)").test("alpha")); + local = roundTrip(local); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + Assert.assertFalse(local.test(searchVertex(graph, "alphabet"))); + Assert.assertEquals(search instanceof ConnectiveP, + local.test(searchVertex(graph, "beta"))); + Assert.assertFalse(local.test(searchVertex(graph, "excluded"))); + HasContainer clone = local.clone(); + Assert.assertFalse(clone.test(searchVertex(other, "alpha"))); + Assert.assertTrue(clone.test(searchVertex(other, "different"))); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + } + } + } + + @Test + public void testPositiveLabelPushdownBeforeUnsafeChild() { + HugeGraph graph = positiveLabelGraph(); + for (P label : new P[]{P.eq("person"), P.within("person", "fan"), + P.within(Collections.emptyList())}) { + Traversal.Admin traversal = traversal(__.V().has(T.label, label).has("age", 18) + .where(__.hasLabel(P.neq("software"))), graph); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + Assert.assertEquals(1, source.getHasContainers().size()); + Assert.assertEquals(label, source.getHasContainers().get(0).getPredicate()); + Assert.assertFalse(hasStepExists(traversal, T.label.getAccessor())); + Assert.assertTrue(hasStepExists(traversal, "age")); + TraversalUtil.extractHasContainer(source, traversal); + Assert.assertEquals(1, source.getHasContainers().size()); + } + } + + @Test + public void testPositiveLabelPushdownDoesNotCrossRange() { + Traversal.Admin traversal = traversal(__.V().hasLabel("person").limit(2) + .hasLabel("fan").hasLabel(P.neq("software")), positiveLabelGraph()); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + Assert.assertEquals(1, source.getHasContainers().size()); + Assert.assertEquals(P.eq("person"), source.getHasContainers().get(0).getPredicate()); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); + } + + @Test + public void testPositiveLabelFallbackKeepsUnsupportedCandidatesLocal() { + HugeGraph graph = positiveLabelGraph(); + VertexLabel disabled = new VertexLabel(graph, IdGenerator.of(3L), "disabled"); + disabled.enableLabelIndex(false); + Mockito.when(graph.vertexLabel("disabled")).thenReturn(disabled); + Mockito.when(graph.vertexLabel("missing")) + .thenThrow(new IllegalArgumentException("Undefined vertex label")); + for (P label : new P[]{P.eq("disabled"), P.eq("missing"), + P.eq(IdGenerator.of(-1L)), P.within("person", IdGenerator.of(-1L)), + P.within("person", "missing"), P.eq(Collections.singletonList("person"))}) { + Traversal.Admin traversal = traversal(__.V().has(T.label, label) + .hasLabel(P.neq("software")), graph); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + Assert.assertTrue(source.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); + } + } + + private static HugeGraph positiveLabelGraph() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.vertexLabel("person")) + .thenReturn(new VertexLabel(graph, IdGenerator.of(1L), "person")); + Mockito.when(graph.vertexLabel("fan")) + .thenReturn(new VertexLabel(graph, IdGenerator.of(2L), "fan")); + return graph; + } + + @Test + public void testLocalSearchPreservesPredicateAndSerialization() throws Exception { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.searchPredicate("(alpha)")).thenReturn("alpha"::equals); + for (P predicate : new P[]{Text.contains("(alpha)"), + Text.contains("(alpha)").or(P.eq("beta")).and(P.neq("excluded"))}) { + Traversal.Admin traversal = traversal( + __.V().has("body", predicate).limit(10).hasLabel(P.neq("other")), graph); + HugeGraphStep source = replaceGraphStep(traversal); + HasContainer original = ((HasStep) source.getNextStep()).getHasContainers().get(0); + TraversalUtil.extractHasContainer(source, traversal); + HasContainer local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertEquals(predicate, local.getPredicate()); + Assert.assertEquals(original.hashCode(), local.hashCode()); + Assert.assertEquals(original.toString(), local.toString()); + HasContainer restored = roundTrip(local); + Assert.assertTrue(restored.test(searchVertex(graph, "alpha"))); + Assert.assertEquals(predicate instanceof ConnectiveP, + restored.test(searchVertex(graph, "beta"))); + Assert.assertFalse(restored.test(searchVertex(graph, "excluded"))); + // A warmed matcher must not serialize its graph/analyzer closure. + restored = roundTrip(restored); + Assert.assertEquals(predicate, restored.getPredicate()); + Assert.assertTrue(restored.test(searchVertex(graph, "alpha"))); + Assert.assertTrue(restored.clone().test(searchVertex(graph, "alpha"))); + + HugeGraph otherGraph = Mockito.mock(HugeGraph.class); + Mockito.when(otherGraph.searchPredicate("(alpha)")).thenReturn("different"::equals); + Assert.assertFalse(restored.test(searchVertex(otherGraph, "alpha"))); + Assert.assertTrue(restored.test(searchVertex(otherGraph, "different"))); + Assert.assertTrue(restored.clone().test(searchVertex(graph, "alpha"))); + } + } + + @Test + public void testLocalSearchSurvivesPredicateConversion() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("body")).thenReturn(propertyKey(1L, "body", DataType.TEXT)); + Mockito.when(graph.searchPredicate("(alpha)")).thenReturn("alpha"::equals); + Traversal.Admin traversal = traversal( + __.V().has("body", Text.contains("(alpha)")) + .limit(10).hasLabel(P.neq("other")), graph); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + HasStep filter = (HasStep) source.getNextStep(); + TraversalUtil.convHasStep(graph, filter); + Vertex vertex = searchVertex(graph, "alpha"); + Assert.assertTrue(HasContainer.testAll(vertex, filter.getHasContainers())); + Assert.assertFalse(HasContainer.testAll(searchVertex(graph, "(alpha)"), + filter.getHasContainers())); + } + + @Test + public void testNegatedSearchPreservesAnalysisMutationAndSerialization() throws Exception { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.searchPredicate("(alpha)")).thenReturn("alpha"::equals); + Mockito.when(graph.searchPredicate("(beta)")).thenReturn("beta"::equals); + Traversal.Admin traversal = traversal( + __.V().has("body", P.not(Text.contains("(alpha)"))) + .limit(10).hasLabel(P.neq("other")), graph); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + HasContainer local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertFalse(local.test(searchVertex(graph, "alpha"))); + Assert.assertTrue(local.test(searchVertex(graph, "(alpha)"))); + HasContainer clone = local.clone(); + @SuppressWarnings("unchecked") + P child = (P) ((NotP) local.getPredicate()).negate(); + child.setValue("(beta)"); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + Assert.assertFalse(local.test(searchVertex(graph, "beta"))); + Assert.assertFalse(clone.test(searchVertex(graph, "alpha"))); + HasContainer restored = roundTrip(local); + Assert.assertFalse(restored.test(searchVertex(graph, "beta"))); + Assert.assertTrue(restored.test(searchVertex(graph, "alpha"))); + + traversal = traversal(__.V().has("body", Text.contains("(alpha)") + .or(P.not(Text.contains("(alpha)")))) + .limit(10).hasLabel(P.neq("other")), graph); + source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + Assert.assertTrue(local.test(searchVertex(graph, "(alpha)"))); + } + + @Test + public void testNegatedLabelCollectionsResolveIdsAndNames() { + Id id = IdGenerator.of(12L); + HugeVertex vertex = Mockito.mock(HugeVertex.class); + Mockito.when(vertex.schemaLabel()).thenReturn(new VertexLabel(null, id, "person")); + for (Object candidate : new Object[]{id, id.asLong(), "person"}) { + P[] predicates = {P.not(P.within(candidate)), P.not(P.without(candidate)), + P.not(P.within(candidate).and(P.eq(candidate)))}; + boolean[] matches = {false, true, false}; + for (int i = 0; i < predicates.length; i++) { + Traversal.Admin traversal = __.V().has(T.label, predicates[i]).asAdmin(); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + HasContainer local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertEquals(matches[i], local.test(vertex)); + Assert.assertEquals(matches[i], local.clone().test(vertex)); + } + } + } + + private static HasContainer roundTrip(HasContainer container) throws Exception { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ObjectOutputStream output = new ObjectOutputStream(bytes)) { + output.writeObject(container); + } + try (ObjectInputStream input = new ObjectInputStream( + new ByteArrayInputStream(bytes.toByteArray()))) { + return (HasContainer) input.readObject(); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testLocalSearchMatcherTracksPredicateMutation() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.searchPredicate("(alpha)")).thenReturn("alpha"::equals); + Mockito.when(graph.searchPredicate("(beta)")).thenReturn("beta"::equals); + Traversal.Admin traversal = traversal( + __.V().has("body", Text.contains("(alpha)")) + .hasLabel(P.neq("other")), graph); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + HasContainer local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + Mockito.verify(graph, Mockito.times(1)).searchPredicate("(alpha)"); + HasContainer clone = local.clone(); + ((P) local.getPredicate()).setValue("(beta)"); + Assert.assertFalse(local.test(searchVertex(graph, "alpha"))); + Assert.assertTrue(local.test(searchVertex(graph, "beta"))); + Assert.assertTrue(clone.test(searchVertex(graph, "alpha"))); + Assert.assertFalse(clone.test(searchVertex(graph, "beta"))); + + traversal = traversal(__.V().has("body", Text.contains("(alpha)").or(P.eq("gamma"))) + .hasLabel(P.neq("other")), graph); + source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + ConnectiveP connective = (ConnectiveP) local.getPredicate(); + connective.getPredicates().get(0).setValue("(beta)"); + Assert.assertFalse(local.test(searchVertex(graph, "alpha"))); + Assert.assertTrue(local.test(searchVertex(graph, "beta"))); + Assert.assertTrue(local.test(searchVertex(graph, "gamma"))); + connective.or(P.eq("delta")); + Assert.assertTrue(local.test(searchVertex(graph, "delta"))); + } + + @SuppressWarnings("unchecked") + private static Vertex searchVertex(HugeGraph graph, String value) { + Vertex vertex = Mockito.mock(Vertex.class); + VertexProperty property = Mockito.mock(VertexProperty.class); + Mockito.when(vertex.graph()).thenReturn(graph); + Mockito.when(vertex.properties("body")) + .thenAnswer(ignored -> Collections.singletonList(property).iterator()); + Mockito.when(property.element()).thenReturn(vertex); + Mockito.when(property.value()).thenReturn(value); + return vertex; + } + + @Test + public void testLocalLabelIdWithoutGraphAndAfterClone() { + Id id = IdGenerator.of(12L); + Traversal.Admin traversal = __.V().has(T.label, id).limit(10) + .hasLabel(P.neq("other")).asAdmin(); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + TraversalUtil.convHasStep(null, (HasStep) source.getNextStep()); + HasContainer filter = ((HasStep) source.getNextStep()).getHasContainers().get(0); + HugeVertex vertex = Mockito.mock(HugeVertex.class); + Mockito.when(vertex.schemaLabel()).thenReturn(new VertexLabel(null, id, "v")); + Assert.assertTrue(filter.test(vertex)); + Assert.assertTrue(filter.clone().test(vertex)); + Mockito.when(vertex.schemaLabel()).thenReturn(new VertexLabel(null, IdGenerator.of(13L), "v")); + Assert.assertFalse(filter.test(vertex)); + Assert.assertFalse(filter.clone().test(vertex)); + } + + @Test + public void testSearchPredicateDefaultCompatibility() throws Exception { + Assert.assertTrue(HugeGraph.class.getMethod("searchPredicate", String.class).isDefault()); + HugeGraph graph = (HugeGraph) Proxy.newProxyInstance( + HugeGraph.class.getClassLoader(), new Class[]{HugeGraph.class}, + (proxy, method, args) -> MethodHandles.privateLookupIn( + method.getDeclaringClass(), MethodHandles.lookup()) + .unreflectSpecial(method, method.getDeclaringClass()) + .bindTo(proxy).invokeWithArguments(args)); + Assert.assertThrows(UnsupportedOperationException.class, () -> graph.searchPredicate("word")); + } + + @Test + public void testLocalContainsWithoutGraphAndAfterClone() { + Id key = IdGenerator.of(42L); + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("age")).thenReturn(propertyKey(42L, "age", DataType.INT)); + HugeVertex vertex = Mockito.mock(HugeVertex.class); + Mockito.when(vertex.graph()).thenReturn(graph); + Mockito.when(vertex.properties("key")).thenReturn(Collections.emptyIterator()); + Mockito.when(vertex.properties("value")).thenReturn(Collections.emptyIterator()); + BaseVertex baseVertex = Mockito.mock(BaseVertex.class); + Mockito.when(vertex.element()).thenReturn(baseVertex); + Mockito.when(baseVertex.sysprop(HugeKeys.PROPERTIES)) + .thenReturn(Collections.singletonMap(key, 20)); + for (GraphTraversal query : new GraphTraversal[]{ + __.V().hasKey("age").hasLabel(P.neq("other")), + __.V().hasValue(20).hasLabel(P.neq("other"))}) { + Traversal.Admin admin = query.asAdmin(); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + @SuppressWarnings("unchecked") + HasStep filter = (HasStep) source.getNextStep(); + @SuppressWarnings("unchecked") + Traverser.Admin traverser = Mockito.mock(Traverser.Admin.class); + Mockito.when(traverser.get()).thenReturn(vertex); + Mockito.when(traverser.bulk()).thenReturn(1L); + filter.addStart(traverser); + Assert.assertTrue(filter.hasNext()); + HasStep clone = filter.clone(); + clone.addStart(traverser); + Assert.assertTrue(clone.hasNext()); + } + } + + @Test + public void testLocalIdSurvivesPredicateConversionAcrossRange() { + for (P predicate : new P[]{P.eq("a").or(P.eq("b")), + P.within("a", "b").and(P.neq("b"))}) { + Traversal.Admin traversal = __.V().hasId(predicate) + .limit(10).hasLabel(P.neq("other")).asAdmin(); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + HasStep step = (HasStep) source.getNextStep(); + TraversalUtil.convHasStep(null, step); + HasContainer local = step.getHasContainers().get(0); + Vertex vertex = Mockito.mock(Vertex.class); + Mockito.when(vertex.id()).thenReturn(IdGenerator.of("a")); + Assert.assertTrue(local.test(vertex)); + Assert.assertTrue(local.clone().test(vertex)); + Mockito.when(vertex.id()).thenReturn(IdGenerator.of("z")); + Assert.assertFalse(local.test(vertex)); + } + } + + @Test + public void testUnsafeLabelKeepsPointLookupPlan() { + for (GraphTraversal query : new GraphTraversal[]{ + __.V().hasId(1).limit(10).hasLabel(P.neq("other")), + EmptyGraph.instance().traversal().E().hasId(1).limit(10).hasLabel(P.neq("other")), + __.V().hasId(P.within(1, 2)).hasLabel(P.neq("other"))}) { + Traversal.Admin admin = query.asAdmin(); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertTrue(source.getIds().length > 0); + Assert.assertEquals(1, source.getIds()[0]); + Assert.assertFalse(hasStepExists(admin, T.id.getAccessor())); + } + Traversal.Admin admin = __.V(1, 2).hasId(2) + .hasLabel(P.neq("other")).asAdmin(); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertEquals(2, source.getIds().length); + Assert.assertTrue(hasStepExists(admin, T.id.getAccessor())); + + admin = __.V().hasId(P.neq(1)).hasLabel(P.neq("other")).asAdmin(); + source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertEquals(0, source.getIds().length); + Assert.assertTrue(hasStepExists(admin, T.id.getAccessor())); + } + + @Test + public void testLabelAfterElementChangeKeepsSourceIndexPlan() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")).thenReturn(propertyKey(2L, "city", DataType.TEXT)); + for (GraphTraversal query : new GraphTraversal[]{ + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")), + __.V().has("city", "Beijing").in().hasLabel(P.neq("author")), + __.V().has("city", "Beijing").both().hasLabel(P.neq("author")), + __.V().has("city", "Beijing").outE().hasLabel(P.neq("knows")), + __.V().has("city", "Beijing").outE().hasLabel(P.neq("knows")).dedup(), + __.V().has("city", "Beijing").outE().hasLabel(P.neq("knows")) + .order().by("weight"), + __.V().has("city", "Beijing").inE().hasLabel(P.neq("knows")), + __.V().has("city", "Beijing").bothE().hasLabel(P.neq("knows")), + __.V().has("city", "Beijing").out().in().hasLabel(P.neq("author")), + __.V().has("city", "Beijing").both().both().hasLabel(P.neq("author")), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")).count(), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")).dedup(), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")) + .order().by("name"), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")) + .valueMap("name"), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")) + .elementMap("name"), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")).fold(), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")) + .groupCount(), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")) + .project("n").by("name"), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")).id(), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")).label(), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")).values("age").sum(), + __.V().has("city", "Beijing").out().where(__.not(__.hasLabel("author"))).count(), + __.V().has("city", "Beijing").out().where(__.not(__.hasLabel("author"))), + __.V().has("city", "Beijing").properties().hasLabel(P.neq("author"))}) { + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertTrue(hasContainer(source, "city")); + Assert.assertFalse(hasStepExists(admin, "city")); + } + for (GraphTraversal query : new GraphTraversal[]{ + __.V().has("city", "Beijing").as("a").out().select("a").hasLabel(P.neq("author")), + __.V().has("city", "Beijing").out().path().unfold().hasLabel(P.neq("author")), + __.V().has("city", "Beijing").as("a").out().hasLabel(P.neq("author")) + .dedup().by(__.select("a")), + __.V().has("city", "Beijing").as("a").out().hasLabel(P.neq("author")) + .order().by(__.select("a")), + __.V().has("city", "Beijing").as("a").out().hasLabel(P.neq("author")) + .project("n").by(__.select("a")), + __.V().has("city", "Beijing").out().filter(__.select("a").hasLabel(P.neq("author")))}) { + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertFalse(hasContainer(source, "city")); + Assert.assertTrue(hasStepExists(admin, "city")); + } + } + + @Test + public void testChildElementChangeKeepsSourceIndexPlan() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")) + .thenReturn(propertyKey(2L, "city", DataType.TEXT)); + for (GraphTraversal query : new GraphTraversal[]{ + __.V().has("city", "Beijing").where(__.out().hasLabel(P.neq("author"))), + __.V().has("city", "Beijing").where(__.out().in().hasLabel(P.neq("author"))), + __.V().has("city", "Beijing").where(__.both().both().hasLabel(P.neq("author"))), + __.V().has("city", "Beijing").where(__.outE().hasLabel(P.neq("knows"))), + __.V().has("city", "Beijing").where(__.inE().hasLabel(P.neq("knows"))), + __.V().has("city", "Beijing").where(__.bothE().hasLabel(P.neq("knows"))), + __.V().has("city", "Beijing").filter(__.out().hasLabel(P.neq("author"))), + __.V().has("city", "Beijing").not(__.out().hasLabel("author"))}) { + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertTrue(hasContainer(source, "city")); + Assert.assertFalse(hasStepExists(admin, "city")); + } + for (GraphTraversal query : new GraphTraversal[]{ + __.V().has("city", "Beijing").where(__.hasLabel(P.neq("author"))), + __.V().has("city", "Beijing").as("a") + .where(__.out().select("a").hasLabel(P.neq("author"))), + __.V().has("city", "Beijing") + .where(__.out().path().unfold().hasLabel(P.neq("author")))}) { + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertFalse(hasContainer(source, "city")); + Assert.assertTrue(hasStepExists(admin, "city")); + } + } + + @Test + public void testStandardSuffixesKeepRootAndChildSourceIndexPlan() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")) + .thenReturn(propertyKey(2L, "city", DataType.TEXT)); + for (GraphTraversal suffix : new GraphTraversal[]{ + __.tail(1), __.values("age").is(P.gt(1)), __.fold().unfold(), + __.constant(1), __.sample(2), __.sample(2).by("name"), + __.group().by(T.label), __.group().by(T.label).by(__.values("age").fold()), + __.aggregate("x"), __.aggregate("x").by("name"), + __.group("x").by(T.label), + __.groupCount("x").by(T.label), __.simplePath(), __.cyclicPath(), + __.simplePath().by("name"), __.coin(0.5d), __.timeLimit(100), + __.fold().tail(Scope.local, 1), __.fold().limit(Scope.local, 1), + __.fold().order(Scope.local), __.fold().dedup(Scope.local), + __.fold().sample(Scope.local, 1), __.fold().count(Scope.local), + __.values("age").fold().sum(Scope.local), + __.values("age").fold().min(Scope.local), + __.values("age").fold().max(Scope.local), + __.values("age").fold().mean(Scope.local), __.fold().index(), + __.sideEffect(__.values("name")), __.identity().profile("metrics")}) { + for (boolean child : new boolean[]{false, true}) { + GraphTraversal chain = __.out().hasLabel(P.neq("author")); + for (Step step : suffix.asAdmin().getSteps()) { + chain.asAdmin().addStep(step.clone()); + } + GraphTraversal query = __.V().has("city", "Beijing"); + if (child) { + query.where(chain); + } else { + for (Step step : chain.asAdmin().getSteps()) { + query.asAdmin().addStep(step.clone()); + } + } + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertTrue(query.toString(), hasContainer(source, "city")); + Assert.assertFalse(query.toString(), hasStepExists(admin, "city")); + } + } + } + + @Test + public void testHistoryAndOpaqueSuffixesKeepSourceFilterLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")) + .thenReturn(propertyKey(2L, "city", DataType.TEXT)); + for (GraphTraversal suffix : new GraphTraversal[]{ + __.group().by(__.select("a")), __.group().by(T.label).by(__.select("a")), + __.aggregate("x").by(__.select("a")), + __.sample(2).by(__.select("a")), __.simplePath().by(__.select("a")), + __.group("x").by(__.select("a")), __.groupCount("x").by(__.select("a")), + __.dedup("a"), __.select("a"), __.path().unfold(), __.tree(), + __.cap("x").unfold(), __.sack(), __.repeat(__.select("a")).times(1), + __.where(P.eq("a")), __.match(__.as("a").out().as("b")), + __.map(t -> t.path().get("a")), __.filter(t -> true), + __.fold(0, (a, b) -> a), __.sideEffect(t -> { })}) { + GraphTraversal query = __.V().has("city", "Beijing").as("a") + .out().hasLabel(P.neq("author")); + for (Step step : suffix.asAdmin().getSteps()) { + query.asAdmin().addStep(step.clone()); + } + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertFalse(query.toString(), hasContainer(source, "city")); + Assert.assertTrue(query.toString(), hasStepExists(admin, "city")); + } + } + + @Test + public void testUnknownHasStepSubclassKeepsSourceFilterLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")) + .thenReturn(propertyKey(2L, "city", DataType.TEXT)); + Traversal.Admin admin = traversal(__.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")), graph); + admin.addStep(new HasStep<>(admin, new HasContainer("name", P.eq("x"))) { }); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertFalse(hasContainer(source, "city")); + Assert.assertTrue(hasStepExists(admin, "city")); + } + + @Test + public void testFilterResultUsesSchemaLabelRepresentations() { + Id id = IdGenerator.of(7L); + HugeVertex vertex = Mockito.mock(HugeVertex.class); + Mockito.when(vertex.schemaLabel()).thenReturn(new VertexLabel(null, id, "person")); + for (P predicate : new P[]{P.eq("person"), P.eq(id), P.eq(id.asLong()), + P.within(id, "person"), P.neq("other")}) { + Assert.assertTrue(TraversalUtil.filterResult( + Collections.singletonList(new HasContainer(T.label.getAccessor(), predicate)), + Collections.singletonList(vertex).iterator()).hasNext()); + } + Assert.assertFalse(TraversalUtil.filterResult(Collections.singletonList( + new HasContainer(T.label.getAccessor(), P.eq("other"))), + Collections.singletonList(vertex).iterator()).hasNext()); + } + + @Test + public void testEdgeEndpointsKeepSourceLabelFallback() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")) + .thenReturn(propertyKey(2L, "city", DataType.TEXT)); + for (boolean child : new boolean[]{false, true}) { + for (GraphTraversal suffix : new GraphTraversal[]{ + __.outE().outV(), __.inE().inV(), __.bothE().bothV(), + __.outE().inV(), __.inE().outV(), + __.outE().barrier().outV(), + __.outE().filter(__.outV().hasLabel(P.neq("author")))}) { + suffix.hasLabel(P.neq("author")); + GraphTraversal query = __.V().has("city", "Beijing"); + if (child) { + query.where(suffix); + } else { + for (Step step : new ArrayList( + suffix.asAdmin().getSteps())) { + suffix.asAdmin().removeStep(step); + query.asAdmin().addStep(step); + } + } + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertFalse(hasContainer(source, "city")); + Assert.assertTrue(hasStepExists(admin, "city")); + } + } + } + + @Test + public void testChildSourceKeepsPropertyBeforeParentUnsafeLabel() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")).thenReturn(propertyKey(2L, "city", DataType.TEXT)); + GraphTraversal child = __.V().has("city", "Beijing"); + traversal(__.inject(1).union(child).hasLabel(P.neq("author")), graph); + Traversal.Admin admin = traversal(child, graph); + HugeGraphStep graphStep = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(graphStep, admin); + Assert.assertTrue(graphStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(admin, "city")); + + child = __.out().has("city", "Beijing"); + traversal(__.V().local(child).hasLabel(P.neq("author")), graph); + admin = traversal(child, graph); + HugeVertexStep vertexStep = replaceVertexStep(admin); + TraversalUtil.extractHasContainer(vertexStep, admin); + Assert.assertTrue(vertexStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(admin, "city")); + } + + @Test + public void testChildSourceStillExtractsWithoutDownstreamUnsafeLabel() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")).thenReturn(propertyKey(2L, "city", DataType.TEXT)); + GraphTraversal child = __.V().has("city", "Beijing"); + traversal(__.inject(1).union(child).hasLabel("person"), graph); + Traversal.Admin admin = traversal(child, graph); + HugeGraphStep graphStep = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(graphStep, admin); + Assert.assertTrue(hasContainer(graphStep, "city")); + Assert.assertFalse(hasStepExists(admin, "city")); + } + + @Test + public void testNotContextOnlyBlocksLabelDependentPushdown() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")).thenReturn(propertyKey(2L, "city", DataType.TEXT)); + Traversal.Admin traversal = traversal( + __.V().has("city", "Beijing").not(__.hasLabel("author")), graph); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + Assert.assertTrue(source.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "city")); + + traversal = traversal(__.V().has("city", "Beijing").not(__.has("name", "Tom")), graph); + source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + Assert.assertTrue(hasContainer(source, "city")); + Assert.assertFalse(hasStepExists(traversal, "city")); } @Test @@ -419,7 +1443,8 @@ private static void replaceStep(Step origin, Step newStep, TraversalHelper.replaceStep((Step) origin, (Step) newStep, traversal); } - private static boolean hasContainer(HugeGraphStep step, String key) { + private static boolean hasContainer(HasContainerHolder step, + String key) { for (HasContainer has : step.getHasContainers()) { if (key.equals(has.getKey())) { return true; @@ -429,12 +1454,17 @@ private static boolean hasContainer(HugeGraphStep step, String key) { } private static boolean hasStepExists(Traversal.Admin traversal) { + return countHasSteps(traversal) > 0; + } + + private static int countHasSteps(Traversal.Admin traversal) { + int count = 0; for (Step step : traversal.getSteps()) { if (step instanceof HasStep) { - return true; + count++; } } - return false; + return count; } private static boolean hasStepExists(Traversal.Admin traversal, diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/BaseUnitTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/BaseUnitTest.java index b6dc0bf96a..5be6c3a5e0 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/BaseUnitTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/BaseUnitTest.java @@ -26,7 +26,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.util.Log; import org.apache.hugegraph.util.TimeUtil; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/FakeObjects.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/FakeObjects.java index 9dabd72a3a..e4bd85f578 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/FakeObjects.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/FakeObjects.java @@ -22,17 +22,17 @@ import org.apache.commons.configuration2.Configuration; import org.apache.commons.configuration2.PropertiesConfiguration; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.config.HugeConfig; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.config.TypedOption; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; -import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; @@ -53,6 +53,10 @@ public FakeObjects(String name) { public FakeObjects() { this.graph = Mockito.mock(HugeGraph.class); Mockito.doReturn(newConfig()).when(this.graph).configuration(); + Mockito.doAnswer(invocation -> { + TypedOption option = invocation.getArgument(0); + return this.graph.configuration().get(option); + }).when(this.graph).option(Mockito.any()); Mockito.doReturn(true).when(this.graph).sameAs(this.graph); } @@ -159,8 +163,7 @@ public HugeEdge newEdge(long sourceVertexId, long targetVertexId) { Id id = EdgeId.parse("L123456>1>1>>L987654"); HugeEdge edge = new HugeEdge(this.graph(), id, el); - Whitebox.setInternalState(edge, "sourceVertex", source); - Whitebox.setInternalState(edge, "targetVertex", target); + edge.vertices(true, source, target); edge.assignId(); edge.addProperty(date, new Date()); edge.addProperty(weight, 0.75); @@ -202,8 +205,7 @@ public HugeEdge newEdge(String sourceVertexId, String targetVertexId) { Id id = EdgeId.parse("L123456>1>1>>L987654"); HugeEdge edge = new HugeEdge(this.graph(), id, el); - Whitebox.setInternalState(edge, "sourceVertex", source); - Whitebox.setInternalState(edge, "targetVertex", target); + edge.vertices(true, source, target); edge.assignId(); edge.addProperty(date, new Date()); edge.addProperty(weight, 0.75); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java index 0e010ae5f7..448669b95a 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -17,23 +17,41 @@ package org.apache.hugegraph.unit; +import org.apache.hugegraph.HugeFactoryTest; +import org.apache.hugegraph.unit.core.LoadingModeTest; +import org.apache.hugegraph.GraphDropPendingTest; +import org.apache.hugegraph.StandardTaskAdmissionTest; +import org.apache.hugegraph.DistributedTaskAdmissionTest; +import org.apache.hugegraph.api.arthas.ArthasConfigTest; import org.apache.hugegraph.api.auth.GraphSpaceAuthPayloadTest; import org.apache.hugegraph.api.auth.GraphSpaceGroupAPITest; +import org.apache.hugegraph.api.cypher.CypherClientTest; +import org.apache.hugegraph.auth.ContextGremlinServerHttpTest; +import org.apache.hugegraph.auth.HttpGremlinRequestHandlerTest; +import org.apache.hugegraph.auth.ContextTaskTest; import org.apache.hugegraph.auth.StandardAuthManagerV2Test; import org.apache.hugegraph.auth.WsAndHttpBasicAuthHandlerTest; import org.apache.hugegraph.backend.page.QueryListTest; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBIteratorPoolTest; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBSnapshotRestoreTest; import org.apache.hugegraph.backend.tx.GraphIndexTransactionTest; import org.apache.hugegraph.backend.tx.GraphTransactionTest; -import org.apache.hugegraph.core.RoleElectionStateMachineTest; +import org.apache.hugegraph.core.memory.MemoryManageTest; import org.apache.hugegraph.meta.EtcdMetaDriverTest; import org.apache.hugegraph.meta.MetaManagerSchemaCacheClearEventTest; import org.apache.hugegraph.meta.managers.AuthMetaManagerTest; +import org.apache.hugegraph.opencypher.CypherOpProcessorTest; import org.apache.hugegraph.store.client.OrderedScanSecurityTest; +import org.apache.hugegraph.tinkerpop.HugeGraphTestInfrastructureTest; import org.apache.hugegraph.traversal.optimize.TraversalUtilOptimizeTest; import org.apache.hugegraph.unit.api.auth.LoginAPITest; +import org.apache.hugegraph.unit.api.cypher.CypherErrorTest; +import org.apache.hugegraph.unit.api.cypher.CypherTemporalFunctionTest; import org.apache.hugegraph.unit.api.filter.AccessLogFilterTest; +import org.apache.hugegraph.unit.api.filter.CompressInterceptorTest; import org.apache.hugegraph.unit.api.filter.LoadDetectFilterTest; import org.apache.hugegraph.unit.api.filter.PathFilterTest; +import org.apache.hugegraph.unit.api.filter.VertexAuthorizationTest; import org.apache.hugegraph.unit.api.gremlin.GremlinQueryAPITest; import org.apache.hugegraph.unit.api.space.GraphSpaceAPITest; import org.apache.hugegraph.unit.api.space.SchemaTemplateAPITest; @@ -43,9 +61,15 @@ import org.apache.hugegraph.unit.cache.CachedGraphTransactionTest; import org.apache.hugegraph.unit.cache.CachedSchemaTransactionTest; import org.apache.hugegraph.unit.cache.RamTableTest; +import org.apache.hugegraph.unit.cache.RequestCacheLifetimeTest; import org.apache.hugegraph.unit.cmd.InitStoreConfigTest; +import org.apache.hugegraph.unit.config.GremlinConfigCompatibilityTest; +import org.apache.hugegraph.unit.config.GraphBinarySchemaCompatibilityTest; +import org.apache.hugegraph.unit.config.K8sResourceQuotaYamlTest; import org.apache.hugegraph.unit.core.AnalyzerTest; +import org.apache.hugegraph.unit.core.BackendSessionPoolTest; import org.apache.hugegraph.unit.core.BackendMutationTest; +import org.apache.hugegraph.unit.core.BackendProviderFactoryTest; import org.apache.hugegraph.unit.core.BackendStoreInfoTest; import org.apache.hugegraph.unit.core.ConditionQueryFlattenTest; import org.apache.hugegraph.unit.core.ConditionTest; @@ -56,8 +80,16 @@ import org.apache.hugegraph.unit.core.DirectionsTest; import org.apache.hugegraph.unit.core.ExceptionTest; import org.apache.hugegraph.unit.core.GraphManagerAdminInitTest; +import org.apache.hugegraph.unit.core.GraphManagerClearGraphSpaceTest; import org.apache.hugegraph.unit.core.GraphManagerConfigTest; +import org.apache.hugegraph.unit.core.GraphManagerLocalGraphBindTest; +import org.apache.hugegraph.unit.core.GraphManagerServiceNamesTest; +import org.apache.hugegraph.unit.core.GroovyScriptEngineCompatibilityTest; +import org.apache.hugegraph.unit.core.HugeFeaturesTest; import org.apache.hugegraph.unit.core.HstoreSessionsTest; +import org.apache.hugegraph.unit.core.HstoreTableRoutingTest; +import org.apache.hugegraph.unit.core.HugeElementViewTest; +import org.apache.hugegraph.unit.core.HugePrimaryKeyTest; import org.apache.hugegraph.unit.core.IdHolderTest; import org.apache.hugegraph.unit.core.LocksTableTest; import org.apache.hugegraph.unit.core.PageStateTest; @@ -67,6 +99,7 @@ import org.apache.hugegraph.unit.core.RolePermissionTest; import org.apache.hugegraph.unit.core.RowLockTest; import org.apache.hugegraph.unit.core.SchemaElementTest; +import org.apache.hugegraph.unit.core.SchemaTaskSecurityTest; import org.apache.hugegraph.unit.core.SecurityManagerTest; import org.apache.hugegraph.unit.core.SerialEnumTest; import org.apache.hugegraph.unit.core.ServerInfoManagerTest; @@ -78,23 +111,29 @@ import org.apache.hugegraph.unit.id.IdTest; import org.apache.hugegraph.unit.id.IdUtilTest; import org.apache.hugegraph.unit.id.SplicingIdGeneratorTest; +import org.apache.hugegraph.unit.opencypher.CypherGremlinPredicatesTest; import org.apache.hugegraph.unit.rocksdb.RocksDBCountersTest; import org.apache.hugegraph.unit.rocksdb.RocksDBSessionTest; import org.apache.hugegraph.unit.rocksdb.RocksDBSessionsTest; +import org.apache.hugegraph.unit.rocksdb.RocksDBStdSessionsLifecycleTest; import org.apache.hugegraph.unit.rocksdb.RocksDBTableQueryByIdsTest; import org.apache.hugegraph.unit.serializer.BinaryBackendEntryTest; import org.apache.hugegraph.unit.serializer.BinaryScatterSerializerTest; import org.apache.hugegraph.unit.serializer.BinarySerializerTest; import org.apache.hugegraph.unit.serializer.BytesBufferTest; +import org.apache.hugegraph.unit.serializer.HugeGraphSONModuleTest; import org.apache.hugegraph.unit.serializer.SerializerFactoryTest; import org.apache.hugegraph.unit.serializer.StoreSerializerTest; import org.apache.hugegraph.unit.serializer.TableBackendEntryTest; import org.apache.hugegraph.unit.serializer.TextBackendEntryTest; import org.apache.hugegraph.unit.serializer.TextSerializerTest; import org.apache.hugegraph.unit.store.RamIntObjectMapTest; +import org.apache.hugegraph.unit.traversal.ConcurrentVerticesConsumerTest; import org.apache.hugegraph.unit.traversal.ShortestPathTraverserTest; import org.apache.hugegraph.unit.util.CompressUtilTest; +import org.apache.hugegraph.unit.util.ConsumersTest; import org.apache.hugegraph.unit.util.JsonUtilTest; +import org.apache.hugegraph.unit.util.MetricsUtilTest; import org.apache.hugegraph.unit.util.RateLimiterTest; import org.apache.hugegraph.unit.util.StringEncodingTest; import org.apache.hugegraph.unit.util.VersionTest; @@ -111,16 +150,34 @@ @Suite.SuiteClasses({ /* api filter */ AccessLogFilterTest.class, + CompressInterceptorTest.class, LoadDetectFilterTest.class, LoginAPITest.class, PathFilterTest.class, + ArthasConfigTest.class, + VertexAuthorizationTest.class, + + /* api cypher */ + CypherErrorTest.class, + CypherTemporalFunctionTest.class, /* api gremlin */ GremlinQueryAPITest.class, + CypherClientTest.class, + CypherGremlinPredicatesTest.class, + CypherOpProcessorTest.class, WsAndHttpBasicAuthHandlerTest.class, + HttpGremlinRequestHandlerTest.class, + ContextGremlinServerHttpTest.class, GraphSpaceGroupAPITest.class, GraphSpaceAuthPayloadTest.class, StandardAuthManagerV2Test.class, + ContextTaskTest.class, + HugeFactoryTest.class, + LoadingModeTest.class, + GraphDropPendingTest.class, + StandardTaskAdmissionTest.class, + DistributedTaskAdmissionTest.class, AuthMetaManagerTest.class, /* api space */ @@ -135,6 +192,7 @@ MetaManagerSchemaCacheClearEventTest.class, EtcdMetaDriverTest.class, CachedGraphTransactionTest.class, + RequestCacheLifetimeTest.class, CacheManagerTest.class, RamTableTest.class, @@ -157,7 +215,11 @@ RowLockTest.class, AnalyzerTest.class, BackendMutationTest.class, + BackendSessionPoolTest.class, + BackendProviderFactoryTest.class, ConditionTest.class, + GroovyScriptEngineCompatibilityTest.class, + HugeFeaturesTest.class, StandardHugeGraphClearBackendTest.class, ConditionQueryFlattenTest.class, GraphIndexTransactionTest.class, @@ -167,12 +229,17 @@ QueryListTest.class, RangeTest.class, SecurityManagerTest.class, + SchemaTaskSecurityTest.class, OrderedScanSecurityTest.class, RolePermissionTest.class, ExceptionTest.class, GraphManagerAdminInitTest.class, + GraphManagerClearGraphSpaceTest.class, GraphManagerConfigTest.class, + GraphManagerLocalGraphBindTest.class, + GraphManagerServiceNamesTest.class, HstoreSessionsTest.class, + HstoreTableRoutingTest.class, BackendStoreInfoTest.class, TraversalUtilTest.class, TraversalUtilOptimizeTest.class, @@ -181,10 +248,13 @@ SystemSchemaStoreTest.class, ServerInfoManagerTest.class, TaskSchedulerServerInfoTest.class, - RoleElectionStateMachineTest.class, HugeGraphAuthProxyTest.class, SchemaElementTest.class, + HugeGraphTestInfrastructureTest.class, + HugeElementViewTest.class, + HugePrimaryKeyTest.class, ShortestPathTraverserTest.class, + ConcurrentVerticesConsumerTest.class, /* cmd */ InitStoreConfigTest.class, @@ -197,20 +267,33 @@ BinaryBackendEntryTest.class, BinarySerializerTest.class, BinaryScatterSerializerTest.class, + HugeGraphSONModuleTest.class, StoreSerializerTest.class, TextSerializerTest.class, + /* config */ + GremlinConfigCompatibilityTest.class, + GraphBinarySchemaCompatibilityTest.class, + K8sResourceQuotaYamlTest.class, /* rocksdb */ RocksDBSessionsTest.class, + RocksDBSnapshotRestoreTest.class, RocksDBSessionTest.class, RocksDBCountersTest.class, RocksDBTableQueryByIdsTest.class, + RocksDBStdSessionsLifecycleTest.class, + RocksDBIteratorPoolTest.class, + + /* memory */ + MemoryManageTest.class, /* utils */ + ConsumersTest.class, VersionTest.class, JsonUtilTest.class, StringEncodingTest.class, CompressUtilTest.class, + MetricsUtilTest.class, RateLimiterTest.FixedTimerWindowRateLimiterTest.class, RateLimiterTest.FixedWatchWindowRateLimiterTest.class, diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/cypher/CypherErrorTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/cypher/CypherErrorTest.java new file mode 100644 index 0000000000..f8c59e1427 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/cypher/CypherErrorTest.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.hugegraph.unit.api.cypher; + +import java.util.Collections; +import java.util.Locale; +import java.util.concurrent.ExecutionException; + +import org.apache.hugegraph.api.cypher.CypherErrorMapper; +import org.apache.hugegraph.api.cypher.CypherModel; +import org.apache.hugegraph.traversal.optimize.TraversalUtil; +import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer; +import org.apache.tinkerpop.gremlin.driver.exception.ResponseException; +import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode; +import org.junit.Assert; +import org.junit.Test; +import org.opencypher.gremlin.translation.CypherAst; +import org.opencypher.gremlin.translation.translator.Translator; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class CypherErrorTest { + + private static final ObjectMapper JSON = new ObjectMapper(); + + @Test + public void testSuccessEnvelope() throws Exception { + JsonNode response = JSON.readTree(JSON.writeValueAsString( + CypherModel.dataOf("request", Collections.singletonList("ok")))); + Assert.assertEquals(3, response.size()); + Assert.assertFalse(response.has("errors")); + Assert.assertEquals("request", response.get("requestId").asText()); + Assert.assertEquals(200, response.at("/status/code").asInt()); + Assert.assertEquals(0, response.at("/status/attributes").size()); + Assert.assertEquals("ok", response.at("/result/data/0").asText()); + } + + @Test + public void testFailureEnvelope() throws Exception { + CypherModel.CypherError error = CypherErrorMapper.map( + new IllegalArgumentException("Invalid input 'R'")); + JsonNode response = JSON.readTree(JSON.writeValueAsString( + CypherModel.failOf("request", "original message", error))); + Assert.assertEquals(3, response.size()); + Assert.assertFalse(response.has("errors")); + Assert.assertEquals(400, response.at("/status/code").asInt()); + Assert.assertEquals("original message", response.at("/status/message").asText()); + Assert.assertTrue(response.at("/result/data").isNull()); + JsonNode metadata = response.at("/status/attributes/errors/0"); + Assert.assertEquals("HugeGraph.Cypher.SyntaxError", metadata.get("code").asText()); + Assert.assertEquals(error.message, metadata.get("message").asText()); + Assert.assertEquals(error.hint, metadata.get("hint").asText()); + Assert.assertFalse(error.hint.isEmpty()); + } + + @Test + public void testFailureWithoutMetadata() throws Exception { + JsonNode response = JSON.readTree(JSON.writeValueAsString( + CypherModel.failOf("request", "failure", null))); + Assert.assertEquals(3, response.size()); + Assert.assertEquals(0, response.at("/status/attributes").size()); + Assert.assertEquals(400, response.at("/status/code").asInt()); + } + + @Test + public void testLegacyFailureFactory() throws Exception { + JsonNode response = JSON.readTree(JSON.writeValueAsString( + CypherModel.failOf("legacy-request", "legacy failure"))); + Assert.assertEquals(3, response.size()); + Assert.assertFalse(response.has("errors")); + Assert.assertEquals("legacy-request", response.get("requestId").asText()); + Assert.assertEquals(400, response.at("/status/code").asInt()); + Assert.assertEquals("legacy failure", response.at("/status/message").asText()); + Assert.assertEquals(0, response.at("/status/attributes").size()); + Assert.assertTrue(response.at("/result/data").isNull()); + Assert.assertEquals(0, response.at("/result/meta").size()); + } + + @Test + public void testRealParserFailures() { + String[] queries = { + "MATCH (n:person RETURN n", + "MATCH (n:person) RETRUN n", + "MATCH (n) RETURN n; MATCH (m) RETURN m", + "RETURN duration('P1Y')", + "MATCH (n:person) RETURN not_defined_var", + "MATCH (n) RETURN n.age + 'a' * true", + "CREATE (n) CREATE (n) RETURN n", + "MATCH (n) WITH n.name RETURN n", + "MATCH (n) RETURN 1 AS a, 2 AS a" + }; + for (String query : queries) { + Exception failure = Assert.assertThrows(Exception.class, () -> translate(query)); + // Gremlin's response exception loses the parser's exception type. + CypherModel.CypherError error = CypherErrorMapper.map( + new ExecutionException(new ResponseException( + ResponseStatusCode.SERVER_ERROR, failure.getMessage()))); + Assert.assertEquals(query, "HugeGraph.Cypher.SyntaxError", error.code); + Assert.assertEquals(query, failure.getMessage(), error.message); + Assert.assertFalse(query, error.hint.isEmpty()); + Assert.assertEquals(query, query.contains("not_defined_var"), + error.hint.startsWith("Declare the variable")); + } + } + + @Test + public void testSchemaFailuresAreNotVariableErrors() { + for (String type : new String[]{"vertex label", "edge label", "property key", + "index label"}) { + CypherModel.CypherError error = CypherErrorMapper.map( + new IllegalArgumentException("Undefined " + type + ": 'x'")); + Assert.assertEquals("HugeGraph.Cypher.ExecutionError", error.code); + Assert.assertTrue(error.hint.startsWith("Create the schema element")); + } + Assert.assertEquals("HugeGraph.Cypher.ExecutionError", CypherErrorMapper.map( + new IllegalArgumentException("Resource not defined")).code); + } + + @Test + public void testUnsupportedAndFallback() { + for (String message : new String[]{"Feature not supported", "Unsupported construct"}) { + CypherModel.CypherError error = CypherErrorMapper.map(new UnsupportedOperationException(message)); + Assert.assertEquals("HugeGraph.Cypher.ExecutionError", error.code); + Assert.assertEquals(message, error.message); + Assert.assertEquals("", error.hint); + } + CypherModel.CypherError fallback = CypherErrorMapper.map(new RuntimeException()); + Assert.assertEquals("HugeGraph.Cypher.ExecutionError", fallback.code); + Assert.assertEquals("", fallback.hint); + Assert.assertFalse(fallback.message.isEmpty()); + } + + @Test + public void testTranslatorWithoutRequestValidationLeavesParametersUnbound() { + String gremlin = translate("MATCH (n:person) WHERE n.name = $name RETURN n"); + Assert.assertTrue(gremlin, gremlin.contains("cypher.null")); + } + + @Test + public void testClassificationIsLocaleIndependent() { + Locale previous = Locale.getDefault(); + try { + Locale.setDefault(Locale.forLanguageTag("tr-TR")); + Assert.assertEquals("HugeGraph.Cypher.SyntaxError", CypherErrorMapper.map( + new IllegalArgumentException("INVALID INPUT 'R'")).code); + } finally { + Locale.setDefault(previous); + } + } + + @Test + public void testRealUnsupportedMatchPattern() { + Exception failure = Assert.assertThrows(Exception.class, () -> translate( + "MATCH p=shortestPath((a)-[*]->(b)) RETURN p")); + Assert.assertTrue(failure.getMessage(), failure.getMessage().startsWith("Unsupported match pattern:")); + CypherModel.CypherError error = transportError(failure); + Assert.assertEquals("HugeGraph.Cypher.UnsupportedFeature", error.code); + Assert.assertEquals(failure.getMessage(), error.message); + Assert.assertTrue(error.hint.startsWith("This construct is not covered")); + } + + @Test + public void testRuntimeUnsupportedErrorsAreNotTranslatorGaps() { + Exception contains = Assert.assertThrows(IllegalArgumentException.class, + () -> TraversalUtil.convContains2Relation(null, new HasContainer("key", P.neq("value")))); + for (Exception failure : new Exception[]{TraversalUtil.newUnsupportedPredicate(P.eq("value")), contains}) { + CypherModel.CypherError error = transportError(failure); + Assert.assertEquals("HugeGraph.Cypher.ExecutionError", error.code); + Assert.assertEquals(failure.getMessage(), error.message); + Assert.assertEquals("", error.hint); + } + } + + @Test + public void testMissingParameterRemainsExecutionFallback() { + CypherModel.CypherError error = transportError( + new IllegalArgumentException("Missing Cypher parameter: missing")); + Assert.assertEquals("HugeGraph.Cypher.ExecutionError", error.code); + Assert.assertEquals("Missing Cypher parameter: missing", error.message); + Assert.assertEquals("", error.hint); + } + + private static CypherModel.CypherError transportError(Exception failure) { + return CypherErrorMapper.map(new ExecutionException( + new ResponseException(ResponseStatusCode.SERVER_ERROR, failure.getMessage()))); + } + + private static String translate(String query) { + return CypherAst.parse(query, Collections.emptyMap()).buildTranslation( + Translator.builder().gremlinGroovy() + .build("gremlin+cfog_server_extensions+inline_parameters")); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/cypher/CypherTemporalFunctionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/cypher/CypherTemporalFunctionTest.java new file mode 100644 index 0000000000..6f63c79f72 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/cypher/CypherTemporalFunctionTest.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.hugegraph.unit.api.cypher; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import org.apache.hugegraph.opencypher.CypherTemporalFunctions; +import org.junit.Assert; +import org.junit.Test; +import org.opencypher.gremlin.translation.CypherAst; +import org.opencypher.gremlin.translation.groovy.GroovyPredicate; +import org.opencypher.gremlin.translation.ir.TranslationWriter; +import org.opencypher.gremlin.translation.ir.model.GremlinStep; +import org.opencypher.gremlin.translation.translator.Translator; +import org.opencypher.gremlin.traversal.ProcedureContext; +import scala.collection.Seq; + +public class CypherTemporalFunctionTest { + + @Test + public void testDateLiteralFoldsInPropertyMatch() throws Exception { + Map.Entry> folded = translate( + "MATCH (n {d0: date('2024-02-05')}) RETURN count(*)", + Collections.emptyMap()); + + Assert.assertTrue(folded.getKey(), folded.getKey().contains("g.V()")); + Assert.assertEquals(expectedDate("yyyy-MM-dd", "2024-02-05"), + syntheticBinding(folded.getValue(), " cypher.fold.date.")); + } + + @Test + public void testBareDateFoldsToStartOfToday() throws Exception { + Map.Entry> folded = + translate("RETURN date()", Collections.emptyMap()); + + Assert.assertEquals(Date.from(LocalDate.now() + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()), + syntheticBinding(folded.getValue(), " cypher.fold.date.")); + } + + @Test + public void testDateWithParameterArgumentFolds() throws Exception { + Map parameters = new HashMap<>(); + parameters.put("d", "2024-02-05"); + Map.Entry> folded = translate( + "MATCH (n) WHERE n.d0 = date($d) RETURN count(*)", parameters); + + Assert.assertEquals(expectedDate("yyyy-MM-dd", "2024-02-05"), + syntheticBinding(folded.getValue(), " cypher.fold.date.")); + Assert.assertEquals("2024-02-05", folded.getValue().get("d")); + } + + @Test + public void testDatetimeAndLocaldatetimeFold() throws Exception { + Map.Entry> datetime = translate( + "RETURN datetime('2024-02-05T10:15:30')", Collections.emptyMap()); + Assert.assertEquals(expectedDate("yyyy-MM-dd'T'HH:mm:ss", "2024-02-05T10:15:30"), + syntheticBinding(datetime.getValue(), " cypher.fold.datetime.")); + + Map.Entry> localdatetime = translate( + "RETURN localdatetime('2024-02-05T10:15:30.123')", Collections.emptyMap()); + Assert.assertEquals(expectedDate("yyyy-MM-dd'T'HH:mm:ss.SSS", "2024-02-05T10:15:30.123"), + syntheticBinding(localdatetime.getValue(), " cypher.fold.localdatetime.")); + } + + @Test + public void testFractionalSecondsFoldAsFractionOfSecond() throws Exception { + Map.Entry> datetime = translate( + "RETURN datetime('2024-02-05T10:15:30.5')", Collections.emptyMap()); + Assert.assertEquals(expectedDate(2024, 2, 5, 10, 15, 30, 500), + syntheticBinding(datetime.getValue(), " cypher.fold.datetime.")); + + Map.Entry> localdatetime = translate( + "RETURN localdatetime('2024-02-05T10:15:30.12')", Collections.emptyMap()); + Assert.assertEquals(expectedDate(2024, 2, 5, 10, 15, 30, 120), + syntheticBinding(localdatetime.getValue(), + " cypher.fold.localdatetime.")); + } + + @Test + public void testDateInsideAnotherCallFolds() throws Exception { + Map.Entry> folded = translate( + "RETURN coalesce(date('2024-02-05'), 'x')", Collections.emptyMap()); + + Assert.assertNotNull(folded.getKey()); + Assert.assertEquals(expectedDate("yyyy-MM-dd", "2024-02-05"), + syntheticBinding(folded.getValue(), " cypher.fold.date.")); + } + + @Test + public void testDateInCreatePropertyFolds() throws Exception { + Map.Entry> folded = translate( + "CREATE (n:person {birthday: date('2000-01-01')}) RETURN n", + Collections.emptyMap()); + + Assert.assertNotNull(folded.getKey()); + Assert.assertEquals(expectedDate("yyyy-MM-dd", "2000-01-01"), + syntheticBinding(folded.getValue(), " cypher.fold.date.")); + } + + @Test + public void testDateInLabelledMatchWhereFolds() throws Exception { + Map.Entry> folded = translate( + "MATCH (n:cypher_person) WHERE n.birthday = date('2000-01-01') RETURN n.name AS name", + Collections.emptyMap()); + + Assert.assertTrue(folded.getKey(), folded.getKey().contains("g.V()")); + Assert.assertEquals(expectedDate("yyyy-MM-dd", "2000-01-01"), + syntheticBinding(folded.getValue(), " cypher.fold.date.")); + } + + @Test + public void testDateInLabelledPropertyMapFolds() throws Exception { + Map.Entry> folded = translate( + "MATCH (n:cypher_person {note: date('2024-02-05')}) RETURN count(n) AS count", + Collections.emptyMap()); + + Assert.assertTrue(folded.getKey(), folded.getKey().contains("g.V()")); + Assert.assertEquals(expectedDate("yyyy-MM-dd", "2024-02-05"), + syntheticBinding(folded.getValue(), " cypher.fold.date.")); + } + + @Test + public void testDateUnderAndRangeFolds() throws Exception { + Map.Entry> folded = translate( + "MATCH (n) WHERE n.d0 > date('2024-02-05') AND n.d0 < date('2025-01-01') RETURN n", + Collections.emptyMap()); + + List foldedDates = new ArrayList<>(); + for (Map.Entry entry : folded.getValue().entrySet()) { + if (entry.getKey().startsWith(" cypher.fold.date.")) { + foldedDates.add(entry.getValue()); + } + } + Assert.assertEquals(2, foldedDates.size()); + Assert.assertTrue(foldedDates.remove(expectedDate("yyyy-MM-dd", "2024-02-05"))); + Assert.assertTrue(foldedDates.remove(expectedDate("yyyy-MM-dd", "2025-01-01"))); + } + + @Test + public void testUnfoldableDateStaysUnknownFunction() { + for (String query : new String[]{"MATCH (n) RETURN date(n.x)", + "RETURN date('tomorrow')", + "RETURN date('2024-2-5')"}) { + Exception failure = Assert.assertThrows(Exception.class, + () -> translate(query, Collections.emptyMap())); + Assert.assertTrue(query, failure.getMessage().contains("Unknown function 'date'")); + } + } + + @Test + public void testDurationStaysUnknownFunction() { + Exception failure = Assert.assertThrows(Exception.class, + () -> translate("RETURN duration('P1Y')", Collections.emptyMap())); + Assert.assertTrue(failure.getMessage(), failure.getMessage() + .contains("Unknown function 'duration'")); + } + + @Test + public void testQueryWithoutTemporalFunctionsKeepsParameters() { + Map parameters = new HashMap<>(); + parameters.put("name", "marko"); + Map.Entry> folded = translate( + "MATCH (n) WHERE n.name = $name RETURN n", parameters); + + Assert.assertSame(parameters, folded.getValue()); + Assert.assertTrue(folded.getKey(), folded.getKey().contains("marko")); + } + + @Test + public void testUpperAndMixedCaseConstructorsFold() throws Exception { + Map.Entry> upper = translate( + "MATCH (n) WHERE n.d0 = DATE('2000-01-01') RETURN n", + Collections.emptyMap()); + Assert.assertEquals(expectedDate("yyyy-MM-dd", "2000-01-01"), + syntheticBinding(upper.getValue(), " cypher.fold.date.")); + + Map.Entry> mixed = translate( + "RETURN DateTime('2024-02-05T10:15:30')", Collections.emptyMap()); + Assert.assertEquals(expectedDate("yyyy-MM-dd'T'HH:mm:ss", "2024-02-05T10:15:30"), + syntheticBinding(mixed.getValue(), " cypher.fold.datetime.")); + + Map.Entry> local = translate( + "RETURN localDateTime('2024-02-05T10:15:30')", Collections.emptyMap()); + Assert.assertEquals(expectedDate("yyyy-MM-dd'T'HH:mm:ss", "2024-02-05T10:15:30"), + syntheticBinding(local.getValue(), " cypher.fold.localdatetime.")); + } + + @Test + public void testSyntheticNameSkipsOccupiedLiteralBinding() throws Exception { + Map parameters = new HashMap<>(); + parameters.put(" cypher.fold.date.0", "original"); + Map.Entry> folded = translate( + "RETURN date('2024-02-05') AS d, $` cypher.fold.date.0` AS other", parameters); + + Assert.assertEquals("original", folded.getValue().get(" cypher.fold.date.0")); + Assert.assertEquals(expectedDate("yyyy-MM-dd", "2024-02-05"), + folded.getValue().get(" cypher.fold.date.1")); + Assert.assertTrue(folded.getKey(), folded.getKey().contains("original")); + } + + @Test + public void testSyntheticNameSkipsOccupiedParameterBinding() throws Exception { + Map parameters = new HashMap<>(); + parameters.put("d", "2024-02-05"); + parameters.put(" cypher.fold.date.0", "original"); + Map.Entry> folded = translate( + "RETURN date($d) AS d, $` cypher.fold.date.0` AS other", parameters); + + Assert.assertEquals("original", folded.getValue().get(" cypher.fold.date.0")); + Assert.assertEquals(expectedDate("yyyy-MM-dd", "2024-02-05"), + folded.getValue().get(" cypher.fold.date.1")); + Assert.assertEquals("2024-02-05", folded.getValue().get("d")); + } + + @Test + public void testBareConstructorsShareOneInstant() throws Exception { + Map.Entry> folded = translate( + "CREATE (a {t: datetime()}), (b {t: datetime()}) RETURN count(*)", + Collections.emptyMap()); + + List values = new ArrayList<>(); + for (Map.Entry entry : folded.getValue().entrySet()) { + if (entry.getKey().startsWith(" cypher.fold.datetime.")) { + values.add((Date) entry.getValue()); + } + } + Assert.assertEquals(2, values.size()); + Assert.assertEquals(values.get(0), values.get(1)); + } + + @Test + public void testExplainTranslationPrintsFoldedDatesAsConstructors() { + String query = "MATCH (n) WHERE n.d0 = date('2000-01-01') RETURN n"; + Map empty = Collections.emptyMap(); + CypherAst ast = CypherAst.parse(query, empty); + Map effective = CypherTemporalFunctions.fold(ast, empty); + Date folded = syntheticBinding(effective, " cypher.fold.date."); + + Translator strTranslator = Translator.builder() + .gremlinGroovy() + .build("gremlin+cfog_server_extensions" + + "+inline_parameters"); + Seq ir = ast.translate(strTranslator.flavor(), + strTranslator.features(), + ProcedureContext.global()); + String gremlin = TranslationWriter.write(ir, strTranslator, + CypherTemporalFunctions.explainParameters(effective)); + + Assert.assertTrue(gremlin, gremlin.contains("new Date(" + folded.getTime() + "L)")); + Assert.assertFalse(gremlin, gremlin.contains(folded.toString())); + } + + @Test + public void testCaseReturnWithFoldedDateKeepsValueMap() throws Exception { + Map parameters = new HashMap<>(); + parameters.put("d", expectedDate("yyyy-MM-dd", "2000-01-01")); + Map.Entry> bound = translate( + "MATCH (n) RETURN CASE WHEN n.d > $d THEN n END AS m", parameters); + Assert.assertTrue(bound.getKey(), bound.getKey().contains("valueMap")); + + Map.Entry> folded = translate( + "MATCH (n) RETURN CASE WHEN n.d > date('2000-01-01') THEN n END AS m", + Collections.emptyMap()); + Assert.assertTrue(folded.getKey(), folded.getKey().contains("valueMap")); + + Map.Entry> relationship = translate( + "MATCH (a)-[r]->(b) RETURN CASE WHEN r.since > date('2000-01-01') THEN r END AS rel", + Collections.emptyMap()); + Assert.assertTrue(relationship.getKey(), relationship.getKey().contains("valueMap")); + } + + private static Map.Entry> translate(String query, + Map parameters) { + CypherAst ast = CypherAst.parse(query, parameters); + Map effective = CypherTemporalFunctions.fold(ast, parameters); + String gremlin = ast.buildTranslation(Translator.builder().gremlinGroovy() + .build("gremlin+cfog_server_extensions" + + "+inline_parameters")); + return new java.util.AbstractMap.SimpleImmutableEntry<>(gremlin, effective); + } + + private static Date expectedDate(String pattern, String literal) throws ParseException { + SimpleDateFormat format = new SimpleDateFormat(pattern, Locale.ROOT); + format.setLenient(false); + return format.parse(literal); + } + + private static Date expectedDate(int year, int month, int day, int hour, + int minute, int second, int millis) { + Calendar calendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT); + calendar.clear(); + calendar.set(year, month - 1, day, hour, minute, second); + calendar.set(Calendar.MILLISECOND, millis); + return calendar.getTime(); + } + + private static Date syntheticBinding(Map parameters, String prefix) { + for (Map.Entry entry : parameters.entrySet()) { + if (entry.getKey().startsWith(prefix)) { + Assert.assertTrue(entry.getKey(), entry.getValue() instanceof Date); + return (Date) entry.getValue(); + } + } + Assert.fail("Missing synthetic binding with prefix " + prefix); + return null; + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/AccessLogFilterTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/AccessLogFilterTest.java index ee7d87091b..bb6e455dca 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/AccessLogFilterTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/AccessLogFilterTest.java @@ -39,9 +39,6 @@ import org.apache.hugegraph.api.filter.DecompressInterceptor; import org.apache.hugegraph.api.filter.DecompressInterceptor.Decompress; import org.apache.hugegraph.api.filter.PathFilter; -import org.apache.hugegraph.api.filter.RedirectFilter; -import org.apache.hugegraph.api.filter.RedirectFilter.RedirectMasterRole; -import org.apache.hugegraph.api.filter.RedirectFilterDynamicFeature; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.config.ServerOptions; import org.apache.hugegraph.core.GraphManager; @@ -65,11 +62,9 @@ import org.mockito.Mockito; import jakarta.inject.Provider; -import jakarta.ws.rs.Priorities; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ResourceInfo; -import jakarta.ws.rs.core.FeatureContext; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.UriInfo; @@ -365,16 +360,6 @@ public void testSkipBody_ForResourceThatDecodesEntity() throws IOException { Mockito.verify(this.requestContext, Mockito.never()).setEntityStream(Mockito.any(InputStream.class)); } - @Test - public void testRedirectRunsAfterBodyCapture() { - this.mockResourceMethod("redirectResource"); - FeatureContext context = Mockito.mock(FeatureContext.class); - - new RedirectFilterDynamicFeature().configure(this.resourceInfo, context); - - Mockito.verify(context).register(RedirectFilter.class, Priorities.USER + 1); - } - /** * Test the slow query log line contains the client IP and the body preview */ @@ -543,11 +528,6 @@ public void decompressResource() { // pass } - @RedirectMasterRole - public void redirectResource() { - // pass - } - private void mockElapsed(long elapsed) { Mockito.when(this.requestContext.getProperty(PathFilter.REQUEST_TIME)) .thenReturn(System.currentTimeMillis() - elapsed); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/CompressInterceptorTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/CompressInterceptorTest.java new file mode 100644 index 0000000000..6fee1fc581 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/CompressInterceptorTest.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.hugegraph.unit.api.filter; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.zip.GZIPInputStream; + +import org.apache.hugegraph.api.filter.CompressInterceptor; +import org.apache.hugegraph.api.filter.CompressInterceptor.Compress; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.server.ContainerRequest; +import org.junit.Test; +import org.mockito.Mockito; + +import jakarta.inject.Provider; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.WriterInterceptorContext; + +public class CompressInterceptorTest { + + private static final byte[] ENTITY = "{\"result\":\"hello HugeGraph\"}".getBytes(StandardCharsets.UTF_8); + + @Test + public void testPlainResponseWhenGzipNotAccepted() throws Exception { + for (String accept : new String[]{null, "", "identity", "br", "gzip;q=0", "*;q=0", + "identity;q=0", "gzip;q=0, identity;q=0", + "*;q=1, gzip;q=0", "gzip;q=0.5, identity;q=1", + "gzip;q=invalid"}) { + this.assertResponse(accept, false); + } + } + + @Test + public void testGzipResponseWhenAccepted() throws Exception { + for (String accept : new String[]{"gzip", "GZIP", "br, gzip", "gzip;q=0.5", "*", + "gzip;q=1, identity;q=0.5", "*;q=0, gzip;q=1"}) { + this.assertResponse(accept, true); + } + } + + @Test + public void testTrailingDotQualityFactors() throws Exception { + for (String accept : new String[]{"gzip;q=0.", "*;q=1, gzip;q=0.", + "gzip;q=0.5, identity;q=1.", "*;q=0.", + "*;q=0.5, identity;q=1.", + "*;q=1., GZIP; Q=0. ", + "gzip;q=0.0", "gzip;q=1.001", "gzip;q=1..", + "gzip;q=0.foo", "gzip;q=1. 0", "gzip;q=1.0000", + "gzip;Q =1.", "gzip;Q= 1."}) { + this.assertResponse(accept, false); + } + for (String accept : new String[]{"gzip;q=1.", "*;q=1.", + "gzip;q=1., identity;q=0.5", "*;q=0., gzip;q=1.", + "gzip;q=0.5, identity;q=0.", + "GZIP; Q=1. ", "GZIP;\tQ=1.\t", + "gzip;q=1.000", "gzip;q=0.001"}) { + this.assertResponse(accept, true); + } + } + + @Test + public void testPreserveVaryFields() throws Exception { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.VARY, "Origin"); + this.writeResponse("gzip", headers, true); + Assert.assertEquals(2, headers.get(HttpHeaders.VARY).size()); + Assert.assertEquals("Origin", headers.get(HttpHeaders.VARY).get(0)); + Assert.assertEquals(HttpHeaders.ACCEPT_ENCODING, headers.get(HttpHeaders.VARY).get(1)); + } + + @Test + public void testDoNotDuplicateVary() throws Exception { + for (String vary : new String[]{"Origin, accept-encoding", "*"}) { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.VARY, vary); + this.writeResponse("identity", headers, true); + Assert.assertEquals(1, headers.get(HttpHeaders.VARY).size()); + Assert.assertEquals(vary, headers.getFirst(HttpHeaders.VARY)); + } + } + + @Test + public void testPreserveExistingEncoding() throws Exception { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.CONTENT_ENCODING, "br"); + headers.add(HttpHeaders.CONTENT_LENGTH, ENTITY.length); + Assert.assertArrayEquals(ENTITY, this.writeResponse("gzip", headers, true)); + Assert.assertEquals("br", headers.getFirst(HttpHeaders.CONTENT_ENCODING)); + Assert.assertEquals(ENTITY.length, headers.getFirst(HttpHeaders.CONTENT_LENGTH)); + } + + @Test + public void testUnannotatedResponseRemainsPlain() throws Exception { + MultivaluedMap headers = new MultivaluedHashMap<>(); + Assert.assertArrayEquals(ENTITY, this.writeResponse("gzip", headers, false)); + Assert.assertFalse(headers.containsKey(HttpHeaders.CONTENT_ENCODING)); + Assert.assertFalse(headers.containsKey(HttpHeaders.VARY)); + } + + @Compress + private static void compressedResource() { + } + + private void assertResponse(String accept, boolean compressed) throws Exception { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.CONTENT_LENGTH, ENTITY.length); + byte[] response = this.writeResponse(accept, headers, true); + Assert.assertEquals(HttpHeaders.ACCEPT_ENCODING, headers.getFirst(HttpHeaders.VARY)); + if (compressed) { + Assert.assertEquals("gzip", headers.getFirst(HttpHeaders.CONTENT_ENCODING)); + Assert.assertFalse(headers.containsKey(HttpHeaders.CONTENT_LENGTH)); + try (GZIPInputStream input = new GZIPInputStream(new ByteArrayInputStream(response))) { + Assert.assertArrayEquals(ENTITY, input.readAllBytes()); + } + } else { + Assert.assertArrayEquals(ENTITY, response); + Assert.assertFalse(headers.containsKey(HttpHeaders.CONTENT_ENCODING)); + Assert.assertEquals(ENTITY.length, headers.getFirst(HttpHeaders.CONTENT_LENGTH)); + } + } + + private byte[] writeResponse(String accept, MultivaluedMap headers, + boolean annotated) throws Exception { + // Real Jersey headers and parser, without creating an HTTP server or socket. + URI uri = URI.create("http://localhost/gremlin"); + ContainerRequest request = new ContainerRequest(uri, uri, "POST", null, + new MapPropertiesDelegate()); + if (accept != null) { + request.getHeaders().add(HttpHeaders.ACCEPT_ENCODING, accept); + } + CompressInterceptor interceptor = new CompressInterceptor(); + Whitebox.setInternalState(interceptor, "requestHeaders", (Provider) () -> request); + WriterInterceptorContext context = Mockito.mock(WriterInterceptorContext.class); + Annotation[] annotations = annotated ? + CompressInterceptorTest.class.getDeclaredMethod("compressedResource") + .getAnnotations() : new Annotation[0]; + Mockito.when(context.getAnnotations()).thenReturn(annotations); + Mockito.when(context.getHeaders()).thenReturn(headers); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + OutputStream[] output = new OutputStream[]{bytes}; + Mockito.when(context.getOutputStream()).thenAnswer(invocation -> output[0]); + Mockito.doAnswer(invocation -> { + output[0] = invocation.getArgument(0); + return null; + }).when(context).setOutputStream(Mockito.any(OutputStream.class)); + Mockito.doAnswer(invocation -> { + output[0].write(ENTITY); + output[0].close(); + return null; + }).when(context).proceed(); + interceptor.aroundWriteTo(context); + Mockito.verify(context, Mockito.times(1)).proceed(); + return bytes.toByteArray(); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/VertexAuthorizationTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/VertexAuthorizationTest.java new file mode 100644 index 0000000000..a9b875c213 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/filter/VertexAuthorizationTest.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.hugegraph.unit.api.filter; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.hugegraph.api.filter.AuthenticationFilter.Authorizer; +import org.apache.hugegraph.api.graph.EdgeAPI; +import org.apache.hugegraph.api.graph.VertexAPI; +import org.apache.hugegraph.auth.HugeAuthenticator.User; +import org.apache.hugegraph.auth.HugePermission; +import org.apache.hugegraph.auth.RolePermission; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.space.GraphSpace; +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.FeatureContext; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.UriInfo; + +public class VertexAuthorizationTest { + + private static final String SPACE = "vertex_scope_space"; + private static final String GRAPH = "shared_graph_name"; + + @Test + public void testScopedVertexPermissions() throws IOException { + List denied = new ArrayList<>(); + for (Method method : endpoints(VertexAPI.class)) { + RolePermission role = RolePermission.role(SPACE, GRAPH, action(method)); + try { + authorize(method, role, SPACE, GRAPH); + } catch (ForbiddenException failure) { + denied.add(method.toGenericString()); + } + } + Assert.assertTrue("Scoped vertex permissions denied: " + denied, denied.isEmpty()); + } + + @Test + public void testSameGraphNameInAnotherSpaceIsDenied() { + for (Method method : endpoints(VertexAPI.class)) { + RolePermission role = RolePermission.role(SPACE, GRAPH, action(method)); + Assert.assertThrows(ForbiddenException.class, + () -> authorize(method, role, "another_space", GRAPH)); + } + } + + @Test + public void testAnotherGraphIsDenied() { + for (Method method : endpoints(VertexAPI.class)) { + RolePermission role = RolePermission.role(SPACE, GRAPH, action(method)); + Assert.assertThrows(ForbiddenException.class, + () -> authorize(method, role, SPACE, "another_graph")); + } + } + + @Test + public void testReadPermissionCannotWriteOrDelete() throws IOException { + RolePermission role = RolePermission.role(SPACE, GRAPH, HugePermission.READ); + for (Method method : endpoints(VertexAPI.class)) { + if (action(method) == HugePermission.READ) { + authorize(method, role, SPACE, GRAPH); + } else { + Assert.assertThrows(ForbiddenException.class, + () -> authorize(method, role, SPACE, GRAPH)); + } + } + } + + @Test + public void testDefaultSpaceWithoutPathParameter() throws IOException { + for (Method method : endpoints(VertexAPI.class)) { + RolePermission role = RolePermission.role("DEFAULT", GRAPH, action(method)); + authorize(method, role, null, GRAPH); + } + } + + @Test + public void testAdminAndSpaceMemberRemainAuthorized() throws IOException { + RolePermission member = RolePermission.role(SPACE, GRAPH, HugePermission.READ); + member.roles().get(SPACE).get(GRAPH).put(HugePermission.SPACE_MEMBER, Collections.emptyMap()); + for (Method method : endpoints(VertexAPI.class)) { + authorize(method, RolePermission.admin(), SPACE, GRAPH); + if (!method.getName().equals("list")) { + authorize(method, member, SPACE, GRAPH); + } + } + } + + @Test + public void testEdgeEndpointsUseTheSameScopedPermissions() throws IOException { + for (Method method : endpoints(EdgeAPI.class)) { + authorize(method, RolePermission.role(SPACE, GRAPH, action(method)), SPACE, GRAPH); + } + } + + private static List endpoints(Class api) { + List methods = Arrays.stream(api.getDeclaredMethods()) + .filter(method -> method.isAnnotationPresent(RolesAllowed.class)) + .toList(); + Assert.assertEquals(7, methods.size()); + return methods; + } + + private static HugePermission action(Method method) { + if (method.isAnnotationPresent(GET.class)) { + return HugePermission.READ; + } + return method.isAnnotationPresent(DELETE.class) ? HugePermission.DELETE : HugePermission.WRITE; + } + + private static void authorize(Method method, RolePermission role, + String space, String graph) throws IOException { + GraphSpace graphSpace = new GraphSpace(SPACE); + graphSpace.auth(true); + GraphManager manager = Mockito.mock(GraphManager.class); + Mockito.when(manager.graphSpace(Mockito.anyString())).thenReturn(graphSpace); + MultivaluedMap parameters = new MultivaluedHashMap<>(); + parameters.putSingle("graph", graph); + if (space != null) { + parameters.putSingle("graphspace", space); + } + UriInfo uri = Mockito.mock(UriInfo.class); + Mockito.when(uri.getPathParameters()).thenReturn(parameters); + Authorizer authorizer = new Authorizer(manager, new User("scoped_user", role), uri); + ContainerRequestContext request = Mockito.mock(ContainerRequestContext.class); + Mockito.when(request.getSecurityContext()).thenReturn(authorizer); + ResourceInfo resource = Mockito.mock(ResourceInfo.class); + Mockito.when(resource.getResourceMethod()).thenReturn(method); + Mockito.doReturn(method.getDeclaringClass()).when(resource).getResourceClass(); + FeatureContext feature = Mockito.mock(FeatureContext.class); + new RolesAllowedDynamicFeature().configure(resource, feature); + ArgumentCaptor registered = ArgumentCaptor.forClass(Object.class); + Mockito.verify(feature).register(registered.capture()); + ((ContainerRequestFilter) registered.getValue()).filter(request); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/GraphSpaceAPITest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/GraphSpaceAPITest.java index 4d893e3199..6783ff2b7e 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/GraphSpaceAPITest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/api/space/GraphSpaceAPITest.java @@ -19,37 +19,59 @@ import static org.apache.hugegraph.HugeFactory.SYS_GRAPH; -import java.lang.reflect.Field; +import java.io.IOException; import java.lang.reflect.Method; +import java.net.URI; import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.HugeFactory; +import org.apache.hugegraph.LoadingMode; import org.apache.hugegraph.api.auth.ManagerAPI; +import org.apache.hugegraph.api.filter.AuthenticationFilter.Authorizer; import org.apache.hugegraph.api.profile.GraphsAPI; import org.apache.hugegraph.api.space.GraphSpaceAPI; import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.HugeAuthenticator; import org.apache.hugegraph.auth.HugeDefaultRole; import org.apache.hugegraph.auth.HugeGraphAuthProxy; +import org.apache.hugegraph.auth.HugePermission; +import org.apache.hugegraph.auth.HugeResource; +import org.apache.hugegraph.auth.ResourceType; import org.apache.hugegraph.auth.HugeUser; import org.apache.hugegraph.auth.RolePermission; import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.ServerOptions; +import org.apache.hugegraph.event.EventHub; import org.apache.hugegraph.meta.MetaManager; import org.apache.hugegraph.space.GraphSpace; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.type.define.GraphMode; import org.apache.hugegraph.unit.BaseUnitTest; +import org.apache.hugegraph.unit.FakeObjects; import org.apache.tinkerpop.gremlin.structure.Graph; import org.junit.After; import org.junit.Test; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.FeatureContext; +import jakarta.ws.rs.ClientErrorException; import jakarta.ws.rs.core.SecurityContext; -import sun.misc.Unsafe; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.UriInfo; public class GraphSpaceAPITest extends BaseUnitTest { @@ -221,7 +243,7 @@ public void testSpaceManagerCannotDeleteSpaceDefaultRole() { @Test public void testPdGraphsIncludesLoadedLocalConfigGraphs() { - GraphManager manager = allocateGraphManager(); + GraphManager manager = newGraphManager(); Whitebox.setInternalState(manager, "PDExist", true); Whitebox.setInternalState(manager, "localGraphs", Collections.emptySet()); @@ -241,6 +263,194 @@ public void testPdGraphsIncludesLoadedLocalConfigGraphs() { Assert.assertFalse(result.contains(SYS_GRAPH)); } + @Test + public void testUnproxiedLoadingOwnerUsesAuthenticatedPrincipal() throws Exception { + HugeConfig config = FakeObjects.newConfig(); + config.setProperty("backend", "memory"); + config.setProperty("serializer", "text"); + config.setProperty("store", "unproxied_loading_owner"); + HugeGraph graph = HugeFactory.open(config); + try { + GraphManager manager = Mockito.mock(GraphManager.class); + Mockito.when(manager.graph("DEFAULT", "hugegraph")).thenReturn(graph); + GraphsAPI api = new GraphsAPI(); + GraphsAPI.JsonLoadingOwner request = new GraphsAPI.JsonLoadingOwner(); + request.instance = (String) graph.loadingMode().get("instance"); + request.epoch = 0L; + request.token = java.util.UUID.randomUUID().toString(); + request.action = "acquire"; + setContext("alice"); + Assert.assertEquals("active", api.loadingMode(manager, "DEFAULT", "hugegraph", request, null).get("state")); + request.action = "release"; + setContext("bob"); + ClientErrorException conflict = org.junit.Assert.assertThrows(ClientErrorException.class, + () -> api.loadingMode(manager, "DEFAULT", "hugegraph", request, null)); + Assert.assertEquals(409, conflict.getResponse().getStatus()); + Assert.assertEquals(GraphMode.LOADING, graph.mode()); + setContext("alice"); + Assert.assertEquals("released", api.loadingMode(manager, "DEFAULT", "hugegraph", request, null).get("state")); + Assert.assertEquals(GraphMode.NONE, graph.mode()); + } finally { + graph.close(); + HugeFactory.remove(graph); + } + } + + @Test + public void testLoadingOwnerRoutesAllowScopedStatusWriter() throws Exception { + RolePermission role = RolePermission.role(GRAPHSPACE, GRAPH, HugePermission.WRITE); + role.roles().get(GRAPHSPACE).get(GRAPH).put(HugePermission.WRITE, + Collections.singletonMap(ResourceType.STATUS.name(), + Collections.singletonList(new HugeResource(ResourceType.STATUS, "*", null)))); + int endpoints = 0; + for (Method method : GraphsAPI.class.getDeclaredMethods()) { + if (!method.getName().equals("loadingMode")) { + continue; + } + endpoints++; + authorizeLoadingOwnerRoute(method, role, GRAPHSPACE, GRAPH); + authorizeLoadingOwnerRoute(method, RolePermission.admin(), GRAPHSPACE, GRAPH); + authorizeLoadingOwnerRoute(method, RolePermission.role(GRAPHSPACE, GRAPH, HugePermission.SPACE), + GRAPHSPACE, GRAPH); + Assert.assertThrows(ForbiddenException.class, + () -> authorizeLoadingOwnerRoute(method, role, "other_space", GRAPH)); + Assert.assertThrows(ForbiddenException.class, + () -> authorizeLoadingOwnerRoute(method, role, GRAPHSPACE, "other_graph")); + Assert.assertThrows(ForbiddenException.class, + () -> authorizeLoadingOwnerRoute(method, RolePermission.none(), GRAPHSPACE, GRAPH)); + } + Assert.assertEquals(2, endpoints); + } + + private static void authorizeLoadingOwnerRoute(Method method, RolePermission role, + String space, String graph) throws IOException { + GraphSpace graphSpace = new GraphSpace(space); + graphSpace.auth(true); + GraphManager manager = Mockito.mock(GraphManager.class); + Mockito.when(manager.graphSpace(space)).thenReturn(graphSpace); + MultivaluedHashMap parameters = new MultivaluedHashMap<>(); + parameters.putSingle("graphspace", space); + parameters.putSingle("name", graph); + UriInfo uri = Mockito.mock(UriInfo.class); + Mockito.when(uri.getPathParameters()).thenReturn(parameters); + Authorizer authorizer = new Authorizer(manager, new HugeAuthenticator.User("writer", role), uri); + ContainerRequestContext request = Mockito.mock(ContainerRequestContext.class); + Mockito.when(request.getSecurityContext()).thenReturn(authorizer); + ResourceInfo resource = Mockito.mock(ResourceInfo.class); + Mockito.when(resource.getResourceMethod()).thenReturn(method); + Mockito.doReturn(GraphsAPI.class).when(resource).getResourceClass(); + FeatureContext feature = Mockito.mock(FeatureContext.class); + new RolesAllowedDynamicFeature().configure(resource, feature); + ArgumentCaptor registered = ArgumentCaptor.forClass(Object.class); + Mockito.verify(feature).register(registered.capture()); + ((ContainerRequestFilter) registered.getValue()).filter(request); + } + + @Test + public void testAuthDisabledSpaceRecoveryStillRequiresGlobalAdmin() throws Exception { + HugeConfig config = FakeObjects.newConfig(); + config.setProperty("backend", "memory"); + config.setProperty("serializer", "text"); + config.setProperty("store", "unproxied_loading_recovery"); + HugeGraph graph = HugeFactory.open(config); + try { + Assert.assertFalse(graph instanceof HugeGraphAuthProxy); + String space = "open_space"; + GraphSpace graphSpace = new GraphSpace(space); + graphSpace.auth(false); + GraphManager manager = Mockito.mock(GraphManager.class); + Mockito.when(manager.requireAuthentication()).thenReturn(true); + Mockito.when(manager.graphSpace(space)).thenReturn(graphSpace); + Mockito.when(manager.graph(space, "hugegraph")).thenReturn(graph); + UriInfo uri = Mockito.mock(UriInfo.class); + MultivaluedHashMap parameters = new MultivaluedHashMap<>(); + parameters.putSingle("graphspace", space); + Mockito.when(uri.getPathParameters()).thenReturn(parameters); + Authorizer restricted = new Authorizer(manager, + new HugeAuthenticator.User("bob", RolePermission.none()), uri); + Assert.assertTrue(restricted.isUserInRole("admin")); + Assert.assertFalse(RolePermission.isAdmin(restricted.role())); + URI endpoint = URI.create("http://localhost/graphspaces/open_space/graphs/hugegraph/mode/owners"); + ContainerRequest context = new ContainerRequest(endpoint, endpoint, "PUT", restricted, + new MapPropertiesDelegate()); + GraphsAPI api = new GraphsAPI(); + Map state = graph.loadingMode(); + String instance = (String) state.get("instance"); + String token = java.util.UUID.randomUUID().toString(); + graph.loadingMode("acquire", instance, 0, token, "alice"); + Map before = api.loadingMode(manager, space, "hugegraph"); + GraphsAPI.JsonLoadingOwner request = new GraphsAPI.JsonLoadingOwner(); + request.action = "recover"; + request.instance = instance; + request.epoch = (Long) before.get("epoch"); + request.revision = (Long) before.get("revision"); + request.confirm = "I'm sure all imports have stopped"; + ForbiddenException denied = org.junit.Assert.assertThrows(ForbiddenException.class, + () -> api.loadingMode(manager, space, "hugegraph", request, context)); + Assert.assertEquals(403, denied.getResponse().getStatus()); + Assert.assertEquals(before, graph.loadingMode()); + Assert.assertEquals(GraphMode.LOADING, graph.mode()); + + for (HugeAuthenticator.User user : new HugeAuthenticator.User[]{HugeAuthenticator.User.ADMIN, + HugeAuthenticator.User.ANONYMOUS}) { + Mockito.when(manager.requireAuthentication()).thenReturn(user != HugeAuthenticator.User.ANONYMOUS); + context.setSecurityContext(new Authorizer(manager, user, uri)); + state = graph.loadingMode(); + request.epoch = (Long) state.get("epoch"); + graph.loadingMode("acquire", instance, request.epoch, token, "alice"); + request.revision = (Long) graph.loadingMode().get("revision"); + Assert.assertEquals("recovered", api.loadingMode(manager, space, "hugegraph", request, + context).get("state")); + Assert.assertEquals(GraphMode.NONE, graph.mode()); + } + } finally { + graph.close(); + HugeFactory.remove(graph); + } + } + + @Test + public void testLoadingRecoveryConfirmationAndConflictResponse() { + GraphManager manager = Mockito.mock(GraphManager.class); + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(manager.graph("DEFAULT", "hugegraph")).thenReturn(graph); + GraphsAPI api = new GraphsAPI(); + GraphsAPI.JsonLoadingOwner request = new GraphsAPI.JsonLoadingOwner(); + request.action = "recover"; + request.instance = "instance"; + request.epoch = 0L; + ContainerRequestContext requestContext = Mockito.mock(ContainerRequestContext.class); + Authorizer authorizer = Mockito.mock(Authorizer.class); + Mockito.when(requestContext.getSecurityContext()).thenReturn(authorizer); + Mockito.when(authorizer.role()).thenReturn(RolePermission.none()); + Assert.assertThrows(ForbiddenException.class, + () -> api.loadingMode(manager, "DEFAULT", "hugegraph", request, requestContext)); + Mockito.verify(graph, Mockito.never()).recoverLoadingMode(Mockito.anyString(), Mockito.anyLong(), + Mockito.anyLong()); + Mockito.when(authorizer.role()).thenReturn(RolePermission.admin()); + Assert.assertThrows(IllegalArgumentException.class, + () -> api.loadingMode(manager, "DEFAULT", "hugegraph", request, requestContext)); + Mockito.verify(graph, Mockito.never()).loadingMode(Mockito.anyString(), Mockito.anyString(), + Mockito.anyLong(), Mockito.any(), Mockito.anyString()); + request.confirm = "I'm sure all imports have stopped"; + Assert.assertThrows(IllegalArgumentException.class, + () -> api.loadingMode(manager, "DEFAULT", "hugegraph", request, requestContext)); + request.revision = 0L; + Map receipt = Collections.singletonMap("state", "recovered"); + Mockito.when(graph.recoverLoadingMode("instance", 0, 0)).thenReturn(receipt); + Assert.assertSame(receipt, api.loadingMode(manager, "DEFAULT", "hugegraph", request, requestContext)); + + Mockito.when(graph.recoverLoadingMode("instance", 0, 0)) + .thenThrow(new LoadingMode.ConflictException("stale epoch")); + ClientErrorException ownerConflict = org.junit.Assert.assertThrows(ClientErrorException.class, + () -> api.loadingMode(manager, "DEFAULT", "hugegraph", request, requestContext)); + Assert.assertEquals(409, ownerConflict.getResponse().getStatus()); + Mockito.doThrow(new LoadingMode.ConflictException("active owners")).when(graph).mode(GraphMode.NONE); + ClientErrorException modeConflict = org.junit.Assert.assertThrows(ClientErrorException.class, + () -> api.mode(manager, "DEFAULT", "hugegraph", GraphMode.NONE)); + Assert.assertEquals(409, modeConflict.getResponse().getStatus()); + } + @Test public void testAdminCanListGraphsWithoutGraphReadRole() { GraphManager manager = managerWithLoadedDefaultGraph(ADMIN, true); @@ -276,7 +486,7 @@ private static GraphManager managerWithDefaultRoleContext(String operator, HugeAuthenticator authenticator = Mockito.mock(HugeAuthenticator.class); Mockito.when(authenticator.authManager()).thenReturn(authManager); - GraphManager manager = allocateGraphManager(); + GraphManager manager = newGraphManager(); Whitebox.setInternalState(manager, "PDExist", true); Whitebox.setInternalState(manager, "authenticator", authenticator); Map spaces = new ConcurrentHashMap<>(); @@ -301,7 +511,7 @@ private static GraphManager managerWithDefaultRoleContext(String operator, private static GraphManager managerWithLoadedDefaultGraph(String operator, boolean admin) { - GraphManager manager = allocateGraphManager(); + GraphManager manager = newGraphManager(); Whitebox.setInternalState(manager, "PDExist", true); Whitebox.setInternalState(manager, "localGraphs", Collections.singleton("hugegraph")); @@ -326,15 +536,11 @@ private static GraphManager managerWithLoadedDefaultGraph(String operator, return manager; } - private static GraphManager allocateGraphManager() { - try { - Field field = Unsafe.class.getDeclaredField("theUnsafe"); - field.setAccessible(true); - Unsafe unsafe = (Unsafe) field.get(null); - return (GraphManager) unsafe.allocateInstance(GraphManager.class); - } catch (Exception e) { - throw new AssertionError(e); - } + private static GraphManager newGraphManager() { + HugeConfig config = FakeObjects.newConfig(); + config.setProperty(ServerOptions.USE_PD.name(), false); + config.setProperty(ServerOptions.GRAPH_LOAD_FROM_LOCAL_CONFIG.name(), false); + return new GraphManager(config, new EventHub("graph-space-api-test")); } private static void setContext(String username) { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java index 37d70af13f..0756724b1b 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java @@ -21,7 +21,9 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.HugeAuthenticator; @@ -33,8 +35,8 @@ import org.apache.hugegraph.auth.RolePermission; import org.apache.hugegraph.auth.UserWithRole; import org.apache.hugegraph.backend.cache.Cache; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.config.AuthOptions; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.task.TaskManager; @@ -42,6 +44,7 @@ import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.unit.BaseUnitTest; +import org.apache.hugegraph.util.Reflection; import org.apache.hugegraph.util.RateLimiter; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -59,6 +62,79 @@ public class HugeGraphAuthProxyTest extends BaseUnitTest { + @Test + public void testLoadingOwnershipRequiresStatusWriteAndUsesAuthenticatedPrincipal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(graph.name()).thenReturn("tc13_mode_auth"); + Mockito.when(graph.graphSpace()).thenReturn("DEFAULT"); + Mockito.when(graph.spaceGraphName()).thenReturn("tc13_mode_auth"); + Mockito.when(graph.configuration()).thenReturn(config); + Mockito.when(graph.authManager()).thenReturn(Mockito.mock(AuthManager.class)); + Mockito.when(graph.taskScheduler()).thenReturn(Mockito.mock(TaskScheduler.class)); + Mockito.when(config.get(AuthOptions.AUTH_CACHE_EXPIRE)).thenReturn(3600L); + Mockito.when(config.get(AuthOptions.AUTH_CACHE_CAPACITY)).thenReturn(100L); + Mockito.when(config.get(AuthOptions.AUTH_AUDIT_LOG_RATE)).thenReturn(1000D); + HugeGraphAuthProxy proxy = new HugeGraphAuthProxy(graph); + setContext(new HugeGraphAuthProxy.Context(new HugeAuthenticator.User( + "reader", RolePermission.role("DEFAULT", "tc13_mode_auth", HugePermission.READ)))); + Assert.assertThrows(jakarta.ws.rs.ForbiddenException.class, (Assert.ThrowableRunnable) proxy::loadingMode); + Assert.assertThrows(jakarta.ws.rs.ForbiddenException.class, + () -> proxy.loadingMode("acquire", "instance", 0, "token", "forged")); + Mockito.verify(graph, Mockito.never()).loadingMode(); + Mockito.verify(graph, Mockito.never()).loadingMode(Mockito.anyString(), Mockito.anyString(), + Mockito.anyLong(), Mockito.anyString(), Mockito.anyString()); + setContext(new HugeGraphAuthProxy.Context(new HugeAuthenticator.User( + "writer", RolePermission.role("DEFAULT", "tc13_mode_auth", HugePermission.WRITE)))); + proxy.loadingMode(); + proxy.loadingMode("acquire", "instance", 0, "token", "forged"); + Mockito.verify(graph).loadingMode(); + Mockito.verify(graph).loadingMode("acquire", "instance", 0, "token", "writer"); + Assert.assertThrows(jakarta.ws.rs.ForbiddenException.class, + () -> proxy.recoverLoadingMode("instance", 0, 0)); + Mockito.verify(graph, Mockito.never()).recoverLoadingMode("instance", 0, 0); + setContext(new HugeGraphAuthProxy.Context(new HugeAuthenticator.User("admin", RolePermission.admin()))); + proxy.recoverLoadingMode("instance", 0, 0); + Mockito.verify(graph).recoverLoadingMode("instance", 0, 0); + } + + @Test + public void testJdk17ReflectionFilters() { + Reflection.registerFieldsToFilter(ReflectionFilterTarget.class, "field"); + Reflection.registerMethodsToFilter(ReflectionFilterTarget.class, "method"); + + Assert.assertThrows(NoSuchFieldException.class, + () -> ReflectionFilterTarget.class.getDeclaredField("field")); + Assert.assertThrows(NoSuchMethodException.class, + () -> ReflectionFilterTarget.class.getDeclaredMethod("method")); + Assert.assertThrows(IllegalArgumentException.class, + () -> Reflection.registerFieldsToFilter( + ReflectionFilterTarget.class, "field")); + } + + @Test + public void testJdk17ReflectionFilterFailureCause() { + Throwable exception = Assert.assertThrows( + HugeException.class, + () -> Reflection.registerFieldsToFilter( + ReflectionFailureTarget.class, (String) null)); + + Assert.assertInstanceOf(NullPointerException.class, exception.getCause()); + } + + private static class ReflectionFailureTarget { + } + + private static class ReflectionFilterTarget { + + @SuppressWarnings("unused") + private String field; + + @SuppressWarnings("unused") + private void method() { + } + } + private static HugeGraphAuthProxy.Context setContext( HugeGraphAuthProxy.Context context) { try { @@ -90,6 +166,30 @@ public void testUsernameWithNullContext() { Assert.assertEquals("anonymous", username); } + @Test + public void testSearchPredicateDoesNotRequireAdminConfigAccess() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(graph.name()).thenReturn("hugegraph"); + Mockito.when(graph.graphSpace()).thenReturn("DEFAULT"); + Mockito.when(graph.spaceGraphName()).thenReturn("hugegraph"); + Mockito.when(graph.configuration()).thenReturn(config); + Mockito.when(graph.authManager()).thenReturn(Mockito.mock(AuthManager.class)); + Mockito.when(graph.taskScheduler()).thenReturn(Mockito.mock(TaskScheduler.class)); + Mockito.when(config.get(AuthOptions.AUTH_CACHE_EXPIRE)).thenReturn(3600L); + Mockito.when(config.get(AuthOptions.AUTH_CACHE_CAPACITY)).thenReturn(100L); + Mockito.when(config.get(AuthOptions.AUTH_AUDIT_LOG_RATE)).thenReturn(1000D); + Predicate matcher = "alpha"::equals; + Mockito.when(graph.searchPredicate("(alpha)")).thenReturn(matcher); + HugeGraphAuthProxy proxy = new HugeGraphAuthProxy(graph); + setContext(new HugeGraphAuthProxy.Context(new HugeAuthenticator.User( + "reader", RolePermission.all("hugegraph")))); + Mockito.clearInvocations(graph); + + Assert.assertSame(matcher, proxy.searchPredicate("(alpha)")); + Mockito.verify(graph, Mockito.never()).configuration(); + } + @Test public void testUsernameWithValidContext() { // Create a user with a specific username diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CacheManagerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CacheManagerTest.java index 8d7ef2166a..034bbf46a8 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CacheManagerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CacheManagerTest.java @@ -26,8 +26,8 @@ import org.apache.hugegraph.backend.cache.CacheManager; import org.apache.hugegraph.backend.cache.OffheapCache; import org.apache.hugegraph.backend.cache.RamCache; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.unit.BaseUnitTest; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CacheTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CacheTest.java index d9bd4cf953..49f0d730d2 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CacheTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CacheTest.java @@ -26,11 +26,13 @@ import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.backend.cache.Cache; +import org.apache.hugegraph.backend.cache.CachedBackendStore.QueryId; import org.apache.hugegraph.backend.cache.LevelCache; import org.apache.hugegraph.backend.cache.OffheapCache; import org.apache.hugegraph.backend.cache.RamCache; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.unit.BaseUnitTest; @@ -184,6 +186,126 @@ protected HugeGraph graph() { return this.graph; } + @Test + public void testQueryKeyGuardSkipsEncodingOutsideMiddleRange() { + int safeLength = BytesBuffer.ID_LEN_MAX / 3; + String[] shortKeys = {"normal", "图查询", "\uD83D\uDE00", + "a".repeat(safeLength), + "图".repeat(safeLength), + "\uD83D\uDE00".repeat(safeLength / 2)}; + for (String text : shortKeys) { + assertQueryKeyGuard(text, true, 0); + } + assertQueryKeyGuard("a".repeat(BytesBuffer.ID_LEN_MAX + 1), false, 0); + + Id normal = Mockito.mock(Id.class); + Assert.assertTrue(Whitebox.invokeStatic( + OffheapCache.class, new Class[]{Id.class}, "cacheableKey", normal)); + Mockito.verifyNoInteractions(normal); + } + + @Test + public void testQueryKeyGuardEncodesMiddleRangeOnce() { + int limit = BytesBuffer.ID_LEN_MAX; + assertQueryKeyGuard("a".repeat(limit / 3 + 1), true, 1); + assertQueryKeyGuard("a".repeat(limit), true, 1); + + String chinese = "图".repeat(limit / 3) + "a".repeat(limit % 3); + assertQueryKeyGuard(chinese, true, 1); + assertQueryKeyGuard(chinese + "a", false, 1); + + String pairs = "\uD83D\uDE00".repeat(limit / 4) + "a".repeat(limit % 4); + assertQueryKeyGuard(pairs, true, 1); + assertQueryKeyGuard(pairs + "a", false, 1); + } + + private static void assertQueryKeyGuard(String text, boolean cacheable, + int encodings) { + CountingQueryId id = new CountingQueryId(text); + Assert.assertEquals(cacheable, Whitebox.invokeStatic( + OffheapCache.class, new Class[]{Id.class}, "cacheableKey", id)); + Assert.assertEquals(encodings, id.encodings); + } + + private static class CountingQueryId extends QueryId { + + private int encodings; + + private CountingQueryId(String query) { + super(query, query.hashCode()); + } + + @Override + public byte[] asBytes() { + this.encodings++; + return super.asBytes(); + } + } + + @Test + public void testOversizedQueryKeyBypassesOffheapCache() { + // Construct L2 directly: LevelCacheTest inherits this method but + // may legitimately retain the same large key in its RAM tier. + Cache cache = new OffheapCache(this.graph(), 10000L, + ENTRY_SIZE, SEGMENTS); + String text = "a".repeat(BytesBuffer.ID_LEN_MAX + 1); + Id oversized = new QueryId(text, text.hashCode()); + Id normal = new QueryId("normal", "normal".hashCode()); + + Assert.assertTrue(cache.update(normal, "retained")); + Assert.assertNull(cache.get(oversized)); + Assert.assertFalse(cache.containsKey(oversized)); + Assert.assertFalse(cache.update(oversized, "ignored")); + cache.invalidate(oversized); + Assert.assertNull(cache.get(oversized)); + Assert.assertFalse(cache.containsKey(oversized)); + Assert.assertEquals("retained", cache.get(normal)); + Assert.assertEquals(1L, cache.size()); + cache.clear(); + } + + @Test + public void testQueryKeyAtBinaryIdLimit() { + Cache cache = new OffheapCache(this.graph(), 10000L, + ENTRY_SIZE, SEGMENTS); + String text = "a".repeat(BytesBuffer.ID_LEN_MAX); + Id boundary = new QueryId(text, text.hashCode()); + + Assert.assertTrue(cache.update(boundary, "boundary")); + Assert.assertTrue(cache.containsKey(boundary)); + Assert.assertEquals("boundary", cache.get(boundary)); + cache.invalidate(boundary); + Assert.assertNull(cache.get(boundary)); + Assert.assertFalse(cache.containsKey(boundary)); + cache.clear(); + } + + @Test + public void testQueryKeyLimitCountsUtf8Bytes() { + Cache cache = new OffheapCache(this.graph(), 10000L, + ENTRY_SIZE, SEGMENTS); + String text = "图".repeat(BytesBuffer.ID_LEN_MAX / 3 + 1); + Id oversized = new QueryId(text, text.hashCode()); + Assert.assertLt(BytesBuffer.ID_LEN_MAX, text.length()); + Assert.assertGt(BytesBuffer.ID_LEN_MAX, oversized.asBytes().length); + + Assert.assertNull(cache.get(oversized)); + Assert.assertFalse(cache.update(oversized, "ignored")); + Assert.assertFalse(cache.containsKey(oversized)); + cache.invalidate(oversized); + Assert.assertEquals(0L, cache.size()); + + text = "图".repeat(BytesBuffer.ID_LEN_MAX / 3) + + "a".repeat(BytesBuffer.ID_LEN_MAX % 3); + Id boundary = new QueryId(text, text.hashCode()); + Assert.assertEquals(BytesBuffer.ID_LEN_MAX, boundary.asBytes().length); + Assert.assertTrue(cache.update(boundary, "utf8-boundary")); + Assert.assertEquals("utf8-boundary", cache.get(boundary)); + cache.invalidate(boundary); + Assert.assertNull(cache.get(boundary)); + cache.clear(); + } + @Test public void testUpdateAndGetWithInvalidDataType() { Cache cache = newCache(); @@ -217,6 +339,45 @@ protected Cache newCache(long capacity) { return new LevelCache(l1cache, l2cache); } + @Test + public void testQueryKeyRoundTripAcrossCacheLevels() { + Cache cache = newCache(); + Cache[] tiers = Whitebox.getInternalState(cache, "caches"); + int limit = BytesBuffer.ID_LEN_MAX; + String chinese = "图".repeat(limit / 3) + "a".repeat(limit % 3); + String pairs = "\uD83D\uDE00".repeat(limit / 4) + "a".repeat(limit % 4); + String[] texts = {"normal", "图查询", "\uD83D\uDE00", "a".repeat(limit), + chinese, pairs, "a".repeat(limit + 1), chinese + "a", pairs + "a"}; + try { + for (String text : texts) { + Id id = new QueryId(text, text.hashCode()); + boolean fitsOffheap = id.asBytes().length <= limit; + Assert.assertTrue(cache.update(id, "cached")); + Assert.assertEquals("cached", cache.get(id)); + Assert.assertEquals("cached", tiers[0].get(id)); + Assert.assertEquals(fitsOffheap, tiers[1].containsKey(id)); + if (fitsOffheap) { + tiers[0].invalidate(id); + Assert.assertEquals("cached", cache.get(id)); + Assert.assertTrue(cache.update(id, "cached")); + } + + cache.invalidate(id); + Assert.assertNull(cache.get(id)); + for (Cache tier : tiers) { + Assert.assertNull(tier.get(id)); + Assert.assertFalse(tier.containsKey(id)); + } + Assert.assertEquals("backend", cache.getOrFetch(id, key -> "backend")); + Assert.assertEquals("backend", cache.get(id)); + Assert.assertEquals(fitsOffheap, tiers[1].containsKey(id)); + cache.invalidate(id); + } + } finally { + cache.clear(); + } + } + @Test @Override public void testUpdateAndGetWithInvalidDataType() { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CachedGraphTransactionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CachedGraphTransactionTest.java index ae0321ee01..a18bcbec6d 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CachedGraphTransactionTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CachedGraphTransactionTest.java @@ -36,14 +36,14 @@ import org.apache.hugegraph.backend.cache.CachedBackendStore.QueryId; import org.apache.hugegraph.backend.cache.CachedGraphTransaction; import org.apache.hugegraph.backend.cache.OffheapCache; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery.OptimizedType; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery.OptimizedType; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryResultContext; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.store.BackendStore; @@ -51,7 +51,7 @@ import org.apache.hugegraph.backend.store.ram.RamTable; import org.apache.hugegraph.event.EventHub; import org.apache.hugegraph.event.EventListener; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeElement; import org.apache.hugegraph.structure.HugeVertex; @@ -357,7 +357,65 @@ public void testClosingNonOwnerKeepsStoreListenerRegistered() } @Test - public void testLastCloseRemovesStoreListener() throws Exception { + public void testGraphCloseRemovesStoreListenerWithoutCacheHolder() throws Exception { + String name = this.params.spaceGraphName(); + Object cacheHolder = graphCacheEventListeners().remove(name); + Assert.assertNotNull(cacheHolder); + Whitebox.invoke(cacheHolder.getClass(), "close", cacheHolder); + Object storeHolder = storeEventListeners().get(name); + EventListener listener = holderListener(storeHolder); + BackendStoreProvider provider = holderProvider(storeHolder); + CachedGraphTransaction.closeGraph(this.params); + Assert.assertFalse(storeEventListeners().containsKey(name)); + Assert.assertFalse(provider.storeEventHub().listeners(EventHub.ANY_EVENT).contains(listener)); + } + + @Test + public void testGraphCloseCleansStoreAfterCacheRegistrationFailure() throws Exception { + String name = this.params.spaceGraphName(); + EventHub hub = Mockito.spy(new EventHub("cache-registration-failure")); + RuntimeException failure = new IllegalStateException("injected cache registration failure"); + Mockito.doThrow(failure).when(hub).listen(Mockito.eq(Events.CACHE), Mockito.any(EventListener.class)); + HugeGraphParams failedParams = Mockito.spy(this.params); + Mockito.doReturn(hub).when(failedParams).graphEventHub(); + try { + new CachedGraphTransaction(failedParams, this.params.loadGraphStore()); + Assert.fail("Cache registration must reach the injected failure"); + } catch (RuntimeException expected) { + Assert.assertSame(failure, expected); + } + Object storeHolder = storeEventListeners().get(name); + Assert.assertSame(hub, Whitebox.getInternalState(storeHolder, "hub")); + BackendStoreProvider provider = holderProvider(storeHolder); + EventListener listener = holderListener(storeHolder); + Assert.assertTrue(provider.storeEventHub().listeners(EventHub.ANY_EVENT).contains(listener)); + CachedGraphTransaction.closeGraph(failedParams); + Assert.assertFalse(storeEventListeners().containsKey(name)); + Assert.assertFalse(provider.storeEventHub().listeners(EventHub.ANY_EVENT).contains(listener)); + } + + @Test + public void testOldGraphCloseKeepsReopenedStoreGeneration() throws Exception { + HugeGraphParams oldParams = this.params; + this.cache.close(); + this.cache = null; + this.graph.clearBackend(); + this.graph.close(); + this.graph = HugeFactory.open(FakeObjects.newConfig()); + this.params = Whitebox.getInternalState(this.graph, "params"); + this.cache = new CachedGraphTransaction(this.params, this.params.loadGraphStore()); + String name = this.params.spaceGraphName(); + Object cacheHolder = graphCacheEventListeners().get(name); + Object storeHolder = storeEventListeners().get(name); + CachedGraphTransaction.closeGraph(oldParams); + Assert.assertSame(cacheHolder, graphCacheEventListeners().get(name)); + Assert.assertSame(storeHolder, storeEventListeners().get(name)); + Assert.assertTrue(holderProvider(storeHolder).storeEventHub().listeners(EventHub.ANY_EVENT) + .contains(holderListener(storeHolder))); + } + + @Test + public void testGraphCloseRemovesStoreListener() throws Exception { ConcurrentMap storeListeners = storeEventListeners(); String graphName = this.params.spaceGraphName(); @@ -374,6 +432,11 @@ public void testLastCloseRemovesStoreListener() throws Exception { this.cache = null; this.params.graphTransaction().close(); + Assert.assertSame(holder, storeListeners.get(graphName)); + Assert.assertEquals(0, holderRefCount(holder)); + this.graph.clearBackend(); + this.graph.close(); + this.graph = null; Assert.assertFalse(storeListeners.containsKey(graphName)); } @@ -473,14 +536,16 @@ public void testReopenGraphReRegistersStoreListener() throws Exception { this.cache = null; this.params.graphTransaction().close(); - // Last close drops the registry entry and unregisters the listener. + // Request leases may end while the graph caches remain warm. + Assert.assertTrue(storeListeners.containsKey(graphName)); + Assert.assertTrue(provider.storeEventHub().listeners(EventHub.ANY_EVENT).contains(registered)); + + this.graph.clearBackend(); + this.graph.close(); Assert.assertFalse(storeListeners.containsKey(graphName)); Assert.assertFalse(provider.storeEventHub() .listeners(EventHub.ANY_EVENT) .contains(registered)); - - this.graph.clearBackend(); - this.graph.close(); this.graph = null; HugeGraph reopened = HugeFactory.open(FakeObjects.newConfig()); @@ -505,7 +570,7 @@ public void testReopenGraphReRegistersStoreListener() throws Exception { } @Test - public void testLastCloseRemovesGraphCacheListener() throws Exception { + public void testGraphCloseRemovesGraphCacheListener() throws Exception { ConcurrentMap cacheListeners = graphCacheEventListeners(); String graphName = this.params.spaceGraphName(); @@ -523,13 +588,15 @@ public void testLastCloseRemovesGraphCacheListener() throws Exception { this.cache = null; this.params.graphTransaction().close(); + Assert.assertTrue(cacheListeners.containsKey(graphName)); + Assert.assertTrue(this.params.graphEventHub().listeners(Events.CACHE).contains(registered)); + + this.graph.clearBackend(); + this.graph.close(); Assert.assertFalse(cacheListeners.containsKey(graphName)); Assert.assertFalse(this.params.graphEventHub() .listeners(Events.CACHE) .contains(registered)); - - this.graph.clearBackend(); - this.graph.close(); this.graph = null; HugeGraph reopened = HugeFactory.open(FakeObjects.newConfig()); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CachedSchemaTransactionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CachedSchemaTransactionTest.java index 9e7aec3842..b019ae16b6 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CachedSchemaTransactionTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/CachedSchemaTransactionTest.java @@ -19,6 +19,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -39,20 +40,27 @@ import org.apache.hugegraph.backend.cache.CacheManager; import org.apache.hugegraph.backend.cache.CachedSchemaTransaction; import org.apache.hugegraph.backend.cache.CachedSchemaTransactionV2; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.event.EventHub; import org.apache.hugegraph.event.EventListener; import org.apache.hugegraph.meta.MetaDriver; import org.apache.hugegraph.meta.MetaManager; +import org.apache.hugegraph.meta.PdMetaDriver; import org.apache.hugegraph.meta.managers.GraphMetaManager; -import org.apache.hugegraph.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.IdStrategy; import org.apache.hugegraph.unit.BaseUnitTest; import org.apache.hugegraph.unit.FakeObjects; import org.apache.hugegraph.util.Events; +import org.apache.hugegraph.util.JsonUtil; +import org.apache.tinkerpop.gremlin.structure.VertexProperty; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -297,7 +305,7 @@ public void testCacheListenerSurvivesOwnerClose() throws Exception { } @Test - public void testLastCloseRemovesSchemaCacheListener() throws Exception { + public void testGraphCloseRemovesSchemaCacheListener() throws Exception { ConcurrentMap registry = schemaCacheEventListeners(); String graphName = this.params.spaceGraphName(); CachedSchemaTransaction owner = this.cache(); @@ -314,13 +322,15 @@ public void testLastCloseRemovesSchemaCacheListener() throws Exception { this.cache = null; this.params.schemaTransaction().close(); + Assert.assertTrue(registry.containsKey(graphName)); + Assert.assertTrue(this.params.schemaEventHub().listeners(Events.CACHE).contains(registered)); + + this.graph.clearBackend(); + this.graph.close(); Assert.assertFalse(registry.containsKey(graphName)); Assert.assertFalse(this.params.schemaEventHub() .listeners(Events.CACHE) .contains(registered)); - - this.graph.clearBackend(); - this.graph.close(); this.graph = null; HugeGraph reopened = HugeFactory.open(FakeObjects.newConfig()); @@ -474,8 +484,11 @@ private static Cache v2Cache(String prefixField, } private static Object newV2SchemaCaches(int size) { - for (Class clazz : - CachedSchemaTransactionV2.class.getDeclaredClasses()) { + return newSchemaCaches(CachedSchemaTransactionV2.class, size); + } + + private static Object newSchemaCaches(Class transactionClass, int size) { + for (Class clazz : transactionClass.getDeclaredClasses()) { if (!"SchemaCaches".equals(clazz.getSimpleName())) { continue; } @@ -491,6 +504,55 @@ private static Object newV2SchemaCaches(int size) { throw new AssertionError("SchemaCaches class not found"); } + @Test + public void testSchemaCacheIdBoundsBeforeNarrowing() { + assertSchemaCacheIdBounds(newSchemaCaches(CachedSchemaTransaction.class, 16), + SchemaElement.class, + Mockito.mock(PropertyKey.class)); + assertSchemaCacheIdBounds(newV2SchemaCaches(16), SchemaElement.class, + Mockito.mock(PropertyKey.class)); + } + + private static void assertSchemaCacheIdBounds(Object cache, Class schemaClass, + Object value) { + Class[] readTypes = {HugeType.class, Id.class}; + Class[] writeTypes = {HugeType.class, Id.class, schemaClass}; + for (HugeType type : new HugeType[]{HugeType.PROPERTY_KEY, HugeType.VERTEX_LABEL, + HugeType.EDGE_LABEL, HugeType.INDEX_LABEL}) { + // Grow the array consecutively before checking its last valid slot. + for (int key = 1; key < 16; key++) { + Whitebox.invoke(cache.getClass(), writeTypes, "set", cache, type, + IdGenerator.of(key), value); + } + Id last = IdGenerator.of(15L); + Assert.assertSame(value, Whitebox.invoke(cache.getClass(), readTypes, + "get", cache, type, last)); + Whitebox.invoke(cache.getClass(), readTypes, "remove", cache, type, last); + Assert.assertNull(Whitebox.invoke(cache.getClass(), readTypes, + "get", cache, type, last)); + Id valid = IdGenerator.of(1L); + Whitebox.invoke(cache.getClass(), writeTypes, "set", cache, type, valid, value); + Assert.assertSame(value, Whitebox.invoke(cache.getClass(), readTypes, + "get", cache, type, valid)); + for (long number : new long[]{16L, Integer.MAX_VALUE, 1L << 31, + (1L << 32) + 1L, Long.MAX_VALUE}) { + Id oversized = IdGenerator.of(number); + Assert.assertNull(Whitebox.invoke(cache.getClass(), readTypes, + "get", cache, type, oversized)); + Whitebox.invoke(cache.getClass(), writeTypes, "set", cache, type, + oversized, null); + Assert.assertSame(value, Whitebox.invoke(cache.getClass(), readTypes, + "get", cache, type, valid)); + Whitebox.invoke(cache.getClass(), readTypes, "remove", cache, type, oversized); + Assert.assertSame(value, Whitebox.invoke(cache.getClass(), readTypes, + "get", cache, type, valid)); + } + Whitebox.invoke(cache.getClass(), readTypes, "remove", cache, type, valid); + Assert.assertNull(Whitebox.invoke(cache.getClass(), readTypes, + "get", cache, type, valid)); + } + } + private static void clearV2SchemaCaches(Object arrayCaches) { Whitebox.invoke(arrayCaches.getClass(), "clear", arrayCaches); } @@ -1002,4 +1064,196 @@ public void testResetCachedAllIfReachedCapacity() throws Exception { Whitebox.setInternalState(cache, "idCache.capacity", old); } } + + @Test + public void testV2WarmRequestsRetainSchemaWithoutMetadataReadsAndReceiveInvalidations() throws Exception { + AtomicBoolean registered = metaListenerFlag(); + boolean previous = registered.getAndSet(true); + List requests = new ArrayList<>(); + Object originalGraphManager = null; + boolean graphManagerReplaced = false; + try { + FakeObjects objects = new FakeObjects("v2-request-schema"); + HugeGraphParams requestParams = v2RequestParams(objects.graph()); + VertexLabel original = objects.newVertexLabel(IdGenerator.of(31), "retained", + IdStrategy.CUSTOMIZE_NUMBER); + AtomicReference metadata = new AtomicReference<>(JsonUtil.toJson(original.asMap())); + AtomicInteger reads = new AtomicInteger(); + AtomicInteger scans = new AtomicInteger(); + PdMetaDriver driver = Mockito.mock(PdMetaDriver.class); + Mockito.when(driver.get(Mockito.anyString())).thenAnswer(call -> { + reads.incrementAndGet(); + return metadata.get(); + }); + Mockito.when(driver.scanWithPrefix(Mockito.anyString())).thenAnswer(call -> { + scans.incrementAndGet(); + return ImmutableMap.of("label", metadata.get()); + }); + originalGraphManager = swapGraphMetaManager(new GraphMetaManager(driver, "test")); + graphManagerReplaced = true; + CachedSchemaTransactionV2 first = new CachedSchemaTransactionV2(driver, "test", requestParams); + requests.add(first); + VertexLabel retainedLabel = first.getVertexLabel(original.id()); + Assert.assertEquals(1, reads.get()); + HugeVertex retained = new HugeVertex(objects.graph(), IdGenerator.of(32), retainedLabel); + first.close(); + for (int request = 0; request < 10; request++) { + CachedSchemaTransactionV2 next = new CachedSchemaTransactionV2(driver, "test", requestParams); + requests.add(next); + Assert.assertSame(retainedLabel, next.getVertexLabel(original.id())); + Assert.assertSame(retainedLabel, next.getVertexLabel(original.name())); + next.close(); + } + Assert.assertEquals("Warm request cleanup must not re-read metadata", 1, reads.get()); + PropertyKey appended = objects.newPropertyKey(IdGenerator.of(33), "appended"); + retainedLabel.properties(appended.id()); + retainedLabel.nullableKeys(appended.id()); + Assert.assertEquals("accepted", retained.property(VertexProperty.Cardinality.single, + appended.name(), "accepted").value()); + Object holder = v2CacheEventListeners().get(this.params.spaceGraphName()); + Assert.assertNotNull(holder); + Assert.assertEquals(0, holderRefCount(holder)); + Assert.assertTrue(this.params.schemaEventHub().listeners(Events.CACHE).contains(holderListener(holder))); + VertexLabel updated = objects.newVertexLabel(original.id(), "changed", IdStrategy.CUSTOMIZE_NUMBER); + metadata.set(JsonUtil.toJson(updated.asMap())); + this.params.schemaEventHub().notify(Events.CACHE, Cache.ACTION_INVALID, + HugeType.VERTEX_LABEL, original.id()).get(); + CachedSchemaTransactionV2 afterInvalidation = new CachedSchemaTransactionV2(driver, "test", requestParams); + requests.add(afterInvalidation); + Assert.assertEquals("changed", afterInvalidation.getVertexLabel(original.id()).name()); + Assert.assertEquals("Invalidation must refresh metadata even with no request lease", 2, reads.get()); + Assert.assertNotSame(retainedLabel, afterInvalidation.getVertexLabel(original.id())); + afterInvalidation.close(); + VertexLabel afterStore = objects.newVertexLabel(original.id(), "store-cleared", + IdStrategy.CUSTOMIZE_NUMBER); + metadata.set(JsonUtil.toJson(afterStore.asMap())); + EventHub storeHub = this.params.loadGraphStore().provider().storeEventHub(); + EventListener v2StoreListener = Whitebox.getInternalState(first, "storeEventListener"); + List otherStoreListeners = new ArrayList<>(storeHub.listeners(EventHub.ANY_EVENT)); + Assert.assertTrue("The V2 Store listener must remain registered without a request lease", + otherStoreListeners.remove(v2StoreListener)); + // Isolate the Store path from V1 listeners that asynchronously emit another CACHE clear. + for (EventListener listener : otherStoreListeners) { + storeHub.unlisten(EventHub.ANY_EVENT, listener); + } + try { + Assert.assertEquals(1, storeHub.notify(Events.STORE_CLEAR, + this.params.loadGraphStore().provider()).get().intValue()); + } finally { + for (EventListener listener : otherStoreListeners) { + storeHub.listen(EventHub.ANY_EVENT, listener); + } + } + Mockito.verify(driver).put(Mockito.anyString(), Mockito.anyString()); + CachedSchemaTransactionV2 afterStoreEvent = new CachedSchemaTransactionV2(driver, "test", requestParams); + requests.add(afterStoreEvent); + Assert.assertEquals("store-cleared", afterStoreEvent.getVertexLabel(original.id()).name()); + Assert.assertEquals("Store events must refresh metadata with no active request", 3, reads.get()); + afterStoreEvent.close(); + VertexLabel allLabel = null; + for (int request = 0; request < 4; request++) { + CachedSchemaTransactionV2 next = new CachedSchemaTransactionV2(driver, "test", requestParams); + requests.add(next); + List all = next.getVertexLabels(); + Assert.assertEquals(1, all.size()); + Assert.assertEquals("store-cleared", all.get(0).name()); + if (allLabel == null) { + allLabel = all.get(0); + } else { + Assert.assertSame(allLabel, all.get(0)); + } + next.close(); + } + Assert.assertEquals("Warm all-schema requests must retain the cached-all flag", 1, scans.get()); + } finally { + for (CachedSchemaTransactionV2 request : requests) { + request.close(); + } + CachedSchemaTransactionV2.closeGraph(this.params); + if (graphManagerReplaced) { + swapGraphMetaManager(originalGraphManager); + } + registered.set(previous); + } + } + + @Test + public void testV2GraphCloseReopenKeepsNewHubSafeFromOldLeases() throws Exception { + AtomicBoolean registered = metaListenerFlag(); + boolean previous = registered.getAndSet(true); + HugeGraphParams oldParams = this.params; + CachedSchemaTransactionV2 oldLease = null; + CachedSchemaTransactionV2 newLease = null; + try { + FakeObjects objects = new FakeObjects("v2-generation-schema"); + VertexLabel original = objects.newVertexLabel(IdGenerator.of(41), "generation", + IdStrategy.CUSTOMIZE_NUMBER); + AtomicInteger reads = new AtomicInteger(); + PdMetaDriver driver = Mockito.mock(PdMetaDriver.class); + Mockito.when(driver.get(Mockito.anyString())).thenAnswer(call -> { + reads.incrementAndGet(); + return JsonUtil.toJson(original.asMap()); + }); + oldLease = new CachedSchemaTransactionV2(driver, "test", oldParams); + VertexLabel oldLabel = oldLease.getVertexLabel(original.id()); + Object oldHolder = v2CacheEventListeners().get(oldParams.spaceGraphName()); + EventListener oldListener = holderListener(oldHolder); + this.cache.close(); + this.cache = null; + this.graph.close(); + this.graph = null; + Assert.assertFalse(v2CacheEventListeners().containsKey(oldParams.spaceGraphName())); + Assert.assertFalse(oldParams.schemaEventHub().listeners(Events.CACHE).contains(oldListener)); + Assert.assertEquals(0L, Whitebox.invoke(oldLease, "idCache", "size")); + this.graph = HugeFactory.open(FakeObjects.newConfig()); + this.params = Whitebox.getInternalState(this.graph, "params"); + Assert.assertNotSame(oldParams.schemaEventHub(), this.params.schemaEventHub()); + newLease = new CachedSchemaTransactionV2(driver, "test", this.params); + VertexLabel newLabel = newLease.getVertexLabel(original.id()); + Object newHolder = v2CacheEventListeners().get(this.params.spaceGraphName()); + Assert.assertNotSame(oldHolder, newHolder); + Assert.assertNotSame(oldLabel, newLabel); + Assert.assertEquals(2, reads.get()); + int newRefCount = holderRefCount(newHolder); + oldLease.close(); + CachedSchemaTransactionV2.closeGraph(oldParams); + Assert.assertSame(newHolder, v2CacheEventListeners().get(this.params.spaceGraphName())); + Assert.assertEquals(newRefCount, holderRefCount(newHolder)); + Assert.assertTrue(this.params.schemaEventHub().listeners(Events.CACHE) + .contains(holderListener(newHolder))); + Assert.assertSame(newLabel, newLease.getVertexLabel(original.id())); + Assert.assertEquals(2, reads.get()); + } finally { + if (oldLease != null) { + oldLease.close(); + } + if (newLease != null) { + newLease.close(); + } + CachedSchemaTransactionV2.closeGraph(oldParams); + CachedSchemaTransactionV2.closeGraph(this.params); + registered.set(previous); + } + } + + private HugeGraphParams v2RequestParams(HugeGraph supplier) { + Mockito.when(supplier.name()).thenReturn(this.params.name()); + Mockito.when(supplier.graphSpace()).thenReturn(this.graph.graphSpace()); + Mockito.when(supplier.spaceGraphName()).thenReturn(this.params.spaceGraphName()); + HugeGraphParams params = Mockito.mock(HugeGraphParams.class); + Mockito.when(params.graph()).thenReturn(supplier); + Mockito.when(params.name()).thenReturn(this.params.name()); + Mockito.when(params.spaceGraphName()).thenReturn(this.params.spaceGraphName()); + Mockito.when(params.configuration()).thenReturn(this.params.configuration()); + Mockito.when(params.schemaEventHub()).thenReturn(this.params.schemaEventHub()); + Mockito.when(params.loadGraphStore()).thenReturn(this.params.loadGraphStore()); + return params; + } + + @SuppressWarnings("unchecked") + private static ConcurrentMap v2CacheEventListeners() throws Exception { + Field field = CachedSchemaTransactionV2.class.getDeclaredField("SCHEMA_CACHE_EVENT_LISTENERS"); + field.setAccessible(true); + return (ConcurrentMap) field.get(null); + } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/RamTableTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/RamTableTest.java index 245f311bcb..4a529bc0e3 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/RamTableTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/RamTableTest.java @@ -17,25 +17,35 @@ package org.apache.hugegraph.unit.cache; +import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; +import java.util.List; -import org.apache.hugegraph.HugeException; import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.IdGenerator; import org.apache.hugegraph.backend.store.ram.RamTable; -import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.schema.SchemaManager; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.unit.FakeObjects; import org.junit.After; import org.junit.Before; import org.junit.Test; +import com.google.common.collect.ImmutableList; + public class RamTableTest { // max value is 4 billion @@ -81,6 +91,129 @@ private HugeGraph graph() { return this.graph; } + @Test + public void testMatchedLabelCandidateContract() { + RamTable table = new RamTable(this.graph(), 10, 20); + table.addEdge(true, 1, 2, Directions.OUT, 1); + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + query.query(Condition.in(HugeKeys.LABEL, ImmutableList.of(IdGenerator.of(1)))); + Assert.assertTrue(table.matched(query)); + + query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + query.query(Condition.in(HugeKeys.LABEL, + ImmutableList.of(IdGenerator.of(1), IdGenerator.of(2)))); + Assert.assertTrue(table.matched(query)); + query.eq(HugeKeys.LABEL, IdGenerator.of(3)); + Assert.assertFalse(table.matched(query)); + } + + @Test + public void testQueryBranchRejectsUnresolvedLabels() { + RamTable table = new RamTable(this.graph(), 10, 20); + table.addEdge(true, 1, 2, Directions.OUT, 1); + for (Condition[] labels : new Condition[][]{ + {Condition.in(HugeKeys.LABEL, ImmutableList.of())}, + {Condition.eq(HugeKeys.LABEL, IdGenerator.of(1)), + Condition.eq(HugeKeys.LABEL, IdGenerator.of(2))}, + {Condition.in(HugeKeys.LABEL, + ImmutableList.of(IdGenerator.of(1), IdGenerator.of(2)))}}) { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + for (Condition label : labels) { + query.query(label); + } + // Bypass matched()/flatten(): the branch itself must fail closed, + // independently of Java assertions and the caller's invariants. + Assert.assertThrows(IllegalStateException.class, () -> + Whitebox.invoke(RamTable.class, "query", table, query)); + } + } + + @Test + public void testQueryBranchWildcardAndSingleLabel() { + HugeGraph graph = this.graph(); + int el1 = (int) graph.edgeLabel("el1").id().asLong(); + int el2 = (int) graph.edgeLabel("el2").id().asLong(); + RamTable table = new RamTable(graph, 10, 20); + table.addEdge(true, 1, 2, Directions.OUT, el1); + table.addEdge(false, 1, 3, Directions.OUT, el2); + for (boolean wildcard : new boolean[]{true, false}) { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + if (!wildcard) { + query.eq(HugeKeys.LABEL, IdGenerator.of(el1)); + } + Iterator edges = Whitebox.invoke(RamTable.class, "query", table, query); + List targets = new ArrayList<>(); + edges.forEachRemaining(edge -> targets.add(edge.id().otherVertexId().asLong())); + Collections.sort(targets); + Assert.assertEquals(wildcard ? ImmutableList.of(2L, 3L) : ImmutableList.of(2L), targets); + } + } + + @Test + public void testQueryByMultipleLabels() { + HugeGraph graph = this.graph(); + int el1 = (int) graph.edgeLabel("el1").id().asLong(); + int el2 = (int) graph.edgeLabel("el2").id().asLong(); + int other = (int) graph.schema().edgeLabel("other") + .sourceLabel("vl1").targetLabel("vl1").create().id().asLong(); + RamTable table = new RamTable(graph, 10, 20); + table.addEdge(true, 1, 2, Directions.OUT, el1); + table.addEdge(false, 1, 3, Directions.IN, el2); + table.addEdge(false, 1, 4, Directions.OUT, other); + for (Directions direction : Directions.values()) { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + if (direction == Directions.BOTH) { + query.query(Condition.or(Condition.eq(HugeKeys.DIRECTION, Directions.OUT), + Condition.eq(HugeKeys.DIRECTION, Directions.IN))); + } else { + query.eq(HugeKeys.DIRECTION, direction); + } + query.query(Condition.in(HugeKeys.LABEL, ImmutableList.of( + IdGenerator.of(el1), IdGenerator.of(el2), IdGenerator.of(el1)))); + Assert.assertTrue(table.matched(query)); + List targets = new ArrayList<>(); + table.query(query).forEachRemaining(edge -> { + Assert.assertEquals(1L, edge.id().ownerVertexId().asLong()); + Assert.assertEquals(edge.direction() == Directions.OUT ? "el1" : "el2", + edge.label()); + targets.add(edge.id().otherVertexId().asLong()); + }); + Collections.sort(targets); + Assert.assertEquals(direction == Directions.BOTH ? ImmutableList.of(2L, 3L) : + ImmutableList.of(direction == Directions.OUT ? 2L : 3L), targets); + } + } + + @Test + public void testMatchedRejectsUnsafeLabelCandidates() { + RamTable table = new RamTable(this.graph(), 10, 20); + table.addEdge(true, 1, 2, Directions.OUT, 1); + for (Condition label : new Condition[]{ + Condition.in(HugeKeys.LABEL, ImmutableList.of()), + Condition.neq(HugeKeys.LABEL, IdGenerator.of(1)), + Condition.in(HugeKeys.LABEL, ImmutableList.of("el1", "el2")), + Condition.eq(HugeKeys.LABEL, IdGenerator.ZERO), + Condition.eq(HugeKeys.LABEL, IdGenerator.of(-1)), + Condition.eq(HugeKeys.LABEL, IdGenerator.of(1L << 32))}) { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + query.query(label); + Assert.assertFalse(table.matched(query)); + } + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + Assert.assertTrue(table.matched(query)); + query.query(Condition.in(HugeKeys.LABEL, + ImmutableList.of(IdGenerator.of(1), IdGenerator.of(2)))); + query.query(Condition.neq(HugeKeys.LABEL, IdGenerator.of(1))); + Assert.assertFalse(table.matched(query)); + } + @Test public void testAddAndQuery() throws Exception { HugeGraph graph = this.graph(); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/RequestCacheLifetimeTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/RequestCacheLifetimeTest.java new file mode 100644 index 0000000000..812343be30 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/RequestCacheLifetimeTest.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.hugegraph.unit.cache; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.hugegraph.HugeFactory; +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.HugeGraphParams; +import org.apache.hugegraph.api.graph.VertexAPI; +import org.apache.hugegraph.api.graph.EdgeAPI; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.util.JsonUtil; +import org.apache.hugegraph.backend.cache.CachedGraphTransaction; +import org.apache.hugegraph.backend.cache.CachedSchemaTransaction; +import org.apache.hugegraph.util.Events; +import org.apache.hugegraph.dist.RegisterUtil; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.unit.FakeObjects; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.tinkerpop.gremlin.structure.Direction; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.junit.Test; +import sun.misc.Unsafe; + +public class RequestCacheLifetimeTest { + + @Test + public void testVertexSchemaAppendAfterRequestCleanupWithWarmElementCaches() throws Exception { + this.checkSchemaAppendAfterRequestCleanup(false); + } + + @Test + public void testEdgeSchemaAppendAfterRequestCleanupWithWarmElementCaches() throws Exception { + this.checkSchemaAppendAfterRequestCleanup(true); + } + + @Test + public void testFailedReopenPreservesCauseAndDeletesDirectory() throws Exception { + Path path = Files.createTempDirectory("request-cache-reopen-failure"); + RuntimeException failure = null; + try { + this.checkSchemaAppendAfterRequestCleanup(false, path, true); + Assert.fail("The invalid backend must fail during reopen"); + } catch (RuntimeException expected) { + failure = expected; + } + System.out.println("REOPEN_FAILURE=" + failure + "; DIRECTORY_EXISTS=" + Files.exists(path)); + Assert.assertNotNull(failure); + Assert.assertTrue(failure instanceof org.apache.hugegraph.exception.HugeException); + Assert.assertEquals("Failed to load backend store provider", failure.getMessage()); + Assert.assertFalse(Files.exists(path)); + } + + private void checkSchemaAppendAfterRequestCleanup(boolean edgeFirst) throws Exception { + Path path = Files.createTempDirectory("request-cache-lifetime"); + this.checkSchemaAppendAfterRequestCleanup(edgeFirst, path, false); + } + + private void checkSchemaAppendAfterRequestCleanup(boolean edgeFirst, Path path, + boolean failReopen) throws Exception { + RegisterUtil.registerBackends(); + HugeConfig config = FakeObjects.newConfig(); + config.setProperty("backend", "rocksdb"); + config.setProperty("serializer", "binary"); + config.setProperty("store", edgeFirst ? "request_cache_lifetime_edge" : "request_cache_lifetime_vertex"); + config.setProperty("rocksdb.data_path", path.toString()); + config.setProperty("rocksdb.wal_path", path.toString()); + HugeGraph graph = HugeFactory.open(config); + try { + graph.initBackend(); + graph.schema().vertexLabel("person").useCustomizeNumberId().create(); + graph.schema().edgeLabel("knows").sourceLabel("person").targetLabel("person").create(); + Vertex first = graph.addVertex(T.label, "person", T.id, 1L); + Vertex second = graph.addVertex(T.label, "person", T.id, 2L); + Edge edge = first.addEdge("knows", second); + Object edgeId = edge.id(); + graph.tx().commit(); + Vertex warm = graph.vertices(1L).next(); + warm.edges(Direction.OUT, "knows").next(); + HugeGraphParams params = Whitebox.getInternalState(graph, "params"); + CachedGraphTransaction tx = (CachedGraphTransaction) params.graphTransaction(); + Assert.assertTrue((long) Whitebox.invoke(tx, "verticesCache", "size") > 0L); + Assert.assertTrue((long) Whitebox.invoke(tx, "edgesCache", "size") > 0L); + HugeFactory.closeCurrentThreadTransactions(); + + graph.schema().propertyKey("added").asText().create(); + graph.schema().vertexLabel("person").properties("added").nullableKeys("added").append(); + graph.schema().edgeLabel("knows").properties("added").nullableKeys("added").append(); + HugeFactory.closeCurrentThreadTransactions(); + + Vertex refetched = graph.vertices(1L).next(); + Edge refetchedEdge = refetched.edges(Direction.OUT, "knows").next(); + if (edgeFirst) { + refetchedEdge.property("added", "edge-value"); + refetched.property("added", "vertex-value"); + } else { + refetched.property("added", "vertex-value"); + refetchedEdge.property("added", "edge-value"); + } + graph.tx().commit(); + HugeFactory.closeCurrentThreadTransactions(); + Assert.assertEquals("vertex-value", graph.vertices(1L).next().value("added")); + Assert.assertEquals("edge-value", graph.edges(edgeId).next().value("added")); + HugeFactory.closeCurrentThreadTransactions(); + updateThroughApi(graph, VertexAPI.class, "JsonVertex", "1", "vertex-api-value"); + HugeFactory.closeCurrentThreadTransactions(); + updateThroughApi(graph, EdgeAPI.class, "JsonEdge", edgeId.toString(), "edge-api-value"); + HugeFactory.closeCurrentThreadTransactions(); + Assert.assertEquals("vertex-api-value", graph.vertices(1L).next().value("added")); + Assert.assertEquals("edge-api-value", graph.edges(edgeId).next().value("added")); + CachedGraphTransaction idleGraph = (CachedGraphTransaction) params.graphTransaction(); + CachedSchemaTransaction idleSchema = (CachedSchemaTransaction) params.schemaTransaction(); + HugeFactory.closeCurrentThreadTransactions(); + Assert.assertTrue((long) Whitebox.invoke(idleGraph, "verticesCache", "size") > 0L); + Assert.assertTrue((long) Whitebox.invoke(idleSchema, "idCache", "size") > 0L); + params.loadGraphStore().provider().storeEventHub() + .notify(Events.STORE_CLEAR, params.loadGraphStore().provider()).get(); + Assert.assertEquals(0L, Whitebox.invoke(idleGraph, "verticesCache", "size")); + Assert.assertEquals(0L, Whitebox.invoke(idleGraph, "edgesCache", "size")); + Assert.assertEquals(0L, Whitebox.invoke(idleSchema, "idCache", "size")); + graph.close(); + graph = null; + if (failReopen) { + config.setProperty("backend", "invalid_reopen_backend"); + } + graph = HugeFactory.open(config); + Assert.assertEquals("vertex-api-value", graph.vertices(1L).next().value("added")); + Assert.assertEquals("edge-api-value", graph.edges(edgeId).next().value("added")); + } finally { + try { + HugeFactory.closeCurrentThreadTransactions(); + if (graph != null) { + try { + graph.clearBackend(); + } finally { + graph.close(); + } + } + } finally { + try (java.util.stream.Stream files = Files.walk(path)) { + for (Path file : files.sorted(java.util.Comparator.reverseOrder()).toArray(Path[]::new)) { + Files.deleteIfExists(file); + } + } + } + } + } + + private static void updateThroughApi(HugeGraph graph, Class api, String bodyClass, + String id, String value) throws Exception { + // Match existing API fixtures: avoid constructor-started RPC/PD services. + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + GraphManager manager = (GraphManager) ((Unsafe) field.get(null)).allocateInstance(GraphManager.class); + Whitebox.setInternalState(manager, "graphs", Collections.singletonMap(graph.spaceGraphName(), graph)); + Class bodyType = Class.forName(api.getName() + "$" + bodyClass); + Object body = JsonUtil.fromJson("{\"properties\":{\"added\":\"" + value + "\"}}", bodyType); + Method update = api.getMethod("update", GraphManager.class, String.class, String.class, + String.class, String.class, bodyType); + String response = (String) update.invoke(api.getConstructor().newInstance(), manager, + "DEFAULT", graph.name(), id, "append", body); + Assert.assertTrue(response.contains(value)); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GraphBinarySchemaCompatibilityTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GraphBinarySchemaCompatibilityTest.java new file mode 100644 index 0000000000..f8b1d704c7 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GraphBinarySchemaCompatibilityTest.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.unit.config; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.hugegraph.backend.Shard; +import org.apache.hugegraph.backend.tx.ISchemaTransaction; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder; +import org.apache.hugegraph.schema.SchemaManager; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.Frequency; +import org.apache.hugegraph.type.define.GraphMode; +import org.apache.hugegraph.type.define.IdStrategy; +import org.apache.hugegraph.type.define.IndexType; +import org.apache.hugegraph.unit.BaseUnitTest; +import org.apache.hugegraph.unit.FakeObjects; +import org.apache.tinkerpop.gremlin.process.traversal.Order; +import org.apache.tinkerpop.gremlin.structure.Direction; +import org.apache.tinkerpop.gremlin.util.message.ResponseMessage; +import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode; +import org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1; +import org.junit.Test; +import org.mockito.Mockito; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +public class GraphBinarySchemaCompatibilityTest extends BaseUnitTest { + + @Test + public void testShardUsesExistingGraphSONFieldsWithStandardClient() throws Exception { + Shard shard = new Shard("start", "end", Long.MAX_VALUE); + Assert.assertEquals(Map.of("start", "start", "end", "end", "length", Long.MAX_VALUE), + roundTrip(shard)); + } + + @Test + public void testShardsInsideListsAndMapsWithStandardClient() throws Exception { + Shard shard = new Shard("a", "z", 1048576L); + Map expected = Map.of("start", "a", "end", "z", "length", 1048576L); + Assert.assertEquals(List.of(expected), roundTrip(List.of(shard))); + Assert.assertEquals(Map.of("splits", List.of(expected)), + roundTrip(Map.of("splits", List.of(shard)))); + } + + @Test + public void testShardPreservesEmptyAndNullBoundaries() throws Exception { + Assert.assertEquals(Map.of("start", "", "end", "", "length", 0L), + roundTrip(new Shard("", "", 0L))); + Map result = (Map) roundTrip(new Shard(null, null, Long.MAX_VALUE)); + Assert.assertEquals(3, result.size()); + Assert.assertTrue(result.containsKey("start")); + Assert.assertTrue(result.containsKey("end")); + Assert.assertNull(result.get("start")); + Assert.assertNull(result.get("end")); + Assert.assertEquals(Long.MAX_VALUE, result.get("length")); + } + + @Test + public void testHugeGraphEnumsUseNamesWithStandardClient() throws Exception { + Assert.assertEquals("OUT", roundTrip(Directions.OUT)); + // DataType constants have individual enum subclasses. + Assert.assertEquals("TEXT", roundTrip(DataType.TEXT)); + Assert.assertEquals("SINGLE", roundTrip(Cardinality.SINGLE)); + Assert.assertEquals("NONE", roundTrip(GraphMode.NONE)); + Assert.assertEquals("VERTEX_LABEL", roundTrip(HugeType.VERTEX_LABEL)); + } + + @Test + public void testNestedEnumsPreserveNativeTinkerPopEnums() throws Exception { + Map result = Map.of( + Directions.OUT, List.of(DataType.TEXT, Direction.OUT, Order.asc)); + Assert.assertEquals(Map.of("OUT", List.of("TEXT", Direction.OUT, Order.asc)), + roundTrip(result)); + Assert.assertEquals(Direction.IN, roundTrip(Direction.IN)); + Assert.assertEquals(Order.desc, roundTrip(Order.desc)); + } + + @Test + public void testEmptySchemaManagerUsesAllFourLists() throws Exception { + FakeObjects objects = new FakeObjects(); + SchemaManager schema = schemaManager(objects); + Assert.assertEquals(Map.of("propertykeys", List.of(), + "vertexlabels", List.of(), + "edgelabels", List.of(), + "indexlabels", List.of()), roundTrip(schema)); + } + + @Test + public void testSchemaManagerConvertsAllSchemasAndHidesInternalNames() throws Exception { + FakeObjects objects = new FakeObjects(); + PropertyKey key = objects.newPropertyKey(IdGenerator.of(1), "name"); + PropertyKey hidden = objects.newPropertyKey(IdGenerator.of(2), "~internal"); + VertexLabel vertex = objects.newVertexLabel(IdGenerator.of(3), "person", + IdStrategy.AUTOMATIC, key.id()); + EdgeLabel edge = objects.newEdgeLabel(IdGenerator.of(4), "knows", Frequency.SINGLE, + vertex.id(), vertex.id(), key.id()); + IndexLabel index = objects.newIndexLabel(IdGenerator.of(5), "by_name", + HugeType.VERTEX_LABEL, vertex.id(), + IndexType.SECONDARY, key.id()); + Mockito.when(objects.graph().propertyKeys()).thenReturn(List.of(key, hidden)); + Mockito.when(objects.graph().vertexLabels()).thenReturn(List.of(vertex)); + Mockito.when(objects.graph().edgeLabels()).thenReturn(List.of(edge)); + Mockito.when(objects.graph().indexLabels()).thenReturn(List.of(index)); + + Map result = (Map) roundTrip(schemaManager(objects)); + assertSchema(result, "propertykeys", "name"); + assertSchema(result, "vertexlabels", "person"); + assertSchema(result, "edgelabels", "knows"); + assertSchema(result, "indexlabels", "by_name"); + Map property = (Map) ((List) result.get("propertykeys")).get(0); + Assert.assertEquals("TEXT", property.get("data_type")); + Assert.assertEquals("SINGLE", property.get("cardinality")); + } + + @Test + public void testSchemaManagerAndSchemaElementsInsideCollections() throws Exception { + FakeObjects objects = new FakeObjects(); + PropertyKey key = objects.newPropertyKey(IdGenerator.of(1), "name"); + Mockito.when(objects.graph().propertyKeys()).thenReturn(List.of(key)); + SchemaManager schema = schemaManager(objects); + Map result = (Map) roundTrip(Map.of("nested", List.of(schema, key))); + List nested = (List) result.get("nested"); + assertSchema((Map) nested.get(0), "propertykeys", "name"); + Assert.assertEquals("name", ((Map) nested.get(1)).get("name")); + } + + @Test + public void testNestedSchemaMetadataKeepsStandardWireValues() throws Exception { + FakeObjects objects = new FakeObjects(); + PropertyKey key = objects.newPropertyKey(IdGenerator.of(1), "name"); + PropertyKey child = objects.newPropertyKey(IdGenerator.of(2), "age", DataType.INT); + key.userdata("nested", Map.of("key", child, + "values", List.of(Directions.OUT, Direction.OUT, Order.asc))); + Map result = (Map) roundTrip(key); + Map metadata = (Map) ((Map) result.get("user_data")).get("nested"); + Assert.assertEquals("age", ((Map) metadata.get("key")).get("name")); + Assert.assertEquals(List.of("OUT", Direction.OUT, Order.asc), metadata.get("values")); + } + + private static SchemaManager schemaManager(FakeObjects objects) { + return new SchemaManager(Mockito.mock(ISchemaTransaction.class), objects.graph()); + } + + private static void assertSchema(Map result, String type, String name) { + List schemas = (List) result.get(type); + Assert.assertEquals(1, schemas.size()); + Assert.assertEquals(name, ((Map) schemas.get(0)).get("name")); + } + + private static Object roundTrip(Object value) throws Exception { + GraphBinaryMessageSerializerV1 server = new GraphBinaryMessageSerializerV1(); + server.configure(Map.of("builder", HugeGraphTypeSerializerRegistryBuilder.class.getName()), + Collections.emptyMap()); + GraphBinaryMessageSerializerV1 client = new GraphBinaryMessageSerializerV1(); + ResponseMessage response = ResponseMessage.build(UUID.randomUUID()) + .code(ResponseStatusCode.SUCCESS) + .result(value) + .create(); + ByteBuf buffer = server.serializeResponseAsBinary(response, ByteBufAllocator.DEFAULT); + try { + return client.deserializeResponse(buffer).getResult().getData(); + } finally { + buffer.release(); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java new file mode 100644 index 0000000000..d66a5aca8c --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/GremlinConfigCompatibilityTest.java @@ -0,0 +1,1515 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.unit.config; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.io.HugeGraphIoRegistry; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.structure.HugeEdge; +import org.apache.hugegraph.structure.HugeFeatures; +import org.apache.hugegraph.structure.HugeVertex; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.unit.BaseUnitTest; +import org.apache.hugegraph.unit.FakeObjects; +import org.apache.hugegraph.util.Blob; +import org.apache.hugegraph.util.JsonUtil; +import org.apache.tinkerpop.gremlin.driver.Result; +import org.apache.tinkerpop.gremlin.groovy.jsr223.GremlinGroovyScriptEngine; +import org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin; +import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.MutablePath; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree; +import org.apache.tinkerpop.gremlin.server.Settings; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONMapper; +import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONVersion; +import org.apache.tinkerpop.gremlin.structure.io.graphson.TypeInfo; +import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdge; +import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedProperty; +import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex; +import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertexProperty; +import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdge; +import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertex; +import org.apache.tinkerpop.gremlin.util.MessageSerializer; +import org.apache.tinkerpop.gremlin.util.message.RequestMessage; +import org.apache.tinkerpop.gremlin.util.message.ResponseMessage; +import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode; +import org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1; +import org.apache.tinkerpop.gremlin.util.ser.MessageTextSerializer; +import org.junit.Test; + +import groovy.lang.GString; +import groovy.lang.GroovyShell; +import org.mockito.Mockito; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.error.YAMLException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +public class GremlinConfigCompatibilityTest extends BaseUnitTest { + + @Test + public void testGremlinSettingsRejectsJavaObjectTags() { + UnexpectedYamlType.constructions = 0; + String yaml = "host: localhost\nscriptEngines:\n gremlin-groovy:\n" + + " config:\n value: !!" + UnexpectedYamlType.class.getName() + " {}\n"; + Assert.assertThrows(YAMLException.class, () -> Settings.read( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8)))); + Assert.assertEquals(0, UnexpectedYamlType.constructions); + } + + public static class UnexpectedYamlType { + + private static int constructions; + + public UnexpectedYamlType() { + constructions++; + } + } + + private static final Pattern CLASS_NAME = + Pattern.compile("className:\\s*([^,}\\s]+)"); + private static final Pattern XML_COMMENT = + Pattern.compile("", Pattern.DOTALL); + private static final Pattern TINKERPOP_DEPENDENCY = Pattern.compile( + "\\s*org\\.apache\\.tinkerpop" + + "(.*?)", Pattern.DOTALL); + private static final Pattern DEPENDENCY_VERSION = + Pattern.compile("(.*?)", Pattern.DOTALL); + private static final Pattern TINKERPOP_VERSION_PROPERTY = Pattern.compile( + "(.*?)"); + private static final Pattern GUICE_DEPENDENCY = Pattern.compile( + "\\s*com\\.google\\.inject" + + "(.*?)", Pattern.DOTALL); + private static final String SUPPORTED_TINKERPOP_VERSION = "3.8.1"; + /* Must match the guice.version property of the TinkerPop parent pom + * (gremlin-test declares guice as a provided dependency, so HugeGraph + * must supply the version TinkerPop itself was built against). */ + private static final String SUPPORTED_GUICE_VERSION = "4.2.3"; + private static final String SERIALIZER_PACKAGE = + "org.apache.tinkerpop.gremlin.util.ser."; + private static final String GRAPHSON_UNTYPED_V1 = + SERIALIZER_PACKAGE + "GraphSONUntypedMessageSerializerV1"; + private static final String GRAPHBINARY_V1 = + SERIALIZER_PACKAGE + "GraphBinaryMessageSerializerV1"; + private static final String GRAPHBINARY_BUILDER = + "org.apache.hugegraph.io.HugeGraphTypeSerializerRegistryBuilder"; + private static final String IO_REGISTRY = + "org.apache.hugegraph.io.HugeGraphIoRegistry"; + private static final String GREMLIN_SERVER_CONFIG = "gremlin-server.yaml"; + private static final String REMOTE_OBJECTS_CONFIG = "remote-objects.yaml"; + private static final List GREMLIN_SERVER_CONFIG_VARIANTS = + Arrays.asList( + "static/conf/gremlin-server.yaml", + "travis/conf-raft1/gremlin-server.yaml", + "travis/conf-raft2/gremlin-server.yaml", + "travis/conf-raft3/gremlin-server.yaml" + ); + private static final List DRIVER_CONFIGS = Arrays.asList( + "gremlin-driver-settings.yaml", "remote.yaml" + ); + private static final List REMOTE_CONFIGS = Arrays.asList( + "gremlin-driver-settings.yaml", + "remote.yaml", + REMOTE_OBJECTS_CONFIG + ); + private static final List TYPED_FALLBACK_SERIALIZERS = + Arrays.asList( + SERIALIZER_PACKAGE + "GraphSONMessageSerializerV2", + SERIALIZER_PACKAGE + "GraphSONMessageSerializerV3" + ); + private static final List TYPED_GRAPHSON_MIME_TYPES = + Arrays.asList( + "application/vnd.gremlin-v2.0+json", + "application/vnd.gremlin-v3.0+json" + ); + private static final List UNTYPED_GRAPHSON_MIME_TYPES = + Arrays.asList( + "application/vnd.gremlin-v1.0+json;types=false", + "application/vnd.gremlin-v2.0+json;types=false", + "application/vnd.gremlin-v3.0+json;types=false" + ); + + @Test + public void testGremlinServerSerializersUseTinkerPopUtilPackage() throws IOException { + String content = readConfig(GREMLIN_SERVER_CONFIG); + + assertUsesHugeGraphIoRegistry(GREMLIN_SERVER_CONFIG, content); + assertSerializerClassNamesUseUtilPackage(GREMLIN_SERVER_CONFIG, + content); + } + + @Test + public void testTinkerPopPomVersionsUseSupportedVersion() + throws IOException { + List mismatches = new ArrayList<>(); + try (Stream files = Files.walk(repositoryRoot())) { + files.filter(path -> "pom.xml".equals( + path.getFileName().toString())) + .forEach(path -> collectTinkerPopVersionMismatches( + path, mismatches)); + } + + Assert.assertTrue("TinkerPop dependencies must use " + + SUPPORTED_TINKERPOP_VERSION + " and guice must " + + "use " + SUPPORTED_GUICE_VERSION + ": " + mismatches, + mismatches.isEmpty()); + } + + @Test + public void testRemoteSerializersUseTinkerPopUtilPackage() throws IOException { + for (String file : REMOTE_CONFIGS) { + String content = readConfig(file); + + assertUsesHugeGraphIoRegistry(file, content); + assertSerializerClassNamesUseUtilPackage(file, content); + } + } + + @Test + public void testConfiguredSerializerClassesAreLoadable() throws Exception { + assertConfiguredSerializerClassesAreLoadable( + GREMLIN_SERVER_CONFIG, readConfig(GREMLIN_SERVER_CONFIG)); + for (String file : REMOTE_CONFIGS) { + assertConfiguredSerializerClassesAreLoadable(file, + readConfig(file)); + } + } + + @Test + public void testGremlinServerConfigVariantsSupportGraphSONMimeTypes() + throws Exception { + Path assembly = serverAssemblyPath(); + + for (String variant : GREMLIN_SERVER_CONFIG_VARIANTS) { + Settings settings = Settings.read(assembly.resolve(variant) + .toString()); + + assertSupportsTypedAndUntypedGraphSONMimeTypes(variant, + graphSONMimeTypes(settings)); + } + } + + @Test + public void testGremlinServerConfigVariantsRejectTypedGraphSONV1() throws Exception { + String javaType = UnexpectedGraphSONType.class.getName(); + String requestJson = "{\"requestId\":\"" + UUID.randomUUID() + + "\",\"op\":\"eval\",\"processor\":\"\",\"args\":{" + + "\"gremlin\":\"1\",\"bindings\":{\"value\":{\"@class\":\"" + + javaType + "\"}}}}"; + for (String variant : GREMLIN_SERVER_CONFIG_VARIANTS) { + Settings settings = Settings.read(serverAssemblyPath().resolve(variant).toString()); + Map mimeTypes = graphSONMimeTypes(settings); + Assert.assertFalse(variant + " must not expose Java class-name typing", + mimeTypes.containsKey("application/vnd.gremlin-v1.0+json")); + for (Settings.SerializerSettings setting : settings.serializers) { + Assert.assertNotEquals(variant, SERIALIZER_PACKAGE + "GraphSONMessageSerializerV1", + setting.className); + if (!GRAPHSON_UNTYPED_V1.equals(setting.className)) { + continue; + } + MessageTextSerializer serializer = newTextSerializer(setting.className); + serializer.configure(config(setting.config), Collections.emptyMap()); + UnexpectedGraphSONType.constructions = 0; + RequestMessage request = serializer.deserializeRequest(requestJson); + Map bindings = (Map) request.getArgs().get("bindings"); + Assert.assertEquals(variant, Map.of("@class", javaType), bindings.get("value")); + Assert.assertEquals(variant, 0, UnexpectedGraphSONType.constructions); + } + Assert.assertEquals(variant, GRAPHSON_UNTYPED_V1, mimeTypes.get("application/json")); + } + } + + public static class UnexpectedGraphSONType { + + private static int constructions; + + public UnexpectedGraphSONType() { + constructions++; + } + } + + @Test + @SuppressWarnings("unchecked") + public void testGremlinServerConfigVariantsImportCompatibleCypherFactories() throws Exception { + String factory = "org.apache.hugegraph.opencypher.CypherGremlinPredicates"; + for (String variant : GREMLIN_SERVER_CONFIG_VARIANTS) { + Settings settings = Settings.read(serverAssemblyPath().resolve(variant).toString()); + Map imports = settings.scriptEngines.get("gremlin-groovy") + .plugins.get(ImportGremlinPlugin.class.getName()); + Collection classes = (Collection) imports.get("classImports"); + Collection methods = (Collection) imports.get("methodImports"); + Assert.assertTrue(variant, classes.contains(factory)); + Assert.assertTrue(variant, methods.contains(factory + "#*")); + Assert.assertFalse(variant, classes.contains("org.opencypher.gremlin.traversal.CustomPredicate")); + ImportGremlinPlugin plugin = ImportGremlinPlugin.build().classImports(classes) + .methodImports(methods).create(); + GremlinGroovyScriptEngine engine = new GremlinGroovyScriptEngine( + plugin.getCustomizers("gremlin-groovy").orElseThrow()); + Assert.assertEquals(variant, Arrays.asList(true, true, false, false), + engine.eval("[cypherRegex('mar.*').test('marko'), " + + "cypherIsString().test('marko'), " + + "cypherIsNode().test(null), " + + "cypherIsRelationship().test(null)]")); + } + } + + @Test + public void testGremlinServerConfigVariantsUseHugeGraphBinaryBuilder() + throws Exception { + Path assembly = serverAssemblyPath(); + + for (String variant : GREMLIN_SERVER_CONFIG_VARIANTS) { + Settings settings = Settings.read(assembly.resolve(variant) + .toString()); + boolean found = false; + for (Settings.SerializerSettings serializer : + settings.serializers) { + if (!serializer.className.startsWith(SERIALIZER_PACKAGE + + "GraphBinary")) { + continue; + } + Assert.assertNotNull(variant, serializer.config); + Assert.assertEquals(variant, GRAPHBINARY_BUILDER, + serializer.config.get("builder")); + found = true; + } + Assert.assertTrue("No GraphBinary serializer in " + variant, + found); + } + } + + private static void assertSupportsTypedAndUntypedGraphSONMimeTypes( + String fileName, Map graphSONMimeTypes) { + for (String mimeType : UNTYPED_GRAPHSON_MIME_TYPES) { + Assert.assertTrue(fileName + " should support untyped " + + "GraphSON MIME " + mimeType, + graphSONMimeTypes.containsKey(mimeType)); + } + for (String mimeType : TYPED_GRAPHSON_MIME_TYPES) { + Assert.assertTrue(fileName + " should support typed GraphSON " + + "MIME " + mimeType, + graphSONMimeTypes.containsKey(mimeType)); + } + Assert.assertEquals(fileName + " should keep application/json " + + "mapped to the untyped V1 serializer", + GRAPHSON_UNTYPED_V1, + graphSONMimeTypes.get("application/json")); + } + + @Test + public void testConfiguredGraphSONSerializersCanSerializeHugeGraphTypes() + throws Exception { + Settings settings = readGremlinServerSettings(); + List typedSerializers = new ArrayList<>(); + boolean foundUntyped = false; + + for (Settings.SerializerSettings serializerSettings : + settings.serializers) { + if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE + + "GraphSON")) { + continue; + } + + MessageTextSerializer serializer = + newTextSerializer(serializerSettings.className); + serializer.configure(config(serializerSettings.config), + Collections.emptyMap()); + boolean typed = !serializerSettings.className.startsWith( + SERIALIZER_PACKAGE + "GraphSONUntyped"); + if (typed) { + typedSerializers.add(serializerSettings.className); + } else { + foundUntyped = true; + } + assertCanSerializeHugeGraphTypes(serializer, typed); + } + + Assert.assertTrue("No untyped GraphSON serializer settings found in " + + GREMLIN_SERVER_CONFIG, foundUntyped); + Assert.assertEquals("Configured typed GraphSON serializers should " + + "match the fallback set", + TYPED_FALLBACK_SERIALIZERS, typedSerializers); + } + + @Test + public void testTypedFallbackSerializersCanRoundTripHugeGraphIds() + throws Exception { + Map config = graphSONV1Config( + readGremlinServerSettings()); + + for (String serializer : TYPED_FALLBACK_SERIALIZERS) { + MessageTextSerializer textSerializer = + newTextSerializer(serializer); + + textSerializer.configure(config(config), Collections.emptyMap()); + assertCanRoundTripHugeGraphIds(serializer, textSerializer); + } + } + + @Test + public void testConfiguredGraphBinarySerializersCanRoundTripStandardPredicate() + throws Exception { + Settings settings = readGremlinServerSettings(); + boolean found = false; + + for (Settings.SerializerSettings serializerSettings : + settings.serializers) { + if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE + + "GraphBinary")) { + continue; + } + + MessageSerializer serializer = + newMessageSerializer(serializerSettings.className); + serializer.configure(config(serializerSettings.config), + Collections.emptyMap()); + assertCanRoundTripStandardPredicate(serializerSettings.className, + serializer); + found = true; + } + + Assert.assertTrue("No GraphBinary serializer settings found in " + + GREMLIN_SERVER_CONFIG, found); + } + + @Test + public void testConfiguredGraphBinarySerializersCanRoundTripElementProperties() + throws Exception { + Settings settings = readGremlinServerSettings(); + boolean found = false; + + for (Settings.SerializerSettings serializerSettings : + settings.serializers) { + if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE + + "GraphBinary")) { + continue; + } + + MessageSerializer serializer = + newMessageSerializer(serializerSettings.className); + serializer.configure(config(serializerSettings.config), + Collections.emptyMap()); + assertCanRoundTripElementProperties(serializerSettings.className, + serializer); + found = true; + } + + Assert.assertTrue("No GraphBinary serializer settings found in " + + GREMLIN_SERVER_CONFIG, found); + } + + @Test + public void testConfiguredGraphBinarySerializersCanRoundTripHugeGraphElements() + throws Exception { + Settings settings = readGremlinServerSettings(); + boolean found = false; + + for (Settings.SerializerSettings serializerSettings : + settings.serializers) { + if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE + + "GraphBinary")) { + continue; + } + + MessageSerializer serializer = + newMessageSerializer(serializerSettings.className); + serializer.configure(config(serializerSettings.config), + Collections.emptyMap()); + assertCanRoundTripHugeGraphElements(serializerSettings.className, + serializer); + found = true; + } + + Assert.assertTrue("No GraphBinary serializer settings found in " + + GREMLIN_SERVER_CONFIG, found); + } + + @Test + public void testConfiguredGraphBinarySerializersUsePrimitiveHugeGraphIds() + throws Exception { + Settings settings = readGremlinServerSettings(); + boolean found = false; + + for (Settings.SerializerSettings serializerSettings : + settings.serializers) { + if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE + + "GraphBinary")) { + continue; + } + + MessageSerializer serializer = + newMessageSerializer(serializerSettings.className); + serializer.configure(config(serializerSettings.config), + Collections.emptyMap()); + assertUsesPrimitiveHugeGraphIds(serializerSettings.className, + serializer); + found = true; + } + + Assert.assertTrue("No GraphBinary serializer settings found in " + + GREMLIN_SERVER_CONFIG, found); + } + + @Test + public void testConfiguredGraphBinarySerializersCanRoundTripReferenceElements() + throws Exception { + Settings settings = readGremlinServerSettings(); + boolean found = false; + + for (Settings.SerializerSettings serializerSettings : + settings.serializers) { + if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE + + "GraphBinary")) { + continue; + } + + MessageSerializer serializer = + newMessageSerializer(serializerSettings.className); + serializer.configure(config(serializerSettings.config), + Collections.emptyMap()); + assertCanRoundTripReferenceElements(serializerSettings.className, + serializer); + found = true; + } + + Assert.assertTrue("No GraphBinary serializer settings found in " + + GREMLIN_SERVER_CONFIG, found); + } + + @Test + public void testConfiguredGraphSONSerializersIncludeElementProperties() + throws Exception { + Settings settings = readGremlinServerSettings(); + boolean found = false; + + for (Settings.SerializerSettings serializerSettings : + settings.serializers) { + if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE + + "GraphSON")) { + continue; + } + + MessageTextSerializer serializer = + newTextSerializer(serializerSettings.className); + serializer.configure(config(serializerSettings.config), + Collections.emptyMap()); + assertIncludesElementProperties(serializerSettings.className, + serializer); + found = true; + } + + Assert.assertTrue("No GraphSON serializer settings found in " + + GREMLIN_SERVER_CONFIG, found); + } + + @Test + public void testConfiguredGraphSONSerializersIncludeTreeElementProperties() + throws Exception { + Settings settings = readGremlinServerSettings(); + boolean found = false; + + for (Settings.SerializerSettings serializerSettings : + settings.serializers) { + if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE + + "GraphSON")) { + continue; + } + + MessageTextSerializer serializer = + newTextSerializer(serializerSettings.className); + serializer.configure(config(serializerSettings.config), + Collections.emptyMap()); + String tree = serializeResponse(serializer, elementTree()); + Assert.assertTrue(serializerSettings.className, + tree.contains("properties")); + Assert.assertTrue(serializerSettings.className, + tree.contains("marko")); + Assert.assertTrue(serializerSettings.className, + tree.contains("weight")); + found = true; + } + + Assert.assertTrue("No GraphSON serializer settings found in " + + GREMLIN_SERVER_CONFIG, found); + } + + @Test + public void testDefaultGraphSONSerializerIncludesHugeGraphElementProperties() + throws Exception { + Settings settings = readGremlinServerSettings(); + Map config = graphSONV1Config(settings); + MessageTextSerializer serializer = + newTextSerializer(GRAPHSON_UNTYPED_V1); + serializer.configure(config(config), Collections.emptyMap()); + + HugeEdge edge = hugeGraphEdgeWithProperties(); + HugeVertex vertex = (HugeVertex) edge.outVertex(); + String vertexJson = serializeResponse(serializer, vertex); + String edgeJson = serializeResponse(serializer, edge); + String pathJson = serializeResponse( + serializer, elementPath(vertex, edge)); + + Assert.assertContains("properties", vertexJson); + Assert.assertContains("name", vertexJson); + Assert.assertContains("tom", vertexJson); + Assert.assertContains("age", vertexJson); + Assert.assertContains("18", vertexJson); + Assert.assertContains("properties", edgeJson); + Assert.assertContains("weight", edgeJson); + Assert.assertContains("0.75", edgeJson); + Assert.assertContains("properties", pathJson); + Assert.assertContains("tom", pathJson); + Assert.assertContains("0.75", pathJson); + } + + @Test + public void testDriverConfigsReturnEvaluatedGroovyStrings() throws Exception { + Object value = new GroovyShell().evaluate("def value = 42; \"r3-value:${value}\""); + Assert.assertInstanceOf(GString.class, value); + Settings.SerializerSettings serverSettings = readGremlinServerSettings().serializers.get(0); + MessageSerializer server = newMessageSerializer(serverSettings.className); + server.configure(config(serverSettings.config), Collections.emptyMap()); + List response = List.of(value, List.of(value), Map.of("value", value)); + List expected = List.of("r3-value:42", List.of("r3-value:42"), + Map.of("value", "r3-value:42")); + for (String config : DRIVER_CONFIGS) { + RemoteSerializerSettings settings = readRemoteSerializerSettings(config); + MessageSerializer client = newMessageSerializer(settings.className); + client.configure(config(settings.config), Collections.emptyMap()); + Assert.assertEquals(config, expected, + roundTripBinaryResponse(server, client, response).getResult().getData()); + } + Assert.assertEquals(expected, + roundTripBinaryResponse(server, new GraphBinaryMessageSerializerV1(), response) + .getResult().getData()); + } + + @Test + public void testUntypedGraphsonKeepsEvaluatedGroovyStringBean() throws Exception { + Object value = new GroovyShell().evaluate("def value = 42; \"r3-value:${value}\""); + Settings.SerializerSettings settings = readGremlinServerSettings().serializers.stream() + .filter(s -> s.className.endsWith( + "GraphSONUntypedMessageSerializerV1")) + .findFirst().orElseThrow(); + MessageTextSerializer serializer = (MessageTextSerializer) newMessageSerializer(settings.className); + serializer.configure(config(settings.config), Collections.emptyMap()); + Object result = roundTripResponse(serializer, value).getResult().getData(); + Assert.assertInstanceOf(Map.class, result); + Map bean = (Map) result; + Assert.assertEquals(List.of(42), bean.get("values")); + Assert.assertEquals(List.of("r3-value:", ""), bean.get("strings")); + } + + @Test + public void testDriverConfigsReturnOptionalAndFile() throws Exception { + Settings.SerializerSettings serverSettings = readGremlinServerSettings() + .serializers.get(0); + MessageSerializer server = newMessageSerializer(serverSettings.className); + server.configure(config(serverSettings.config), Collections.emptyMap()); + File file = new File("some/directory/test.text"); + List values = Arrays.asList(Optional.of("present"), Optional.empty(), + file, Optional.of(file), + Optional.of(Optional.of("nested"))); + List expected = Arrays.asList("present", null, + Map.of("file", "test.text"), + Map.of("file", "test.text"), "nested"); + List response = Arrays.asList(values, Map.of("values", values)); + for (String config : DRIVER_CONFIGS) { + RemoteSerializerSettings settings = readRemoteSerializerSettings(config); + MessageSerializer client = newMessageSerializer(settings.className); + client.configure(config(settings.config), Collections.emptyMap()); + List results = (List) roundTripBinaryResponse(server, client, response) + .getResult().getData(); + Assert.assertEquals(config, expected, results.get(0)); + Assert.assertEquals(config, Map.of("values", expected), results.get(1)); + } + } + + @Test + public void testDriverConfigsReturnSchemaAndBlob() throws Exception { + Settings.SerializerSettings serverSettings = readGremlinServerSettings() + .serializers.get(0); + MessageSerializer server = newMessageSerializer(serverSettings.className); + server.configure(config(serverSettings.config), Collections.emptyMap()); + PropertyKey schema = new FakeObjects().newPropertyKey(IdGenerator.of(1L), "name"); + byte[] bytes = new byte[]{0, 1, (byte) 255}; + Blob blob = Blob.wrap(bytes); + DetachedVertex vertex = DetachedVertex.build().setId("blob-one").setLabel("document") + .addProperty(DetachedVertexProperty.build().setId(1L) + .setLabel("blob").setValue(blob).create()).create(); + schema.userdata("nested", Map.of("kind", DataType.BLOB, "values", Arrays.asList(DataType.TEXT, blob))); + DetachedEdge edge = DetachedEdge.build().setId("blob-edge").setLabel("link") + .setOutV(vertex).setInV(vertex) + .addProperty(new DetachedProperty<>("blob", blob)).create(); + List expected = Arrays.asList(schema, blob, vertex, + Collections.singletonMap("nested", Arrays.asList(schema, blob)), edge); + for (String file : DRIVER_CONFIGS) { + RemoteSerializerSettings settings = readRemoteSerializerSettings(file); + MessageSerializer client = newMessageSerializer(settings.className); + client.configure(config(settings.config), Collections.emptyMap()); + List results = (List) roundTripBinaryResponse(server, client, expected) + .getResult().getData(); + Map actualSchema = (Map) results.get(0); + Assert.assertEquals(file, "name", actualSchema.get("name")); + Assert.assertEquals(file, "TEXT", actualSchema.get("data_type")); + Assert.assertEquals(file, 1L, actualSchema.get("id")); + Map userdata = (Map) ((Map) actualSchema.get("user_data")).get("nested"); + Assert.assertEquals("BLOB", userdata.get("kind")); + Assert.assertEquals(Arrays.asList("TEXT", ByteBuffer.wrap(bytes)), userdata.get("values")); + Assert.assertEquals(ByteBuffer.wrap(bytes), results.get(1)); + Vertex actualVertex = new Result(results.get(2)).getVertex(); + Assert.assertEquals(file, "blob-one", actualVertex.id()); + Assert.assertEquals(ByteBuffer.wrap(bytes), actualVertex.value("blob")); + List nested = (List) ((Map) results.get(3)).get("nested"); + Assert.assertEquals(file, actualSchema, nested.get(0)); + Assert.assertEquals(ByteBuffer.wrap(bytes), nested.get(1)); + Edge actualEdge = new Result(results.get(4)).getEdge(); + Assert.assertEquals("blob-edge", actualEdge.id()); + Assert.assertEquals(ByteBuffer.wrap(bytes), actualEdge.value("blob")); + } + } + + @Test + public void testDefaultGraphSONTreeRetainsArrayShapeAndScalarKeys() throws Exception { + MessageTextSerializer serializer = newTextSerializer(GRAPHSON_UNTYPED_V1); + serializer.configure(config(graphSONV1Config(readGremlinServerSettings())), Collections.emptyMap()); + Tree scalar = new Tree<>(); + scalar.put("marko", new Tree<>()); + Map response = JsonUtil.fromJson(serializeResponse(serializer, scalar), Map.class); + Object data = ((Map) response.get("result")).get("data"); + Assert.assertEquals(Collections.singletonList(Map.of("key", "marko", "value", Collections.emptyList())), + data); + response = JsonUtil.fromJson(serializeResponse(serializer, elementTree()), Map.class); + List entries = (List) ((Map) response.get("result")).get("data"); + Assert.assertEquals(1, entries.size()); + Map entry = (Map) entries.get(0); + Assert.assertEquals(Set.of("key", "value"), entry.keySet()); + Assert.assertEquals("person", ((Map) entry.get("key")).get("label")); + Assert.assertContains("marko", serializeResponse(serializer, elementTree())); + List children = (List) entry.get("value"); + Assert.assertEquals(1, children.size()); + Assert.assertEquals(Collections.emptyList(), ((Map) children.get(0)).get("value")); + } + + @Test + public void testTypedGraphSONTreesRoundTripScalarElementAndNestedKeys() throws Exception { + Tree scalar = new Tree<>(); + Tree nested = new Tree<>(); + nested.put(123L, new Tree<>()); + scalar.put("marko", nested); + int serializers = 0; + for (Settings.SerializerSettings setting : readGremlinServerSettings().serializers) { + if (!setting.className.startsWith(SERIALIZER_PACKAGE + "GraphSONMessageSerializer")) { + continue; + } + MessageTextSerializer serializer = newTextSerializer(setting.className); + serializer.configure(config(setting.config), Collections.emptyMap()); + Object data = roundTripResponse(serializer, scalar).getResult().getData(); + Assert.assertInstanceOf(Tree.class, data); + Assert.assertEquals(scalar, data); + Tree elements = (Tree) roundTripResponse(serializer, elementTree()).getResult().getData(); + Assert.assertEquals(1, elements.size()); + Vertex vertex = (Vertex) elements.keySet().iterator().next(); + Assert.assertEquals("marko", vertex.value("name")); + Tree children = (Tree) elements.get(vertex); + Assert.assertEquals(1, children.size()); + Edge edge = (Edge) children.keySet().iterator().next(); + Assert.assertEquals(0.5D, (Double) edge.value("weight"), 0.0D); + serializers++; + } + Assert.assertEquals(2, serializers); + } + + @Test + public void testStandardGraphSONV1IoTreeRetainsIdKeyedMap() throws Exception { + org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper mapper = GraphSONMapper.build() + .version(GraphSONVersion.V1_0).typeInfo(TypeInfo.NO_TYPES) + .addRegistry(HugeGraphIoRegistry.instance()).create().createMapper(); + Tree tree = elementTree(); + String json = mapper.writeValueAsString(tree); + Map result = mapper.readValue(json, HashMap.class); + String id = ((Vertex) tree.keySet().iterator().next()).id().toString(); + Assert.assertEquals(Collections.singleton(id), result.keySet()); + Map entry = (Map) result.get(id); + Assert.assertEquals(Set.of("key", "value"), entry.keySet()); + Assert.assertEquals("person", ((Map) entry.get("key")).get("label")); + Assert.assertInstanceOf(Map.class, entry.get("value")); + } + + @Test + public void testLegacyTreeRegistryIsOnlyUsedByUntypedGraphSONV1() throws Exception { + String registry = "org.apache.hugegraph.io.HugeGraphSONV1MessageIoRegistry"; + for (String variant : GREMLIN_SERVER_CONFIG_VARIANTS) { + Settings settings = Settings.read(serverAssemblyPath().resolve(variant).toString()); + for (Settings.SerializerSettings serializer : settings.serializers) { + List registries = (List) serializer.config.get("ioRegistries"); + Assert.assertEquals(variant + ": " + serializer.className, + GRAPHSON_UNTYPED_V1.equals(serializer.className), + registries.contains(registry)); + } + } + } + + @Test + public void testDriverConfigsSupportTypedResultAccess() throws Exception { + Settings.SerializerSettings serverSettings = readGremlinServerSettings() + .serializers.get(0); + Assert.assertEquals(GRAPHBINARY_V1, serverSettings.className); + MessageSerializer server = newMessageSerializer(serverSettings.className); + server.configure(config(serverSettings.config), Collections.emptyMap()); + + HugeEdge expectedEdge = hugeGraphEdgeWithProperties(); + HugeVertex expectedVertex = (HugeVertex) expectedEdge.outVertex(); + UUID uuid = UUID.fromString("3cfcafc8-7906-4ab7-a207-4ded056f58de"); + List expected = Arrays.asList( + expectedVertex, expectedEdge, + elementPath(expectedVertex, expectedEdge), + IdGenerator.of(uuid), IdGenerator.of("marko"), + IdGenerator.of(123L), expectedEdge.id()); + + for (String file : DRIVER_CONFIGS) { + RemoteSerializerSettings settings = readRemoteSerializerSettings(file); + Assert.assertEquals(file, GRAPHBINARY_V1, settings.className); + Assert.assertEquals(file, false, settings.config.get("serializeResultToString")); + Assert.assertEquals(file, GRAPHBINARY_BUILDER, settings.config.get("builder")); + Assert.assertEquals(file, Collections.singletonList(IO_REGISTRY), + settings.config.get("ioRegistries")); + MessageSerializer client = newMessageSerializer(settings.className); + client.configure(config(settings.config), Collections.emptyMap()); + + Object data = roundTripBinaryResponse(server, client, expected) + .getResult().getData(); + Assert.assertInstanceOf(List.class, data); + List results = (List) data; + Assert.assertEquals(file, expected.size(), results.size()); + + Vertex vertex = new Result(results.get(0)).getVertex(); + Assert.assertEquals(file, expectedVertex.id().asLong(), vertex.id()); + Assert.assertEquals(file, "person", vertex.label()); + Assert.assertEquals(file, "tom", vertex.value("name")); + Assert.assertEquals(file, 18, ((Number) vertex.value("age")).intValue()); + + Edge edge = new Result(results.get(1)).getEdge(); + Assert.assertEquals(file, expectedEdge.id().asString(), edge.id()); + Assert.assertEquals(file, "knows", edge.label()); + Assert.assertEquals(file, vertex.id(), edge.outVertex().id()); + Assert.assertEquals(file, 0.75D, (Double) edge.value("weight"), 0.0D); + + org.apache.tinkerpop.gremlin.process.traversal.Path path = + new Result(results.get(2)).getPath(); + Assert.assertEquals(file, Arrays.asList(Set.of("v"), Set.of("e")), path.labels()); + Assert.assertEquals(file, "tom", ((Vertex) path.get(0)).value("name")); + Assert.assertEquals(file, edge.id(), ((Edge) path.get(1)).id()); + Assert.assertEquals(file, uuid, new Result(results.get(3)).get(UUID.class)); + Assert.assertEquals(file, "marko", new Result(results.get(4)).getString()); + Assert.assertEquals(file, 123L, new Result(results.get(5)).getLong()); + Assert.assertEquals(file, expectedEdge.id().asString(), + new Result(results.get(6)).getString()); + } + } + + @Test + public void testRemoteObjectsSerializerKeepsJsonResultShape() throws Exception { + RemoteSerializerSettings settings = readRemoteSerializerSettings(REMOTE_OBJECTS_CONFIG); + Assert.assertEquals(GRAPHSON_UNTYPED_V1, settings.className); + Assert.assertEquals(false, settings.config.get("serializeResultToString")); + Assert.assertEquals(Arrays.asList(IO_REGISTRY, IO_REGISTRY), + settings.config.get("ioRegistries")); + MessageTextSerializer serializer = newTextSerializer(settings.className); + serializer.configure(config(settings.config), Collections.emptyMap()); + + HugeEdge edge = hugeGraphEdgeWithProperties(); + Object vertex = roundTripResponse(serializer, edge.outVertex()) + .getResult().getData(); + Assert.assertInstanceOf(Map.class, vertex); + Assert.assertEquals("person", ((Map) vertex).get("label")); + Assert.assertTrue(((Map) vertex).containsKey("properties")); + UUID uuid = UUID.fromString("3cfcafc8-7906-4ab7-a207-4ded056f58de"); + Object id = roundTripResponse(serializer, IdGenerator.of(uuid)) + .getResult().getData(); + Assert.assertEquals(uuid.toString(), id); + } + + @Test + public void testRemoteObjectsSerializerCanSerializePathShape() + throws Exception { + RemoteSerializerSettings settings = + readRemoteSerializerSettings(REMOTE_OBJECTS_CONFIG); + MessageTextSerializer serializer = + newTextSerializer(settings.className); + + serializer.configure(config(settings.config), Collections.emptyMap()); + + String json = serializeResponse(serializer, testPath()); + + Assert.assertContains("\"labels\"", json); + Assert.assertContains("\"objects\"", json); + Assert.assertContains("marko", json); + Assert.assertContains("lop", json); + Assert.assertContains("\"a\"", json); + Assert.assertContains("\"b\"", json); + Assert.assertContains("\"software\"", json); + } + + private static Settings readGremlinServerSettings() throws Exception { + return Settings.read(configPath(GREMLIN_SERVER_CONFIG).toString()); + } + + private static String readConfig(String fileName) throws IOException { + return Files.readString(configPath(fileName), StandardCharsets.UTF_8); + } + + private static Path configPath(String fileName) { + return findConfDir().resolve(fileName); + } + + private static Path serverAssemblyPath() { + return findConfDir().getParent().getParent(); + } + + private static Path repositoryRoot() { + Path current = Paths.get(System.getProperty("user.dir")) + .toAbsolutePath(); + while (current != null) { + if (Files.isDirectory(current.resolve("hugegraph-server")) && + Files.isDirectory(current.resolve("hugegraph-pd"))) { + return current; + } + current = current.getParent(); + } + + Assert.fail("Can't find HugeGraph repository root from " + + System.getProperty("user.dir")); + return Paths.get(System.getProperty("user.dir")); + } + + private static void collectTinkerPopVersionMismatches( + Path pom, List mismatches) { + try { + String content = Files.readString(pom, StandardCharsets.UTF_8); + content = XML_COMMENT.matcher(content).replaceAll(""); + + Matcher property = TINKERPOP_VERSION_PROPERTY.matcher(content); + while (property.find()) { + collectVersionMismatch(pom, property.group(1), mismatches); + } + + Matcher dependency = TINKERPOP_DEPENDENCY.matcher(content); + while (dependency.find()) { + Matcher version = DEPENDENCY_VERSION.matcher( + dependency.group(1)); + if (version.find()) { + collectVersionMismatch(pom, version.group(1), mismatches); + } + } + + Matcher guice = GUICE_DEPENDENCY.matcher(content); + while (guice.find()) { + Matcher version = DEPENDENCY_VERSION.matcher( + guice.group(1)); + if (version.find()) { + collectGuiceVersionMismatch(pom, version.group(1), + mismatches); + } + } + } catch (IOException e) { + throw new IllegalStateException("Failed to read " + pom, e); + } + } + + private static void collectGuiceVersionMismatch(Path pom, String version, + List mismatches) { + String actual = version.trim(); + if (SUPPORTED_GUICE_VERSION.equals(actual)) { + return; + } + mismatches.add(repositoryRoot().relativize(pom) + "=guice:" + actual); + } + + private static void collectVersionMismatch(Path pom, String version, + List mismatches) { + String actual = version.trim(); + if (SUPPORTED_TINKERPOP_VERSION.equals(actual) || + "${tinkerpop.version}".equals(actual)) { + return; + } + mismatches.add(repositoryRoot().relativize(pom) + "=" + actual); + } + + private static Path findConfDir() { + String configuredDir = System.getProperty("hugegraph.conf.dir"); + Path configuredPath = resolveConfiguredDir(configuredDir); + if (configuredPath != null) { + return configuredPath; + } + + String envDir = System.getenv("HUGEGRAPH_CONF_DIR"); + Path envPath = resolveConfiguredDir(envDir); + if (envPath != null) { + return envPath; + } + + Path userDir = Paths.get(System.getProperty("user.dir")); + List candidates = new ArrayList<>(); + + Path parent = userDir.getParent(); + if (parent != null) { + candidates.add(parent.resolve("hugegraph-dist") + .resolve("src") + .resolve("assembly") + .resolve("static") + .resolve("conf")); + } + candidates.add(userDir.resolve("hugegraph-server") + .resolve("hugegraph-dist") + .resolve("src") + .resolve("assembly") + .resolve("static") + .resolve("conf")); + + for (Path candidate : candidates) { + if (Files.isDirectory(candidate)) { + return candidate; + } + } + + Assert.fail(String.format("Can't find hugegraph-dist static conf from" + + " %s (hugegraph.conf.dir=%s," + + " HUGEGRAPH_CONF_DIR=%s, candidates=%s)", + userDir, configuredDir, envDir, candidates)); + return userDir; + } + + private static Path resolveConfiguredDir(String path) { + if (path == null || path.isEmpty()) { + return null; + } + Path configured = Paths.get(path); + if (Files.isDirectory(configured)) { + return configured; + } + return null; + } + + private static void assertUsesHugeGraphIoRegistry(String fileName, + String content) { + Assert.assertTrue(fileName + " should keep HugeGraphIoRegistry", + content.contains(IO_REGISTRY)); + } + + private static void assertSerializerClassNamesUseUtilPackage( + String fileName, String content) { + Assert.assertFalse(content.contains( + "org.apache.tinkerpop.gremlin.driver.ser.")); + Assert.assertFalse(content.contains( + "org.apache.tinkerpop.gremlin.server.ser.")); + + Matcher matcher = CLASS_NAME.matcher(content); + boolean found = false; + while (matcher.find()) { + found = true; + String className = matcher.group(1); + Assert.assertTrue(fileName + " has outdated serializer " + + className, + className.startsWith(SERIALIZER_PACKAGE)); + } + Assert.assertTrue("No serializer className found in " + fileName, + found); + } + + private static void assertConfiguredSerializerClassesAreLoadable( + String fileName, String content) throws ClassNotFoundException { + Matcher matcher = CLASS_NAME.matcher(content); + boolean found = false; + while (matcher.find()) { + found = true; + Class.forName(matcher.group(1)); + } + Assert.assertTrue("No serializer className found in " + fileName, + found); + } + + private static Map graphSONMimeTypes(Settings settings) + throws Exception { + Map mimeTypes = new HashMap<>(); + + for (Settings.SerializerSettings serializerSettings : + settings.serializers) { + if (!serializerSettings.className.startsWith(SERIALIZER_PACKAGE + + "GraphSON")) { + continue; + } + + MessageSerializer serializer = + newMessageSerializer(serializerSettings.className); + for (String mimeType : serializer.mimeTypesSupported()) { + mimeTypes.putIfAbsent(mimeType, serializerSettings.className); + } + } + + return mimeTypes; + } + + private static MessageTextSerializer newTextSerializer(String className) + throws Exception { + MessageSerializer serializer = newMessageSerializer(className); + + Assert.assertTrue(className + " should be a MessageTextSerializer", + serializer instanceof MessageTextSerializer); + return (MessageTextSerializer) serializer; + } + + private static MessageSerializer newMessageSerializer(String className) + throws Exception { + Object serializer = Class.forName(className) + .getDeclaredConstructor() + .newInstance(); + + Assert.assertTrue(className + " should be a MessageSerializer", + serializer instanceof MessageSerializer); + return (MessageSerializer) serializer; + } + + private static String serializeResponse(MessageTextSerializer serializer, + Object result) + throws Exception { + ResponseMessage response = ResponseMessage.build(UUID.randomUUID()) + .code(ResponseStatusCode.SUCCESS) + .result(result) + .create(); + + return serializer.serializeResponseAsString(response, + ByteBufAllocator.DEFAULT); + } + + private static ResponseMessage roundTripResponse( + MessageTextSerializer serializer, Object result) + throws Exception { + ResponseMessage response = ResponseMessage.build(UUID.randomUUID()) + .code(ResponseStatusCode.SUCCESS) + .result(result) + .create(); + String json = serializer.serializeResponseAsString( + response, ByteBufAllocator.DEFAULT); + return serializer.deserializeResponse(json); + } + + private static ResponseMessage roundTripBinaryResponse( + MessageSerializer serializer, Object result) + throws Exception { + return roundTripBinaryResponse(serializer, serializer, result); + } + + private static ResponseMessage roundTripBinaryResponse( + MessageSerializer serverSerializer, + MessageSerializer clientSerializer, Object result) + throws Exception { + ResponseMessage response = ResponseMessage.build(UUID.randomUUID()) + .code(ResponseStatusCode.SUCCESS) + .result(result) + .create(); + ByteBuf buffer = serverSerializer.serializeResponseAsBinary( + response, ByteBufAllocator.DEFAULT); + try { + return clientSerializer.deserializeResponse(buffer); + } finally { + buffer.release(); + } + } + + @SuppressWarnings("unchecked") + private static RemoteSerializerSettings readRemoteSerializerSettings( + String fileName) throws IOException { + try (InputStream input = Files.newInputStream(configPath(fileName))) { + Map root = new Yaml().load(input); + Map serializer = + (Map) root.get("serializer"); + + Assert.assertNotNull("No serializer in " + fileName, serializer); + String className = (String) serializer.get("className"); + Map config = + (Map) serializer.get("config"); + + Assert.assertNotNull("No serializer className in " + fileName, + className); + Assert.assertNotNull("No serializer config in " + fileName, + config); + return new RemoteSerializerSettings(className, config); + } + } + + private static Map graphSONV1Config(Settings settings) { + for (Settings.SerializerSettings serializer : settings.serializers) { + if (GRAPHSON_UNTYPED_V1.equals(serializer.className)) { + Assert.assertNotNull(serializer.config); + return serializer.config; + } + } + + Assert.fail("No " + GRAPHSON_UNTYPED_V1 + " found in " + + GREMLIN_SERVER_CONFIG); + return Collections.emptyMap(); + } + + private static Map config(Map config) { + if (config == null) { + return Collections.emptyMap(); + } + return new HashMap<>(config); + } + + private static org.apache.tinkerpop.gremlin.process.traversal.Path testPath() { + return MutablePath.make() + .extend(IdGenerator.of("marko"), Set.of("a")) + .extend(IdGenerator.of("lop"), + Set.of("b", "software")); + } + + private static void assertCanSerializeHugeGraphTypes( + MessageTextSerializer serializer, boolean typed) + throws Exception { + Object id = IdGenerator.of("marko"); + Object uuidId = IdGenerator.of( + UUID.fromString("3cfcafc8-7906-4ab7-a207-4ded056f58de")); + Object edgeId = EdgeId.parse("S1>2>3>4>L6"); + String fileJson = serializeResponse(serializer, new File("test.text")); + String idJson = serializeResponse(serializer, id); + String uuidJson = serializeResponse(serializer, uuidId); + String edgeJson = serializeResponse(serializer, edgeId); + + Assert.assertContains("\"file\"", fileJson); + Assert.assertContains("test.text", fileJson); + Assert.assertContains("marko", idJson); + Assert.assertContains("3cfcafc8-7906-4ab7-a207-4ded056f58de", + uuidJson); + Assert.assertContains("S1>2>3>4>L6", edgeJson); + + if (typed) { + assertContainsGraphSONType(fileJson, "hugegraph:File"); + assertContainsGraphSONType(idJson, "hugegraph:StringId"); + assertContainsGraphSONType(uuidJson, "hugegraph:UuidId"); + assertContainsGraphSONType(edgeJson, "hugegraph:EdgeId"); + } + } + + private static void assertCanRoundTripHugeGraphIds( + String serializerName, MessageTextSerializer serializer) + throws Exception { + List ids = Arrays.asList( + IdGenerator.of("marko"), + IdGenerator.of(123L), + IdGenerator.of(UUID.fromString( + "3cfcafc8-7906-4ab7-a207-4ded056f58de")), + EdgeId.parse("S1>2>3>4>L6") + ); + + for (Object expected : ids) { + ResponseMessage response = roundTripResponse(serializer, expected); + Object actual = response.getResult().getData(); + String message = serializerName + " should round-trip " + + expected.getClass().getSimpleName(); + Assert.assertEquals(message, expected.getClass(), + actual.getClass()); + Assert.assertEquals(message, expected, actual); + } + } + + private static void assertCanRoundTripStandardPredicate( + String serializerName, MessageSerializer serializer) + throws Exception { + P expected = P.eq("marko"); + ResponseMessage response = roundTripBinaryResponse(serializer, + expected); + Object actual = response.getResult().getData(); + String message = serializerName + + " should round-trip a standard predicate"; + Assert.assertInstanceOf(P.class, actual); + Assert.assertEquals(message, expected, actual); + } + + private static void assertCanRoundTripElementProperties( + String serializerName, MessageSerializer serializer) + throws Exception { + ResponseMessage vertexResponse = roundTripBinaryResponse( + serializer, vertexWithProperties()); + Object vertexResult = vertexResponse.getResult().getData(); + Assert.assertInstanceOf(Vertex.class, vertexResult); + Vertex vertex = (Vertex) vertexResult; + Assert.assertEquals(serializerName, 1, vertex.id()); + Assert.assertEquals(serializerName, "person", vertex.label()); + Assert.assertEquals(serializerName, "marko", vertex.value("name")); + Assert.assertEquals(serializerName, 29, + ((Number) vertex.value("age")).intValue()); + + ResponseMessage edgeResponse = roundTripBinaryResponse( + serializer, edgeWithProperties()); + Object edgeResult = edgeResponse.getResult().getData(); + Assert.assertInstanceOf(Edge.class, edgeResult); + Edge edge = (Edge) edgeResult; + Assert.assertEquals(serializerName, 7, edge.id()); + Assert.assertEquals(serializerName, "knows", edge.label()); + Assert.assertEquals(serializerName, 0.5D, + (Double) edge.value("weight"), 0.0D); + + org.apache.tinkerpop.gremlin.process.traversal.Path sourcePath = + elementPath(vertexWithProperties(), edgeWithProperties()); + Object pathResult = roundTripBinaryResponse(serializer, sourcePath) + .getResult().getData(); + Assert.assertInstanceOf( + org.apache.tinkerpop.gremlin.process.traversal.Path.class, + pathResult); + org.apache.tinkerpop.gremlin.process.traversal.Path path = + (org.apache.tinkerpop.gremlin.process.traversal.Path) + pathResult; + Vertex pathVertex = path.get(0); + Edge pathEdge = path.get(1); + Assert.assertEquals(serializerName, "marko", + pathVertex.value("name")); + Assert.assertEquals(serializerName, 0.5D, + (Double) pathEdge.value("weight"), 0.0D); + } + + private static void assertCanRoundTripHugeGraphElements( + String serializerName, MessageSerializer serializer) + throws Exception { + HugeEdge expectedEdge = hugeGraphEdgeWithProperties(); + HugeVertex expectedVertex = (HugeVertex) expectedEdge.outVertex(); + MessageSerializer standardClient = + new GraphBinaryMessageSerializerV1(); + + Object vertexResult = roundTripBinaryResponse(serializer, + standardClient, + expectedVertex) + .getResult().getData(); + Assert.assertInstanceOf(Vertex.class, vertexResult); + Vertex vertex = (Vertex) vertexResult; + Assert.assertEquals(serializerName, expectedVertex.id().asLong(), + ((Number) vertex.id()).longValue()); + Assert.assertEquals(serializerName, "person", vertex.label()); + Assert.assertEquals(serializerName, "tom", vertex.value("name")); + Assert.assertEquals(serializerName, 18, + ((Number) vertex.value("age")).intValue()); + + Object edgeResult = roundTripBinaryResponse(serializer, standardClient, + expectedEdge) + .getResult().getData(); + Assert.assertInstanceOf(Edge.class, edgeResult); + Edge edge = (Edge) edgeResult; + Assert.assertEquals(serializerName, expectedEdge.id().asString(), + edge.id().toString()); + Assert.assertEquals(serializerName, "knows", edge.label()); + Assert.assertEquals(serializerName, 0.75D, + (Double) edge.value("weight"), 0.0D); + + Object pathResult = roundTripBinaryResponse( + serializer, standardClient, + elementPath(expectedVertex, expectedEdge)) + .getResult().getData(); + Assert.assertInstanceOf( + org.apache.tinkerpop.gremlin.process.traversal.Path.class, + pathResult); + org.apache.tinkerpop.gremlin.process.traversal.Path path = + (org.apache.tinkerpop.gremlin.process.traversal.Path) + pathResult; + Vertex pathVertex = path.get(0); + Edge pathEdge = path.get(1); + Assert.assertEquals(serializerName, "tom", + pathVertex.value("name")); + Assert.assertEquals(serializerName, 0.75D, + (Double) pathEdge.value("weight"), 0.0D); + } + + private static void assertUsesPrimitiveHugeGraphIds( + String serializerName, MessageSerializer serializer) + throws Exception { + List ids = Arrays.asList( + IdGenerator.of("marko"), + IdGenerator.of(123L), + IdGenerator.of(UUID.fromString( + "3cfcafc8-7906-4ab7-a207-4ded056f58de")), + EdgeId.parse("S1>2>3>4>L6") + ); + MessageSerializer standardClient = + new GraphBinaryMessageSerializerV1(); + + for (Id id : ids) { + Object actual = roundTripBinaryResponse(serializer, standardClient, + id) + .getResult().getData(); + Assert.assertEquals(serializerName, id.asObject(), actual); + } + } + + private static void assertCanRoundTripReferenceElements( + String serializerName, MessageSerializer serializer) + throws Exception { + ReferenceVertex marko = new ReferenceVertex(1, "person"); + ReferenceVertex vadas = new ReferenceVertex(2, "person"); + ReferenceEdge knows = new ReferenceEdge(7, "knows", marko, vadas); + + Object vertexResult = roundTripBinaryResponse(serializer, marko) + .getResult().getData(); + Assert.assertInstanceOf(Vertex.class, vertexResult); + Assert.assertFalse(serializerName, + ((Vertex) vertexResult).properties().hasNext()); + + Object edgeResult = roundTripBinaryResponse(serializer, knows) + .getResult().getData(); + Assert.assertInstanceOf(Edge.class, edgeResult); + Assert.assertFalse(serializerName, + ((Edge) edgeResult).properties().hasNext()); + + org.apache.tinkerpop.gremlin.process.traversal.Path sourcePath = + elementPath(marko, knows); + Object pathResult = roundTripBinaryResponse(serializer, sourcePath) + .getResult().getData(); + Assert.assertInstanceOf( + org.apache.tinkerpop.gremlin.process.traversal.Path.class, + pathResult); + org.apache.tinkerpop.gremlin.process.traversal.Path path = + (org.apache.tinkerpop.gremlin.process.traversal.Path) + pathResult; + Assert.assertFalse(serializerName, + ((Vertex) path.get(0)).properties().hasNext()); + Assert.assertFalse(serializerName, + ((Edge) path.get(1)).properties().hasNext()); + } + + private static void assertIncludesElementProperties( + String serializerName, MessageTextSerializer serializer) + throws Exception { + String vertex = serializeResponse(serializer, vertexWithProperties()); + Assert.assertTrue(serializerName, vertex.contains("properties")); + Assert.assertTrue(serializerName, vertex.contains("name")); + Assert.assertTrue(serializerName, vertex.contains("marko")); + Assert.assertTrue(serializerName, vertex.contains("age")); + Assert.assertTrue(serializerName, vertex.contains("29")); + + String edge = serializeResponse(serializer, edgeWithProperties()); + Assert.assertTrue(serializerName, edge.contains("properties")); + Assert.assertTrue(serializerName, edge.contains("weight")); + Assert.assertTrue(serializerName, edge.contains("0.5")); + + String path = serializeResponse( + serializer, + elementPath(vertexWithProperties(), edgeWithProperties())); + Assert.assertTrue(serializerName, path.contains("properties")); + Assert.assertTrue(serializerName, path.contains("marko")); + Assert.assertTrue(serializerName, path.contains("weight")); + } + + private static DetachedVertex vertexWithProperties() { + return DetachedVertex.build() + .setId(1) + .setLabel("person") + .addProperty(new DetachedVertexProperty<>( + 11, "name", "marko", + Collections.emptyMap())) + .addProperty(new DetachedVertexProperty<>( + 12, "age", 29, + Collections.emptyMap())) + .create(); + } + + private static org.apache.tinkerpop.gremlin.process.traversal.Path + elementPath(Vertex vertex, Edge edge) { + return MutablePath.make() + .extend(vertex, Set.of("v")) + .extend(edge, Set.of("e")); + } + + private static Tree elementTree() { + Tree tree = new Tree<>(); + Tree children = new Tree<>(); + children.put(edgeWithProperties(), new Tree<>()); + tree.put(vertexWithProperties(), children); + return tree; + } + + private static HugeEdge hugeGraphEdgeWithProperties() { + FakeObjects objects = new FakeObjects(); + Mockito.doReturn(new HugeFeatures(objects.graph(), false)) + .when(objects.graph()).features(); + return objects.newEdge(123, 456); + } + + private static DetachedEdge edgeWithProperties() { + DetachedVertex marko = DetachedVertex.build() + .setId(1) + .setLabel("person") + .create(); + DetachedVertex vadas = DetachedVertex.build() + .setId(2) + .setLabel("person") + .create(); + return DetachedEdge.build() + .setId(7) + .setLabel("knows") + .setOutV(marko) + .setInV(vadas) + .addProperty(new DetachedProperty<>("weight", 0.5D)) + .create(); + } + + private static void assertContainsGraphSONType(String json, + String graphSONType) { + Assert.assertContains("\"@type\"", json); + Assert.assertContains(graphSONType, json); + } + + private static final class RemoteSerializerSettings { + + private final String className; + private final Map config; + + private RemoteSerializerSettings(String className, + Map config) { + this.className = className; + this.config = config; + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/K8sResourceQuotaYamlTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/K8sResourceQuotaYamlTest.java new file mode 100644 index 0000000000..387b8a9769 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/config/K8sResourceQuotaYamlTest.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.hugegraph.unit.config; + +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.hugegraph.config.CoreOptions; +import org.apache.hugegraph.k8s.K8sDriver; +import org.apache.hugegraph.k8s.K8sManager; +import org.apache.hugegraph.testutil.Assert; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.utils.Serialization; + +public class K8sResourceQuotaYamlTest { + + private Path template; + private byte[] originalTemplate; + private boolean existingDirectory; + private Field driverField; + private Object originalDriver; + private K8sDriver driver; + + @Before + public void setUp() throws Exception { + this.template = Paths.get(CoreOptions.K8S_QUOTA_TEMPLATE.defaultValue()); + this.existingDirectory = Files.exists(this.template.getParent()); + this.originalTemplate = Files.exists(this.template) ? Files.readAllBytes(this.template) : null; + Files.createDirectories(this.template.getParent()); + this.driverField = K8sManager.class.getDeclaredField("k8sDriver"); + this.driverField.setAccessible(true); + this.originalDriver = this.driverField.get(K8sManager.instance()); + this.driver = Mockito.mock(K8sDriver.class); + this.driverField.set(K8sManager.instance(), this.driver); + UnexpectedType.constructions = 0; + } + + @After + public void tearDown() throws Exception { + this.driverField.set(K8sManager.instance(), this.originalDriver); + if (this.originalTemplate == null) { + Files.deleteIfExists(this.template); + } else { + Files.write(this.template, this.originalTemplate); + } + if (!this.existingDirectory) { + Files.deleteIfExists(this.template.getParent()); + } + } + + @Test + public void testQuotaMapRoundTrip() throws Exception { + Files.write(this.template, ("apiVersion: v1\nkind: ResourceQuota\n" + + "metadata:\n name: old-quota\n labels: {team: graphs}\n" + + "spec:\n hard: {}\n").getBytes(StandardCharsets.UTF_8)); + K8sManager.instance().loadResourceQuota("Team_Graph", 4, 8); + ArgumentCaptor yaml = ArgumentCaptor.forClass(String.class); + Mockito.verify(this.driver).createOrReplaceResourceQuota(Mockito.eq("team-graph"), + yaml.capture()); + Map quota = new Yaml(new SafeConstructor(new LoaderOptions())) + .load(yaml.getValue()); + Map metadata = (Map) quota.get("metadata"); + Map hard = (Map) ((Map) quota.get("spec")).get("hard"); + Assert.assertEquals("team-graph-resource-quota", metadata.get("name")); + Assert.assertEquals("graphs", ((Map) metadata.get("labels")).get("team")); + Assert.assertEquals("4", hard.get("requests.cpu")); + Assert.assertEquals("4", hard.get("limits.cpu")); + Assert.assertEquals("8Gi", hard.get("requests.memory")); + Assert.assertEquals("8Gi", hard.get("limits.memory")); + } + + @Test + public void testConfigBuilderReadsKubeconfig() throws Exception { + Path kubeconfig = Files.createTempFile("hugegraph-kubeconfig-", ".yaml"); + Map properties = new LinkedHashMap<>(); + properties.put(Config.KUBERNETES_KUBECONFIG_FILE, kubeconfig.toString()); + properties.put(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "true"); + properties.put(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + properties.put(Config.KUBERNETES_DISABLE_AUTO_CONFIG_SYSTEM_PROPERTY, "false"); + Map originals = new LinkedHashMap<>(); + try { + Files.write(kubeconfig, ("apiVersion: v1\nkind: Config\n" + + "clusters:\n- name: test\n" + + " cluster: {server: 'https://127.0.0.1:6443'}\n" + + "users:\n- name: test-user\n" + + " user: {token: local-test-token}\n" + + "contexts:\n- name: test-context\n" + + " context: {cluster: test, user: test-user, " + + "namespace: hugegraph-test}\n" + + "current-context: test-context\n" + + "extensions:\n- name: fixture-extension\n" + + " extension: {ratio: 0.5, count: 1}\n").getBytes(StandardCharsets.UTF_8)); + properties.forEach((key, value) -> { + originals.put(key, System.getProperty(key)); + System.setProperty(key, value); + }); + // This is the same auto-configuration entry point as K8sDriver, + // without constructing a client or contacting a Kubernetes server. + Config config = new ConfigBuilder().build(); + Assert.assertEquals("https://127.0.0.1:6443/", config.getMasterUrl()); + Assert.assertEquals("hugegraph-test", config.getNamespace()); + Assert.assertEquals("local-test-token", config.getOauthToken()); + Config roundTrip = Serialization.unmarshal(Serialization.asYaml(config), Config.class); + Assert.assertEquals(config.getMasterUrl(), roundTrip.getMasterUrl()); + Assert.assertEquals(config.getNamespace(), roundTrip.getNamespace()); + Assert.assertEquals(config.getOauthToken(), roundTrip.getOauthToken()); + } finally { + originals.forEach((key, value) -> { + if (value == null) { + System.clearProperty(key); + } else { + System.setProperty(key, value); + } + }); + Files.deleteIfExists(kubeconfig); + } + } + + @Test + public void testRejectJavaTypeBeforeConstruction() throws Exception { + String tagged = "!!" + UnexpectedType.class.getName() + " {}\n"; + Files.write(this.template, tagged.getBytes(StandardCharsets.UTF_8)); + K8sManager.instance().loadResourceQuota("graphs", 4, 8); + Assert.assertEquals(0, UnexpectedType.constructions); + Mockito.verifyNoInteractions(this.driver); + } + + public static class UnexpectedType { + + private static int constructions; + + public UnexpectedType() { + constructions++; + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/AnalyzerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/AnalyzerTest.java index f2b2f02b10..47027e4341 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/AnalyzerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/AnalyzerTest.java @@ -23,6 +23,7 @@ import org.apache.hugegraph.analyzer.Analyzer; import org.apache.hugegraph.analyzer.AnalyzerFactory; +import org.apache.hugegraph.config.ConfigException; import org.apache.hugegraph.testutil.Assert; import org.junit.After; import org.junit.Before; @@ -44,6 +45,15 @@ public void teardown() { // pass } + @Test + public void testWordAnalyzerRequiresOptionalDependency() { + ConfigException exception = Assert.assertThrows(ConfigException.class, () -> { + AnalyzerFactory.analyzer("word", "PureEnglish"); + }); + Assert.assertTrue(exception.getMessage().contains("org.apdplat:word:1.3")); + Assert.assertTrue(exception.getCause() instanceof NoClassDefFoundError); + } + @Test public void testAnsjAnalyzer() { // BaseAnalysis mode diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendMutationTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendMutationTest.java index e07727fd23..728fc7fb1e 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendMutationTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendMutationTest.java @@ -19,9 +19,9 @@ import java.util.List; -import org.apache.hugegraph.HugeException; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.SplicingIdGenerator; import org.apache.hugegraph.backend.serializer.TextBackendEntry; import org.apache.hugegraph.backend.store.BackendAction; import org.apache.hugegraph.backend.store.BackendEntry; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendProviderFactoryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendProviderFactoryTest.java new file mode 100644 index 0000000000..58bd977b6e --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendProviderFactoryTest.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.hugegraph.unit.core; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.backend.store.BackendProviderFactory; +import org.apache.hugegraph.backend.store.memory.InMemoryDBStoreProvider; +import org.apache.hugegraph.backend.store.raft.RaftBackendStoreProvider; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.unit.BaseUnitTest; +import org.junit.Test; + +public class BackendProviderFactoryTest extends BaseUnitTest { + + @Test + public void testRegister() { + String name = "fake-provider"; + String provider = InMemoryDBStoreProvider.class.getName(); + + BackendProviderFactory.register(name, provider); + BackendProviderFactory.register(name, provider); + + Assert.assertThrows(BackendException.class, () -> { + BackendProviderFactory.register( + name, RaftBackendStoreProvider.class.getName()); + }, e -> { + Assert.assertContains("Exists BackendStoreProvider:", + e.getMessage()); + }); + } + + @Test + public void testRegisterConcurrently() throws Exception { + String name = "concurrent-provider-" + System.nanoTime(); + CountDownLatch ready = new CountDownLatch(2); + CountDownLatch start = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + List>> registrations = List.of( + executor.submit(() -> { + ready.countDown(); + start.await(); + BackendProviderFactory.register( + name, + InMemoryDBStoreProvider.class.getName()); + return InMemoryDBStoreProvider.class; + }), + executor.submit(() -> { + ready.countDown(); + start.await(); + BackendProviderFactory.register( + name, + RaftBackendStoreProvider.class.getName()); + return RaftBackendStoreProvider.class; + })); + + Assert.assertTrue(ready.await(5L, TimeUnit.SECONDS)); + start.countDown(); + + int successes = 0; + int conflicts = 0; + Class registered = null; + for (Future> registration : registrations) { + try { + registered = registration.get(5L, TimeUnit.SECONDS); + successes++; + } catch (ExecutionException e) { + Assert.assertInstanceOf(BackendException.class, + e.getCause()); + Assert.assertContains("Exists BackendStoreProvider:", + e.getCause().getMessage()); + conflicts++; + } + } + + Assert.assertEquals(1, successes); + Assert.assertEquals(1, conflicts); + Assert.assertNotNull(registered); + BackendProviderFactory.register(name, registered.getName()); + } finally { + start.countDown(); + executor.shutdownNow(); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendSessionPoolTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendSessionPoolTest.java new file mode 100644 index 0000000000..80970b8e5b --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/BackendSessionPoolTest.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.hugegraph.unit.core; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; + +import org.apache.hugegraph.backend.store.BackendSession; +import org.apache.hugegraph.backend.store.BackendSessionPool; +import org.apache.hugegraph.backend.store.BackendStoreProvider; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBStore.RocksDBGraphStore; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.unit.FakeObjects; +import org.junit.Test; +import org.mockito.Mockito; + +public class BackendSessionPoolTest { + + private static final long TIMEOUT_SECONDS = 10L; + private static final long CLOSE_HOLD_SECONDS = 30L; + + @Test + public void testConcurrentAcquireDoesNotLeaveSessionOnClosedBackend() + throws Exception { + TestSessionPool pool = new TestSessionPool(); + pool.open(); + + AtomicReference closeFailure = new AtomicReference<>(); + CountDownLatch acquireAttempted = new CountDownLatch(1); + FutureTask acquisition = + new FutureTask<>(() -> { + acquireAttempted.countDown(); + return pool.getOrNewSession(); + }); + + Thread closer = new Thread(() -> { + try { + pool.getOrNewSession(); + pool.close(); + } catch (Throwable e) { + closeFailure.set(e); + } + }, "backend-session-closer"); + Thread acquirer = new Thread(acquisition, "backend-session-acquirer"); + + closer.start(); + try { + Assert.assertTrue("last-session close did not enter doClose", + pool.closeEntered.await(TIMEOUT_SECONDS, + TimeUnit.SECONDS)); + + acquirer.start(); + Assert.assertTrue("acquirer did not start", + acquireAttempted.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + boolean acquiredWhileClosePaused = + awaitBorrowerInCloseWindow(pool, closer, acquirer); + BackendSession activeSession = acquiredWhileClosePaused ? + acquisition.get(TIMEOUT_SECONDS, + TimeUnit.SECONDS) : null; + pool.allowClose.countDown(); + + closer.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)); + acquirer.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)); + Assert.assertFalse("closer did not finish", closer.isAlive()); + Assert.assertFalse("acquirer did not finish", acquirer.isAlive()); + Assert.assertNull(closeFailure.get()); + if (acquiredWhileClosePaused) { + Assert.assertNotNull(activeSession); + Assert.assertTrue("new session should still be open", + activeSession.opened()); + Assert.assertFalse("pool should still count the new active session", + pool.closed()); + + Assert.assertTrue("doClose closed the backend while a newly " + + "acquired session remained active", + pool.opened()); + } else { + try { + acquisition.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + Assert.fail("late acquire should be rejected after backend close"); + } catch (ExecutionException e) { + Assert.assertTrue("late acquire must fail because backend is closed", + e.getCause() instanceof IllegalStateException); + Assert.assertEquals("Backend session pool is closed", e.getCause().getMessage()); + Assert.assertFalse("backend should be closed before rejecting late acquire", + pool.opened()); + Assert.assertTrue("pool should count no active session after rejection", + pool.closed()); + } + } + } finally { + pool.allowClose.countDown(); + closer.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)); + if (acquirer.getState() != Thread.State.NEW) { + acquirer.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)); + } + } + } + + @Test + public void testAcquireAfterLastSessionCloseIsRejectedByPool() { + TestSessionPool pool = new TestSessionPool(); + pool.open(); + BackendSession original = pool.getOrNewSession(); + pool.allowClose.countDown(); + Assert.assertTrue(pool.close()); + Assert.assertFalse(pool.opened()); + + Throwable failure = Assert.assertThrows(IllegalStateException.class, pool::getOrNewSession); + Assert.assertEquals("Backend session pool is closed", failure.getMessage()); + Assert.assertTrue(pool.closed()); + Assert.assertSame("rejected acquire must never call newSession", original, pool.session()); + } + + @Test + public void testStoreOpenKeepsExistingPoolAliveUntilBorrowCompletes() throws Exception { + RocksDBGraphStore store = new RocksDBGraphStore( + Mockito.mock(BackendStoreProvider.class), "db", "test"); + RocksDBSessions pool = Mockito.mock(RocksDBSessions.class); + AtomicInteger leases = new AtomicInteger(1); + AtomicBoolean nativeOpened = new AtomicBoolean(true); + CountDownLatch borrowing = new CountDownLatch(1); + CountDownLatch finishBorrow = new CountDownLatch(1); + CountDownLatch closing = new CountDownLatch(1); + CountDownLatch closeFinished = new CountDownLatch(1); + Mockito.doAnswer(invocation -> leases.get() == 0).when(pool).closed(); + Mockito.doAnswer(invocation -> { + borrowing.countDown(); + Assert.assertTrue(finishBorrow.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + Assert.assertTrue("last close released the backend before the new store borrow", + nativeOpened.get()); + leases.incrementAndGet(); + return new TestSession(); + }).when(pool).useSession(); + Mockito.doAnswer(invocation -> { + boolean last = leases.decrementAndGet() == 0; + if (last) { + nativeOpened.set(false); + } + return last; + }).when(pool).close(); + Whitebox.setInternalState(store, "sessions", pool); + ConcurrentHashMap databases = new ConcurrentHashMap<>(); + databases.put("db", pool); + Whitebox.setInternalState(store, "dbs", databases); + + AtomicReference failure = new AtomicReference<>(); + Thread opener = new Thread(() -> { + try { + store.open(FakeObjects.newConfig()); + } catch (Throwable error) { + failure.compareAndSet(null, error); + } + }, "store-session-opener"); + Thread closer = new Thread(() -> { + closing.countDown(); + try { + store.close(); + } catch (Throwable error) { + failure.compareAndSet(null, error); + } finally { + closeFinished.countDown(); + } + }, "store-session-closer"); + opener.start(); + try { + Assert.assertTrue(borrowing.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + closer.start(); + Assert.assertTrue(closing.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(TIMEOUT_SECONDS); + // Wait for close to either reach the held store monitor or finish. + while (closeFinished.getCount() != 0L) { + ThreadInfo info = ManagementFactory.getThreadMXBean().getThreadInfo(closer.getId()); + if (info != null && info.getThreadState() == Thread.State.BLOCKED && + info.getLockOwnerId() == opener.getId()) { + break; + } + Assert.assertTrue("close did not reach the store open window", System.nanoTime() < deadline); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1L)); + } + } finally { + finishBorrow.countDown(); + opener.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)); + if (closer.getState() != Thread.State.NEW) { + closer.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)); + } + } + Assert.assertFalse(opener.isAlive()); + Assert.assertFalse(closer.isAlive()); + Assert.assertNull(failure.get()); + Assert.assertTrue(nativeOpened.get()); + Assert.assertEquals(1, leases.get()); + Mockito.verify(pool).useSession(); + Mockito.verify(pool).close(); + } + + private static boolean awaitBorrowerInCloseWindow(TestSessionPool pool, + Thread closer, + Thread acquirer) { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(TIMEOUT_SECONDS); + while (System.nanoTime() < deadline) { + if (pool.acquirerEnteredNewSession.getCount() == 0L) { + return true; + } + ThreadInfo info = ManagementFactory.getThreadMXBean() + .getThreadInfo(acquirer.getId()); + if (info != null && info.getThreadState() == Thread.State.BLOCKED && + info.getLockOwnerId() == closer.getId() && + info.getLockInfo() != null && + info.getLockInfo().getClassName().equals(TestSessionPool.class.getName())) { + return false; + } + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1L)); + } + throw new AssertionError("acquirer neither entered newSession nor blocked on close"); + } + + private static final class TestSessionPool extends BackendSessionPool { + + private final AtomicBoolean opened; + private final AtomicReference session; + private final CountDownLatch closeEntered; + private final CountDownLatch allowClose; + private final CountDownLatch acquirerEnteredNewSession; + + private TestSessionPool() { + super(FakeObjects.newConfig(), "test"); + this.opened = new AtomicBoolean(); + this.session = new AtomicReference<>(); + this.closeEntered = new CountDownLatch(1); + this.allowClose = new CountDownLatch(1); + this.acquirerEnteredNewSession = new CountDownLatch(1); + } + + @Override + public void open() { + this.opened.set(true); + } + + @Override + protected boolean opened() { + return this.opened.get(); + } + + @Override + public BackendSession session() { + return this.session.get(); + } + + @Override + protected BackendSession newSession() { + if (Thread.currentThread().getName().equals("backend-session-acquirer")) { + this.acquirerEnteredNewSession.countDown(); + } + BackendSession newSession = new TestSession(); + this.session.set(newSession); + return newSession; + } + + @Override + protected void doClose() { + this.closeEntered.countDown(); + try { + if (!this.allowClose.await(CLOSE_HOLD_SECONDS, TimeUnit.SECONDS)) { + throw new AssertionError("timed out waiting to finish doClose"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError("interrupted while waiting to finish doClose", e); + } + this.opened.set(false); + } + } + + private static final class TestSession extends BackendSession.AbstractBackendSession { + + @Override + public void open() { + this.opened = true; + } + + @Override + public void close() { + this.opened = false; + } + + @Override + public Object commit() { + return null; + } + + @Override + public void rollback() { + // No transaction state in this test session. + } + + @Override + public boolean hasChanges() { + return false; + } + } + +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ConditionQueryFlattenTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ConditionQueryFlattenTest.java index 64833a3f70..0d9c2187a2 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ConditionQueryFlattenTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ConditionQueryFlattenTest.java @@ -21,10 +21,10 @@ import java.util.Collection; import java.util.List; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.backend.query.ConditionQueryFlatten; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.HugeType; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ConditionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ConditionTest.java index ba4b09dcab..0305fa1bf5 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ConditionTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ConditionTest.java @@ -19,14 +19,16 @@ import java.util.Date; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.Condition.Relation; -import org.apache.hugegraph.backend.query.Condition.RelationType; -import org.apache.hugegraph.backend.query.Condition.SyspropRelation; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.Condition.Relation; +import org.apache.hugegraph.query.Condition.RelationType; +import org.apache.hugegraph.query.Condition.SyspropRelation; import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.traversal.optimize.ConditionP; import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.unit.BaseUnitTest; +import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate; import org.junit.Test; import com.google.common.collect.ImmutableList; @@ -149,6 +151,69 @@ public void testConditionEq() { Assert.assertFalse(c4.test(new Date(0L))); } + @Test + @SuppressWarnings("unchecked") + public void testRelationTypeImplementsTinkerPopBiPredicate() { + PBiPredicate contains = + (PBiPredicate) (Object) RelationType.CONTAINS; + Assert.assertEquals("contains", contains.getPredicateName()); + Assert.assertTrue(contains.test(ImmutableList.of("marko", "josh"), + "marko")); + Assert.assertFalse(contains.test(ImmutableList.of("marko", "josh"), + "vadas")); + + PBiPredicate containsKey = + (PBiPredicate) (Object) + RelationType.CONTAINS_KEY; + Assert.assertEquals("containsk", containsKey.getPredicateName()); + Assert.assertTrue(containsKey.test(ImmutableMap.of("name", "marko"), + "name")); + + PBiPredicate textContains = + (PBiPredicate) (Object) + RelationType.TEXT_CONTAINS; + Assert.assertEquals("textcontains", + textContains.getPredicateName()); + Assert.assertTrue(textContains.test("marko", "ark")); + Assert.assertFalse(textContains.test("marko", "vadas")); + } + + @Test + public void testConditionPUsesRelationTypeBiPredicate() { + ConditionP contains = ConditionP.contains("marko"); + Assert.assertEquals("contains", + contains.getBiPredicate().getPredicateName()); + Assert.assertTrue(contains.test(ImmutableList.of("marko", "josh"))); + Assert.assertFalse(ConditionP.contains("vadas") + .test(ImmutableList.of("marko", "josh"))); + + ConditionP containsKey = ConditionP.containsK("name"); + Assert.assertEquals("containsk", + containsKey.getBiPredicate().getPredicateName()); + Assert.assertTrue(containsKey.test(ImmutableMap.of("name", "marko"))); + Assert.assertFalse(ConditionP.containsK("age") + .test(ImmutableMap.of("name", "marko"))); + + ConditionP containsValue = ConditionP.containsV("marko"); + Assert.assertEquals("containsv", + containsValue.getBiPredicate().getPredicateName()); + Assert.assertTrue(containsValue.test( + ImmutableMap.of("name", "marko"))); + Assert.assertFalse(ConditionP.containsV("vadas") + .test(ImmutableMap.of("name", "marko"))); + + ConditionP textContains = ConditionP.textContains("ark"); + Assert.assertEquals("textcontains", + textContains.getBiPredicate().getPredicateName()); + Assert.assertTrue(textContains.test("marko")); + Assert.assertFalse(ConditionP.textContains("vadas").test("marko")); + + ConditionP eq = ConditionP.eq(new String[]{"a", "b"}); + Assert.assertEquals("==", eq.getBiPredicate().getPredicateName()); + Assert.assertTrue(eq.test(new String[]{"a", "b"})); + Assert.assertFalse(eq.test(new String[]{"a", "c"})); + } + @Test public void testConditionGt() { Condition c1 = Condition.gt(HugeKeys.ID, 123); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/DataTypeTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/DataTypeTest.java index 3ccff1a131..5b4d9aaad7 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/DataTypeTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/DataTypeTest.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.unit.core; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.Date; import java.util.UUID; @@ -73,6 +75,17 @@ public void testValueToDate() { Assert.assertNull(DataType.DATE.valueToDate(true)); } + @Test + public void testValueToDateFromOffsetDateTime() { + OffsetDateTime value = OffsetDateTime.parse("2023-08-02T08:00:00.123456789+08:00"); + Date expected = Date.from(value.toInstant()); + Assert.assertEquals(expected, DataType.DATE.valueToDate(value)); + Assert.assertEquals(expected, DataType.DATE.valueToDate( + value.withOffsetSameInstant(ZoneOffset.UTC))); + Assert.assertEquals(value.toInstant().toEpochMilli(), expected.getTime()); + Assert.assertNull(DataType.TEXT.valueToDate(value)); + } + @Test public void testValueToUUID() { UUID uuid = UUID.randomUUID(); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/DirectionsTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/DirectionsTest.java index 0f6b69e8fc..75678b82cc 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/DirectionsTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/DirectionsTest.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.unit.core; +import org.apache.hugegraph.util.TinkerPopUtil; + import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Directions; @@ -59,16 +61,16 @@ public void testOpposite() { @Test public void testToDirection() { - Assert.assertEquals(Direction.OUT, Directions.OUT.direction()); - Assert.assertEquals(Direction.IN, Directions.IN.direction()); - Assert.assertEquals(Direction.BOTH, Directions.BOTH.direction()); + Assert.assertEquals(Direction.OUT, TinkerPopUtil.direction(Directions.OUT)); + Assert.assertEquals(Direction.IN, TinkerPopUtil.direction(Directions.IN)); + Assert.assertEquals(Direction.BOTH, TinkerPopUtil.direction(Directions.BOTH)); } @Test public void testFromDirection() { - Assert.assertEquals(Directions.OUT, Directions.convert(Direction.OUT)); - Assert.assertEquals(Directions.IN, Directions.convert(Direction.IN)); + Assert.assertEquals(Directions.OUT, TinkerPopUtil.direction(Direction.OUT)); + Assert.assertEquals(Directions.IN, TinkerPopUtil.direction(Direction.IN)); Assert.assertEquals(Directions.BOTH, - Directions.convert(Direction.BOTH)); + TinkerPopUtil.direction(Direction.BOTH)); } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerAdminInitTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerAdminInitTest.java index 60e59666f4..490ffb9920 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerAdminInitTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerAdminInitTest.java @@ -22,7 +22,7 @@ import java.util.Map; import org.apache.commons.configuration2.PropertiesConfiguration; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.auth.HugeAuthenticator; import org.apache.hugegraph.auth.HugeUser; import org.apache.hugegraph.auth.StandardAuthenticator; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerClearGraphSpaceTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerClearGraphSpaceTest.java new file mode 100644 index 0000000000..9ff0b14060 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerClearGraphSpaceTest.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.hugegraph.unit.core; + +import java.util.Collections; +import java.util.Map; + +import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.event.EventHub; +import org.apache.hugegraph.meta.MetaManager; +import org.apache.hugegraph.space.GraphSpace; +import org.apache.hugegraph.space.Service; +import org.apache.hugegraph.task.TaskScheduler; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.junit.Test; +import org.mockito.Mockito; + +public class GraphManagerClearGraphSpaceTest { + + /** + * Graph and service keys are "{graphspace}-{name}", and a graph space + * name may be a prefix of another one, so clearing "gs" must leave the + * graphs and services of "gs2" and "gs_x" alone + */ + @Test + public void testClearKeepsGraphsOfSpacesNamedLikeIt() { + HugeGraph cleared = mockGraph("gs-g"); + HugeGraph kept = mockGraph("gs2-g"); + HugeGraph keptUnderscore = mockGraph("gs_x-g"); + MetaManager meta = Mockito.mock(MetaManager.class); + + // A mock hub: the executor of EventHub is static, and a test that ran + // earlier in the suite may have shut it down (HugeFactory.shutdown) + GraphManager manager = new GraphManager( + new HugeConfig(new PropertiesConfiguration()), + Mockito.mock(EventHub.class)); + try { + Whitebox.setInternalState(manager, "PDExist", true); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "localGraphs", + Collections.emptySet()); + Map spaces = + Whitebox.getInternalState(manager, "graphSpaces"); + for (String space : new String[]{"gs", "gs2", "gs_x"}) { + spaces.put(space, new GraphSpace(space)); + } + Map graphs = + Whitebox.getInternalState(manager, "graphs"); + graphs.put("gs-g", cleared); + graphs.put("gs2-g", kept); + graphs.put("gs_x-g", keptUnderscore); + Map services = + Whitebox.getInternalState(manager, "services"); + services.put("gs-s", Mockito.mock(Service.class)); + services.put("gs2-s", Mockito.mock(Service.class)); + services.put("gs_x-s", Mockito.mock(Service.class)); + + manager.clearGraphSpace("gs"); + + Mockito.verify(cleared).clearBackend(); + Mockito.verify(kept, Mockito.never()).clearBackend(); + Mockito.verify(keptUnderscore, Mockito.never()).clearBackend(); + Assert.assertFalse(graphs.containsKey("gs-g")); + Assert.assertTrue(graphs.containsKey("gs2-g")); + Assert.assertTrue(graphs.containsKey("gs_x-g")); + + Mockito.verify(meta).service("gs", "s"); + Mockito.verify(meta, Mockito.never()).service("gs2", "s"); + Mockito.verify(meta, Mockito.never()).service("gs_x", "s"); + } finally { + manager.close(); + } + } + + private static HugeGraph mockGraph(String spaceGraphName) { + HugeGraph graph = Mockito.mock(HugeGraph.class); + // No tasks to drain: the drop closes task admission and goes on + TaskScheduler scheduler = Mockito.mock(TaskScheduler.class); + Mockito.when(scheduler.close()).thenReturn(true); + Mockito.when(graph.taskScheduler()).thenReturn(scheduler); + Mockito.when(graph.spaceGraphName()).thenReturn(spaceGraphName); + return graph; + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerConfigTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerConfigTest.java index b8601c22d5..51628c0d98 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerConfigTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerConfigTest.java @@ -17,21 +17,3388 @@ package org.apache.hugegraph.unit.core; +import java.io.File; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import javax.script.ScriptException; +import javax.script.SimpleBindings; + +import org.apache.commons.configuration2.Configuration; import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.hugegraph.HugeFactory; +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.HugeGraphParams; +import org.apache.hugegraph.StandardHugeGraph; +import org.apache.hugegraph.auth.AuthContext; +import org.apache.hugegraph.auth.ContextGremlinServer; +import org.apache.hugegraph.auth.HugeAuthenticator; +import org.apache.hugegraph.auth.HugeGraphAuthProxy; +import org.apache.hugegraph.auth.HugePermission; +import org.apache.hugegraph.auth.HugeResource; +import org.apache.hugegraph.auth.RolePermission; +import org.apache.hugegraph.backend.store.BackendFeatures; +import org.apache.hugegraph.backend.store.BackendProviderFactory; +import org.apache.hugegraph.backend.store.BackendStoreProvider; +import org.apache.hugegraph.backend.store.raft.RaftBackendStoreProvider; +import org.apache.hugegraph.backend.store.raft.RaftContext; +import org.apache.hugegraph.backend.store.raft.RaftNode; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBOptions; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBStoreProvider; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.OptionSpace; +import org.apache.hugegraph.config.RpcOptions; import org.apache.hugegraph.config.ServerOptions; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.event.EventHub; +import org.apache.hugegraph.event.EventListener; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.rpc.RpcClientProvider; +import org.apache.hugegraph.rpc.RpcServer; +import org.apache.hugegraph.rpc.RpcServiceConfig4Client; +import org.apache.hugegraph.rpc.RpcServiceConfig4Server; +import org.apache.hugegraph.space.GraphSpace; +import org.apache.hugegraph.task.TaskManager; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.Events; +import org.apache.hugegraph.meta.MetaManager; +import org.apache.tinkerpop.gremlin.server.Settings; +import org.apache.tinkerpop.gremlin.server.util.DefaultGraphManager; +import org.mockito.Mockito; +import org.apache.tinkerpop.gremlin.groovy.jsr223.GremlinGroovyScriptEngine; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import com.alipay.sofa.jraft.Node; +import com.alipay.sofa.jraft.NodeManager; +import com.alipay.sofa.jraft.core.State; +import com.alipay.sofa.jraft.entity.PeerId; +import com.alipay.sofa.jraft.option.CliOptions; +import com.alipay.sofa.jraft.rpc.CliRequests; +import com.alipay.sofa.jraft.rpc.impl.BoltRpcServer; +import com.alipay.sofa.jraft.rpc.impl.cli.CliClientServiceImpl; +import com.alipay.sofa.rpc.config.ServerConfig; +import com.alipay.sofa.jraft.rpc.RaftRpcServerFactory; +import com.alipay.sofa.jraft.util.Endpoint; public class GraphManagerConfigTest { + private static ExecutorService previousEventExecutor; + private static ExecutorService previousTaskDbExecutor; + private static ExecutorService taskDbExecutor; + + @BeforeClass + public static void initEventExecutor() { + // Other suite classes may have destroyed the global EventHub executor. + ExecutorService previous = Whitebox.getInternalState(EventHub.class, "executor"); + previousEventExecutor = previous != null && !previous.isShutdown() ? previous : null; + Whitebox.setInternalState(EventHub.class, "executor", null); + EventHub.init(2); + previousTaskDbExecutor = Whitebox.getInternalState(TaskManager.instance(), "taskDbExecutor"); + taskDbExecutor = Executors.newSingleThreadExecutor( + runnable -> new Thread(runnable, "task-db-worker-rpc-config-test")); + Whitebox.setInternalState(TaskManager.instance(), "taskDbExecutor", taskDbExecutor); + } + + @AfterClass + public static void restoreEventExecutor() throws InterruptedException { + try { + Assert.assertTrue(EventHub.destroy(5)); + } finally { + Whitebox.setInternalState(EventHub.class, "executor", previousEventExecutor != null && + !previousEventExecutor.isShutdown() ? previousEventExecutor : null); + taskDbExecutor.shutdown(); + try { + Assert.assertTrue(taskDbExecutor.awaitTermination(5, TimeUnit.SECONDS)); + } finally { + Whitebox.setInternalState(TaskManager.instance(), "taskDbExecutor", previousTaskDbExecutor); + } + } + } + + @Test + public void testFixturePreservesLivePreviousEventExecutor() throws Exception { + this.checkFixtureEventExecutor(false); + } + + @Test + public void testFixtureDoesNotRestoreDestroyedEventExecutor() throws Exception { + this.checkFixtureEventExecutor(true); + } + + private void checkFixtureEventExecutor(boolean destroyed) throws Exception { + ExecutorService activeEvent = Whitebox.getInternalState(EventHub.class, "executor"); + ExecutorService activeTask = Whitebox.getInternalState(TaskManager.instance(), "taskDbExecutor"); + ExecutorService savedPreviousEvent = previousEventExecutor; + ExecutorService savedPreviousTask = previousTaskDbExecutor; + ExecutorService savedTask = taskDbExecutor; + ExecutorService prior = Executors.newSingleThreadExecutor(); + try { + prior.submit(() -> { }).get(5, TimeUnit.SECONDS); + if (destroyed) { + prior.shutdown(); + Assert.assertTrue(prior.awaitTermination(5, TimeUnit.SECONDS)); + } + Whitebox.setInternalState(EventHub.class, "executor", prior); + initEventExecutor(); + restoreEventExecutor(); + EventHub.init(1); + EventHub hub = new EventHub("rpc-fixture-executor-control"); + hub.listen("probe", event -> true); + RuntimeException submission = null; + try { + Assert.assertEquals(Integer.valueOf(1), hub.notify("probe").get(5, TimeUnit.SECONDS)); + } catch (RuntimeException failure) { + submission = failure; + } + ExecutorService restored = Whitebox.getInternalState(EventHub.class, "executor"); + Assert.assertNull("Previous destroyed=" + destroyed + ", restoredShutdown=" + + restored.isShutdown() + ", notifyFailure=" + submission, submission); + if (!destroyed) { + Assert.assertSame(prior, restored); + } else { + Assert.assertNotSame(prior, restored); + } + } finally { + ExecutorService current = Whitebox.getInternalState(EventHub.class, "executor"); + if (current != null && current != activeEvent) { + current.shutdown(); + Assert.assertTrue(current.awaitTermination(5, TimeUnit.SECONDS)); + } + prior.shutdown(); + Assert.assertTrue(prior.awaitTermination(5, TimeUnit.SECONDS)); + Whitebox.setInternalState(EventHub.class, "executor", activeEvent); + Whitebox.setInternalState(TaskManager.instance(), "taskDbExecutor", activeTask); + previousEventExecutor = savedPreviousEvent; + previousTaskDbExecutor = savedPreviousTask; + taskDbExecutor = savedTask; + } + } + + @Rule + public TemporaryFolder temporary = new TemporaryFolder(); + + @Test + public void testRpcFeatureCheckPreservesCallbackTransaction() throws Exception { + for (boolean shared : new boolean[]{false, true}) { + for (boolean opened : new boolean[]{false, true}) { + this.checkRpcCallbackTransaction(shared, opened, false); + if (shared) { + this.checkRpcCallbackTransaction(true, opened, true); + } + } + } + } + + private void checkRpcCallbackTransaction(boolean shared, boolean opened, boolean failing) throws Exception { + OptionSpace.register("core", CoreOptions.instance()); + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(CoreOptions.BACKEND.name(), "memory"); + properties.setProperty(CoreOptions.STORE.name(), "callback_tx"); + StandardHugeGraph graph = shared ? new SharedMemoryGraph(new HugeConfig(properties)) : + new StandardHugeGraph(new HugeConfig(properties)); + RpcServiceConfig4Server server = Mockito.mock(RpcServiceConfig4Server.class); + RpcServiceConfig4Client client = Mockito.mock(RpcServiceConfig4Client.class); + IllegalStateException failure = new IllegalStateException("RPC proxy failed"); + if (failing) { + Mockito.doThrow(failure).when(client).serviceProxy(Mockito.anyString(), Mockito.any(Class.class)); + } + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", graph); + ExecutorService callback = Executors.newSingleThreadExecutor(); + try { + callback.submit(() -> { + try { + if (opened) { + graph.tx().open(); + } + if (failing) { + Assert.assertThrows(IllegalStateException.class, + () -> graph.registerRpcServices(server, client), + caught -> Assert.assertSame(failure, caught)); + } else { + graph.registerRpcServices(server, client); + } + Assert.assertEquals(opened, graph.tx().isOpen()); + AtomicInteger refs = Whitebox.getInternalState(graph.tx(), "refs"); + Assert.assertEquals(opened ? 1 : 0, refs.get()); + ThreadLocal transactions = Whitebox.getInternalState(graph.tx(), "transactions"); + Assert.assertEquals(opened, transactions.get() != null); + } finally { + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", graph); + } + }).get(5, TimeUnit.SECONDS); + } finally { + callback.shutdownNow(); + Assert.assertTrue(callback.awaitTermination(5, TimeUnit.SECONDS)); + graph.close(); + } + } + + @Test + public void testRpcFeatureFailurePreservesPrimaryException() throws Exception { + OptionSpace.register("core", CoreOptions.instance()); + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(CoreOptions.BACKEND.name(), "memory"); + properties.setProperty(CoreOptions.STORE.name(), "feature_failure"); + FeatureFailureGraph graph = new FeatureFailureGraph(new HugeConfig(properties)); + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", graph); + IllegalStateException failure = new IllegalStateException("backend feature read failed"); + IllegalStateException cleanup = new IllegalStateException("transaction listener failed"); + graph.failure = failure; + graph.tx().addTransactionListener(status -> { + throw cleanup; + }); + try { + Assert.assertThrows(IllegalStateException.class, () -> graph.registerRpcServices(null, null), + caught -> Assert.assertSame(failure, caught)); + Assert.assertFalse(graph.tx().isOpen()); + Assert.assertEquals(1, failure.getSuppressed().length); + Assert.assertSame(cleanup, failure.getSuppressed()[0]); + ThreadLocal transactions = Whitebox.getInternalState(graph.tx(), "transactions"); + Assert.assertNull(transactions.get()); + } finally { + graph.failure = null; + graph.tx().clearTransactionListeners(); + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", graph); + graph.close(); + } + } + + private static class FeatureFailureGraph extends StandardHugeGraph { + + private RuntimeException failure; + + FeatureFailureGraph(HugeConfig config) { + super(config); + } + + @Override + public BackendFeatures backendStoreFeatures() { + BackendFeatures features = super.backendStoreFeatures(); + if (this.failure != null) { + throw this.failure; + } + return features; + } + } + + @Test + public void testLocalLoadRollbackUsesAdminScope() throws Exception { + this.checkLocalLoadRollback(false); + } + + @Test + public void testLocalLoadRollbackRetainsOwnerAfterCloseFailure() throws Exception { + this.checkLocalLoadRollback(true); + } + + private void checkLocalLoadRollback(boolean foreignTransaction) throws Exception { + OptionSpace.register("core", CoreOptions.instance()); + OptionSpace.register("rpc", RpcOptions.instance()); + OptionSpace.register("server", ServerOptions.instance()); + String name = foreignTransaction ? "rollback_pending" : "rollback_admin"; + File config = this.temporary.newFile(name + ".properties"); + Files.write(config.toPath(), ("gremlin.graph=" + TrackedAuthMemoryGraph.class.getName() + + "\nbackend=memory\nstore=" + name + "\n").getBytes(StandardCharsets.UTF_8)); + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(ServerOptions.GRAPHS.name(), this.temporary.newFolder(name).getPath()); + properties.setProperty(ServerOptions.USE_PD.name(), false); + properties.setProperty(RpcOptions.RPC_SERVER_HOST.name(), "127.0.0.1"); + properties.setProperty(RpcOptions.RPC_REMOTE_URL.name(), "127.0.0.1:1"); + GraphManager manager = new GraphManager(new HugeConfig(properties), new EventHub(name)); + RpcServer server = Mockito.mock(RpcServer.class); + Mockito.when(server.enabled()).thenReturn(true); + RpcServer originalServer = Whitebox.getInternalState(manager, "rpcServer"); + Mockito.when(server.config()).thenReturn(originalServer.config()); + IllegalStateException failure = new IllegalStateException("RPC publication failed"); + Mockito.doThrow(failure).when(server).exportAll(); + Whitebox.setInternalState(manager, "rpcServer", server); + Whitebox.setInternalState(manager, "rpcInitialized", true); + StandardHugeGraph graph = (StandardHugeGraph) HugeFactory.open(new HugeConfig(config.getPath())); + graph.initBackend(); + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", graph); + CountDownLatch ready = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + ExecutorService worker = Executors.newSingleThreadExecutor(); + Future transaction = foreignTransaction ? worker.submit(() -> { + graph.tx().open(); + ready.countDown(); + try { + Assert.assertTrue(release.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(interrupted); + } finally { + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", graph); + } + }) : null; + String caller = foreignTransaction ? HugeAuthenticator.User.ADMIN.toJson() : + new HugeAuthenticator.User("writer", new WriterRole("DEFAULT", name)).toJson(); + AuthContext.setContext(caller); + TaskManager.setContext(caller); + HugeGraphAuthProxy.setRequestGraphSpace("DEFAULT"); + try { + if (foreignTransaction) { + Assert.assertTrue(ready.await(5, TimeUnit.SECONDS)); + } + manager.loadGraphsFromLocal(Map.of(name, config.getPath())); + Assert.assertEquals(caller, AuthContext.getContext()); + Assert.assertEquals(caller, TaskManager.getContext()); + Assert.assertEquals("DEFAULT", HugeGraphAuthProxy.getRequestGraphSpace()); + Map owners = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + if (foreignTransaction) { + Assert.assertFalse(graph.closed()); + Assert.assertSame(graph, owners.get(graph.spaceGraphName())); + Assert.assertSame(TrackedAuthMemoryGraph.last, manager.graph(graph.spaceGraphName())); + Assert.assertEquals(1, failure.getSuppressed().length); + } else { + Assert.assertTrue(graph.closed()); + Assert.assertFalse(owners.containsKey(graph.spaceGraphName())); + Assert.assertFalse(manager.graphs().contains(graph.spaceGraphName())); + Assert.assertEquals(0, failure.getSuppressed().length); + } + } finally { + release.countDown(); + if (transaction != null) { + transaction.get(5, TimeUnit.SECONDS); + } + worker.shutdownNow(); + Assert.assertTrue(worker.awaitTermination(5, TimeUnit.SECONDS)); + HugeGraphAuthProxy.runAsAdmin(() -> { + manager.close(); + try { + graph.close(); + } catch (Exception cleanup) { + throw new IllegalStateException(cleanup); + } + }); + HugeFactory.remove(graph); + TrackedAuthMemoryGraph.last = null; + HugeGraphAuthProxy.resetContext(); + TaskManager.resetContext(); + } + } + + @Test + public void testRetainedLoadCandidateIsNotRepublishedAtRpcStartup() throws Exception { + this.checkRetainedCandidateRpcStartup(false); + } + + @Test + public void testRetainedCreateCandidateIsNotRepublishedAtRpcStartup() throws Exception { + this.checkRetainedCandidateRpcStartup(true); + } + + private void checkRetainedCandidateRpcStartup(boolean create) throws Exception { + String name = create ? "failed_create_startup" : "failed_load_startup"; + GraphManager manager = this.dropManager(name); + RpcServer original = Whitebox.getInternalState(manager, "rpcServer"); + RpcServer server = Mockito.spy(original); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + Whitebox.setInternalState(manager, "rpcServer", server); + Whitebox.setInternalState(manager, "rpcInitialized", true); + ExecutorService worker = Executors.newSingleThreadExecutor(); + CountDownLatch ready = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicReference> transaction = new AtomicReference<>(); + IllegalStateException rejected = new IllegalStateException("candidate RPC export rejected"); + Mockito.doAnswer(invocation -> { + HugeGraph graph = TrackedSharedMemoryGraph.last; + Assert.assertNotNull(graph); + if (!create) { + graph.initBackend(); + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", graph); + } + transaction.set(worker.submit(() -> { + graph.tx().open(); + ready.countDown(); + try { + E.checkState(release.await(10, TimeUnit.SECONDS), "Candidate transaction barrier timed out"); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(interrupted); + } finally { + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", graph); + } + })); + Assert.assertTrue(ready.await(5, TimeUnit.SECONDS)); + throw rejected; + }).when(server).exportAll(); + try { + String configText = "gremlin.graph=" + TrackedSharedMemoryGraph.class.getName() + + "\nbackend=memory\nstore=" + name + "\n"; + if (create) { + Assert.assertThrows(IllegalStateException.class, () -> manager.createGraphLocal(name, configText), + caught -> Assert.assertSame(rejected, caught)); + } else { + File config = this.temporary.newFile(name + ".properties"); + Files.write(config.toPath(), configText.getBytes(StandardCharsets.UTF_8)); + manager.loadGraphsFromLocal(Map.of(name, config.getPath())); + } + HugeGraph candidate = TrackedSharedMemoryGraph.last; + Assert.assertFalse(candidate.closed()); + Assert.assertSame(candidate, manager.graph(candidate.spaceGraphName())); + Assert.assertEquals(1, rejected.getSuppressed().length); + assertRpcResources(server, client, 0); + Mockito.doCallRealMethod().when(server).exportAll(); + Whitebox.setInternalState(manager, "rpcInitialized", false); + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + Assert.assertSame(candidate, manager.graph(candidate.spaceGraphName())); + Assert.assertFalse(candidate.closed()); + assertRpcResources(server, client, 0); + } finally { + release.countDown(); + if (transaction.get() != null) { + transaction.get().get(5, TimeUnit.SECONDS); + } + worker.shutdown(); + Assert.assertTrue(worker.awaitTermination(5, TimeUnit.SECONDS)); + manager.close(); + if (TrackedSharedMemoryGraph.last != null) { + TrackedSharedMemoryGraph.last.close(); + HugeFactory.remove(TrackedSharedMemoryGraph.last); + } + TrackedSharedMemoryGraph.last = null; + } + } + + public static class TrackedSharedMemoryGraph extends SharedMemoryGraph { + + private static TrackedSharedMemoryGraph last; + + public TrackedSharedMemoryGraph(HugeConfig config) { + super(config); + } + + public static TrackedSharedMemoryGraph open(Configuration config) { + last = new TrackedSharedMemoryGraph(new HugeConfig(config)); + return last; + } + } + + public static class TrackedAuthMemoryGraph { + + private static HugeGraph last; + + public static HugeGraph open(Configuration config) { + last = new HugeGraphAuthProxy(HugeFactory.open(config)); + return last; + } + } + + @Test + public void testAuthenticatedRpcCleanupPreservesOwnerUntilAdmin() throws Exception { + OptionSpace.register("core", CoreOptions.instance()); + OptionSpace.register("rpc", RpcOptions.instance()); + OptionSpace.register("server", ServerOptions.instance()); + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(ServerOptions.GRAPHS.name(), this.temporary.newFolder("auth").getPath()); + properties.setProperty(ServerOptions.USE_PD.name(), false); + properties.setProperty(ServerOptions.GRAPH_LOAD_FROM_LOCAL_CONFIG.name(), false); + properties.setProperty(ServerOptions.ENABLE_DYNAMIC_CREATE_DROP.name(), true); + properties.setProperty(RpcOptions.RPC_SERVER_HOST.name(), "127.0.0.1"); + properties.setProperty(RpcOptions.RPC_SERVER_PORT.name(), 0); + try (ServerSocket reservation = new ServerSocket(0)) { + properties.setProperty(RpcOptions.RPC_REMOTE_URL.name(), "127.0.0.1:" + reservation.getLocalPort()); + } + EventHub hub = new EventHub("authenticated-rpc-cleanup"); + GraphManager manager = new GraphManager(new HugeConfig(properties), hub); + String originalThreadName = Thread.currentThread().getName(); + AuthContext.setContext(HugeAuthenticator.User.ADMIN.toJson()); + TaskManager.setContext(HugeAuthenticator.User.ADMIN.toJson()); + try { + String factory = "gremlin.graph=" + AuthSharedMemoryGraph.class.getName() + "\nbackend=memory\nstore="; + manager.createGraphLocal("auth_startup", factory + "auth_startup\n"); + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + HugeGraph owner = manager.createGraphLocal("auth_late", factory + "auth_late\n"); + RpcServer server = Whitebox.getInternalState(manager, "rpcServer"); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + assertRpcResources(server, client, 4); + EventListener rejection = event -> { + throw new IllegalStateException("reject authenticated candidate"); + }; + hub.listen(Events.GRAPH_CREATE, rejection); + try { + Assert.assertThrows(HugeException.class, () -> manager.createGraphLocal( + "auth_rejected", factory + "auth_rejected\n")); + } finally { + hub.unlisten(Events.GRAPH_CREATE, rejection); + } + assertRpcResources(server, client, 4); + TaskManager.resetContext(); + RolePermission execute = RolePermission.role(owner.graphSpace(), owner.name(), HugePermission.EXECUTE); + Assert.assertFalse(RolePermission.isAdmin(execute)); + AuthContext.setContext(new HugeAuthenticator.User("reader", execute).toJson()); + SimpleBindings bindings = new SimpleBindings(); + bindings.put("graph", owner); + GremlinGroovyScriptEngine engine = new GremlinGroovyScriptEngine(); + Assert.assertThrows(ScriptException.class, () -> engine.eval("graph.unregisterRpcServices()", bindings), + error -> Assert.assertContains("Permission denied", error.getMessage())); + assertRpcResources(server, client, 4); + AuthContext.resetContext(); + Assert.assertThrows(IllegalStateException.class, owner::unregisterRpcServices, + error -> Assert.assertContains("Missing authentication context", error.getMessage())); + assertRpcResources(server, client, 4); + HugeGraphAuthProxy.runAsAdmin(owner::unregisterRpcServices); + assertRpcResources(server, client, 2); + AuthContext.setContext(HugeAuthenticator.User.ADMIN.toJson()); + // Use the normal PD-drop branch with explicit admin identity and no placement metadata writes. + Whitebox.setInternalState(manager, "PDExist", true); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + spaces.put("DEFAULT", new GraphSpace("DEFAULT")); + Thread.currentThread().setName("grpc-auth-cleanup-test"); + manager.dropGraph("DEFAULT", "auth_late", false); + Assert.assertTrue(owner.closed()); + assertRpcResources(server, client, 2); + } finally { + Thread.currentThread().setName(originalThreadName); + try { + HugeGraphAuthProxy.runAsAdmin(manager::close); + } finally { + HugeGraphAuthProxy.resetContext(); + TaskManager.resetContext(); + } + } + } + + @Test + public void testAuthenticatedPublicationPreservesCallerScope() throws Exception { + OptionSpace.register("core", CoreOptions.instance()); + OptionSpace.register("rpc", RpcOptions.instance()); + OptionSpace.register("server", ServerOptions.instance()); + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(ServerOptions.GRAPHS.name(), this.temporary.newFolder("lazy").getPath()); + properties.setProperty(ServerOptions.USE_PD.name(), false); + properties.setProperty(ServerOptions.GRAPH_LOAD_FROM_LOCAL_CONFIG.name(), false); + properties.setProperty(RpcOptions.RPC_SERVER_HOST.name(), "127.0.0.1"); + properties.setProperty(RpcOptions.RPC_SERVER_PORT.name(), 0); + try (ServerSocket reservation = new ServerSocket(0)) { + properties.setProperty(RpcOptions.RPC_REMOTE_URL.name(), "127.0.0.1:" + reservation.getLocalPort()); + } + GraphManager manager = new GraphManager(new HugeConfig(properties), new EventHub("lazy-auth-rpc")); + SharedMemoryGraph warm = null; + try { + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + String space = "rpcscope"; + String name = "lazy_auth"; + PropertiesConfiguration config = new PropertiesConfiguration(); + config.setProperty(CoreOptions.BACKEND.name(), "memory"); + config.setProperty(CoreOptions.STORE.name(), name); + config.setProperty(CoreOptions.GRAPH_SPACE.name(), space); + warm = new SharedMemoryGraph(new HugeConfig(config)); + warm.initBackend(); + HugeGraph owner = new HugeGraphAuthProxy(warm); + RolePermission role = new WriterRole(space, name); + Assert.assertFalse(RolePermission.isAdmin(role)); + String caller = new HugeAuthenticator.User("writer", role).toJson(); + AuthContext.setContext(caller); + TaskManager.setContext(caller); + HugeGraphAuthProxy.setRequestGraphSpace(space); + Whitebox.invoke(GraphManager.class, new Class[]{String.class, HugeGraph.class}, + "publishGraph", manager, owner.spaceGraphName(), owner); + Assert.assertSame(owner, manager.graph(space + "-" + name)); + Assert.assertEquals(caller, AuthContext.getContext()); + Assert.assertEquals(caller, TaskManager.getContext()); + Assert.assertEquals(space, HugeGraphAuthProxy.getRequestGraphSpace()); + Assert.assertEquals("writer", HugeGraphAuthProxy.username()); + RpcServer server = Whitebox.getInternalState(manager, "rpcServer"); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + assertRpcResources(server, client, 2); + Assert.assertThrows(jakarta.ws.rs.ForbiddenException.class, owner::unregisterRpcServices); + assertRpcResources(server, client, 2); + } finally { + HugeGraph retained = warm; + HugeGraphAuthProxy.runAsAdmin(() -> { + manager.close(); + if (retained != null) { + try { + retained.close(); + } catch (Exception failure) { + throw new IllegalStateException(failure); + } finally { + HugeFactory.remove(retained); + } + } + }); + HugeGraphAuthProxy.resetContext(); + TaskManager.resetContext(); + } + } + + private static class WriterRole extends RolePermission { + + WriterRole(String space, String graph) { + this.add(space, graph, HugePermission.READ, HugeResource.ALL_RES); + this.add(space, graph, HugePermission.WRITE, HugeResource.ALL_RES); + } + } + + public static class AuthSharedMemoryGraph { + + public static HugeGraph open(Configuration config) { + return new HugeGraphAuthProxy(new SharedMemoryGraph(new HugeConfig(config))); + } + } + + @Test + public void testLegacyDefaultCleanupDoesNotAbortDropWithoutRpc() throws Exception { + this.checkLegacyDrop(false, false); + } + + @Test + public void testActualCleanupFailureStillPropagates() throws Exception { + this.checkLegacyDrop(true, false); + } + + @Test + public void testClearCleanupFailurePreservesOwnerForRetry() throws Exception { + this.checkLegacyDrop(true, true); + } + + @Test + public void testQuotaUpdateFailurePreservesOwnerAndCountForRetry() throws Exception { + this.checkLegacyDrop(false, false, true); + } + + private void checkLegacyDrop(boolean failCleanup, boolean clear) throws Exception { + this.checkLegacyDrop(failCleanup, clear, false); + } + + private void checkLegacyDrop(boolean failCleanup, boolean clear, boolean failQuota) throws Exception { + OptionSpace.register("core", CoreOptions.instance()); + OptionSpace.register("rpc", RpcOptions.instance()); + OptionSpace.register("server", ServerOptions.instance()); + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(ServerOptions.GRAPHS.name(), this.temporary.newFolder("legacy").getPath()); + properties.setProperty(ServerOptions.GRAPH_LOAD_FROM_LOCAL_CONFIG.name(), false); + properties.setProperty(ServerOptions.USE_PD.name(), false); + properties.setProperty(ServerOptions.ENABLE_DYNAMIC_CREATE_DROP.name(), true); + properties.setProperty(RpcOptions.RPC_SERVER_HOST.name(), ""); + properties.setProperty(RpcOptions.RPC_REMOTE_URL.name(), ""); + EventHub hub = new EventHub("legacy-rpc-cleanup-test"); + GraphManager manager = new GraphManager(new HugeConfig(properties), hub); + HugeGraph delegate = null; + String originalThreadName = Thread.currentThread().getName(); + try { + delegate = manager.createGraphLocal("legacy_cleanup", "gremlin.graph=" + HugeFactory.class.getName() + + "\nbackend=memory\nstore=legacy_cleanup\n"); + HugeGraph owner = delegate; + IllegalStateException failure = new IllegalStateException("actual owner cleanup failed"); + AtomicBoolean failing = new AtomicBoolean(failCleanup); + HugeGraph legacy = (HugeGraph) Proxy.newProxyInstance(HugeGraph.class.getClassLoader(), + new Class[]{HugeGraph.class}, (proxy, method, args) -> { + if ("unregisterRpcServices".equals(method.getName())) { + if (failing.get()) { + throw failure; + } + return InvocationHandler.invokeDefault(proxy, method, args); + } + try { + return method.invoke(owner, args); + } catch (InvocationTargetException error) { + throw error.getCause(); + } + }); + Map graphs = Whitebox.getInternalState(manager, "graphs"); + graphs.put(legacy.spaceGraphName(), legacy); + DefaultGraphManager bindings = new DefaultGraphManager(new Settings()); + bindings.putGraph(legacy.spaceGraphName(), legacy); + CountDownLatch dropped = new CountDownLatch(1); + hub.listen(Events.GRAPH_DROP, event -> { + if (bindings.getGraph(legacy.spaceGraphName()) == legacy) { + try { + bindings.removeGraph(legacy.spaceGraphName()); + } catch (Exception failureToUnbind) { + throw new IllegalStateException(failureToUnbind); + } + } + dropped.countDown(); + return true; + }); + MetaManager meta = Mockito.mock(MetaManager.class); + Whitebox.setInternalState(manager, "metaManager", meta); + RpcServer server = Whitebox.getInternalState(manager, "rpcServer"); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + Assert.assertFalse(server.enabled()); + Assert.assertFalse(client.enabled()); + // Reach the normal PD-drop branch without booting PD or writing placement metadata. + Whitebox.setInternalState(manager, "PDExist", true); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + GraphSpace space = new GraphSpace("DEFAULT"); + space.graphNumberUsed(5); + spaces.put("DEFAULT", space); + if (failQuota) { + Mockito.doThrow(failure).doNothing().when(meta).updateGraphSpaceConfig("DEFAULT", space); + } else { + Thread.currentThread().setName("grpc-legacy-cleanup-test"); + } + if (failCleanup || failQuota) { + Assert.assertThrows(IllegalStateException.class, () -> manager.dropGraph( + "DEFAULT", "legacy_cleanup", clear), caught -> Assert.assertSame(failure, caught)); + Assert.assertEquals(!clear || failQuota, owner.closed()); + Assert.assertSame(legacy, manager.graph(legacy.spaceGraphName())); + Assert.assertSame(legacy, bindings.getGraph(legacy.spaceGraphName())); + Map factory = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + Assert.assertSame(owner, factory.get(legacy.spaceGraphName())); + Assert.assertEquals(1L, dropped.getCount()); + if (failCleanup) { + Mockito.verifyNoInteractions(meta); + } + Assert.assertEquals(5, space.graphNumberUsed()); + failing.set(false); + } + manager.dropGraph("DEFAULT", "legacy_cleanup", clear); + Assert.assertTrue(dropped.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(owner.closed()); + Map factory = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + Assert.assertFalse(factory.containsKey(legacy.spaceGraphName())); + Assert.assertNull(bindings.getGraph(legacy.spaceGraphName())); + Assert.assertFalse(manager.graphs().contains(legacy.spaceGraphName())); + if (failQuota) { + Assert.assertEquals(4, space.graphNumberUsed()); + Mockito.verify(meta, Mockito.times(2)).updateGraphSpaceConfig("DEFAULT", space); + } + } finally { + Thread.currentThread().setName(originalThreadName); + manager.close(); + if (delegate != null) { + delegate.close(); + HugeFactory.remove(delegate); + } + } + } + + @Test + public void testPeerRemovalBatchContinuesAfterOpenTransaction() throws Exception { + GraphManager manager = this.dropManager("peer_batch"); + HugeGraph busy = null; + HugeGraph next = null; + ExecutorService worker = Executors.newSingleThreadExecutor(); + CountDownLatch opened = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + String threadName = Thread.currentThread().getName(); + try { + busy = manager.createGraphLocal("peer_busy", sharedMemoryConfig("peer_busy")); + next = manager.createGraphLocal("peer_next", sharedMemoryConfig("peer_next")); + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + MetaManager meta = Mockito.mock(MetaManager.class); + Mockito.when(meta.extractGraphsFromResponse("batch")).thenReturn( + List.of(busy.spaceGraphName(), next.spaceGraphName())); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + Thread.currentThread().setName("grpc-peer-removal-test"); + HugeGraph owner = busy; + Future transaction = worker.submit(() -> { + try { + owner.tx().open(); + opened.countDown(); + E.checkState(release.await(10, TimeUnit.SECONDS), "Peer transaction barrier timed out"); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(failure); + } finally { + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", owner); + } + }); + Assert.assertTrue(opened.await(5, TimeUnit.SECONDS)); + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphRemoveHandler", manager, "batch"); + HugeException refusal = Assert.assertThrows(HugeException.class, () -> manager.dropGraph( + "DEFAULT", "peer_busy", false)); + Assert.assertTrue(refusal.getCause() instanceof IllegalStateException); + Assert.assertContains("Ensure scheduler and tx closed", refusal.getCause().getMessage()); + Assert.assertFalse(busy.closed()); + Assert.assertSame(busy, manager.graph(busy.spaceGraphName())); + Assert.assertTrue(next.closed()); + Assert.assertFalse(manager.graphs().contains(next.spaceGraphName())); + RpcServer server = Whitebox.getInternalState(manager, "rpcServer"); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + assertRpcResources(server, client, 2); + release.countDown(); + transaction.get(5, TimeUnit.SECONDS); + } finally { + release.countDown(); + worker.shutdownNow(); + Assert.assertTrue(worker.awaitTermination(5, TimeUnit.SECONDS)); + Thread.currentThread().setName(threadName); + manager.close(); + if (busy != null) { + HugeFactory.remove(busy); + } + if (next != null) { + HugeFactory.remove(next); + } + } + } + + @Test + public void testClosedGraphCloseFailureRetainsOwnerUntilRetry() throws Exception { + this.checkPostCloseDrop(false); + } + + @Test + public void testClosedGraphRpcFailureRetriesWithClear() throws Exception { + this.checkPostCloseDrop(true); + } + + @Test + public void testReceivedSameNameAddLoadsLatestAfterOwnerCleanup() throws Exception { + this.checkReceivedSameNameAddAfterCleanup(true); + } + + @Test + public void testSameNameAddAfterOwnerCleanupLoadsLatestConfig() throws Exception { + this.checkReceivedSameNameAddAfterCleanup(false); + } + + @Test + public void testReceivedSameNameAddDoesNotReloadMissingConfig() throws Exception { + this.checkReceivedSameNameAddAfterCleanup(true, 1); + } + + @Test + public void testReplacementLoadFailureDoesNotFailCompletedDelete() throws Exception { + this.checkReceivedSameNameAddAfterCleanup(true, 2); + } + + @Test + public void testPeerRemoveFailureRetriesOnSameNameAdd() throws Exception { + this.checkReceivedSameNameAddAfterCleanup(true, 0, true); + } + + @Test + public void testLateSelfAddRetriesFailedPeerRemoval() throws Exception { + this.checkReceivedSameNameAddAfterCleanup(false, 0, true, true); + } + + @Test + public void testPeerRemoveFailureDoesNotReloadMissingConfig() throws Exception { + this.checkReceivedSameNameAddAfterCleanup(true, 1, true); + } + + private void checkReceivedSameNameAddAfterCleanup(boolean addDuringCleanup) throws Exception { + this.checkReceivedSameNameAddAfterCleanup(addDuringCleanup, 0); + } + + private void checkReceivedSameNameAddAfterCleanup(boolean addDuringCleanup, int replayFailure) throws Exception { + this.checkReceivedSameNameAddAfterCleanup(addDuringCleanup, replayFailure, false); + } + + private void checkReceivedSameNameAddAfterCleanup(boolean addDuringCleanup, int replayFailure, + boolean peer) throws Exception { + this.checkReceivedSameNameAddAfterCleanup(addDuringCleanup, replayFailure, peer, false); + } + + private void checkReceivedSameNameAddAfterCleanup(boolean addDuringCleanup, int replayFailure, + boolean peer, boolean lateSelfAdd) throws Exception { + String name = lateSelfAdd ? "late_self_add_peer_retry" : + replayFailure == 1 ? "received_add_missing" : replayFailure == 2 ? "received_add_retry" : + addDuringCleanup ? "received_add_early" : "received_add_late"; + String key = "DEFAULT-" + name; + GraphManager manager = this.dropManager(name); + EventHub hub = Whitebox.getInternalState(manager, "eventHub"); + Settings settings = new Settings(); + settings.gremlinPool = 1; + settings.threadPoolBoss = 1; + settings.threadPoolWorker = 1; + ContextGremlinServer gremlin = new ContextGremlinServer(settings, hub); + org.apache.tinkerpop.gremlin.server.GraphManager bindings = + gremlin.getServerGremlinExecutor().getGraphManager(); + HugeGraph owner = null; + HugeGraph replacement = null; + AtomicBoolean reject = new AtomicBoolean(true); + AtomicBoolean rejectReplay = new AtomicBoolean(); + IllegalStateException readFailure = new IllegalStateException("replacement metadata read rejected"); + AtomicInteger cleanupAttempts = new AtomicInteger(); + AtomicInteger singleConfigReads = new AtomicInteger(); + AtomicInteger allConfigReads = new AtomicInteger(); + AtomicReference> latest = new AtomicReference<>(); + try { + owner = manager.createGraphLocal(name, "gremlin.graph=" + HugeFactory.class.getName() + + "\nbackend=memory\nstore=" + name + "\nalias.graph.id=old-config-marker\n"); + HugeGraph graph = owner; + Map factories = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + Assert.assertSame(graph, factories.get(key)); + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + RpcServer server = Whitebox.getInternalState(manager, "rpcServer"); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + // Memory is not a shared backend: actual cache-RPC configuration stays empty. + assertRpcResources(server, client, 0); + BackendStoreProvider provider = Mockito.spy(owner.storeProvider()); + Whitebox.setInternalState(owner, "storeProvider", provider); + IllegalStateException failure = new IllegalStateException("old owner cleanup is retained"); + Mockito.doAnswer(invocation -> { + invocation.callRealMethod(); + cleanupAttempts.incrementAndGet(); + if (reject.get()) { + throw failure; + } + return null; + }).when(provider).close(); + MetaManager meta = Mockito.mock(MetaManager.class); + GraphSpace space = new GraphSpace("DEFAULT"); + space.graphNumberUsed(5); + Mockito.when(meta.graphSpace("DEFAULT")).thenReturn(space); + Mockito.when(meta.extractGraphsFromResponse("new-configuration")).thenReturn( + java.util.Collections.singletonList(key)); + Mockito.when(meta.extractGraphsFromResponse("remove-configuration")).thenReturn( + java.util.Collections.singletonList(key)); + Mockito.when(meta.getGraphConfig("DEFAULT", name)).thenAnswer(invocation -> { + singleConfigReads.incrementAndGet(); + if (rejectReplay.get()) { + throw readFailure; + } + Map current = latest.get(); + return current == null ? null : new HashMap<>(current); + }); + Mockito.when(meta.graphConfigs("DEFAULT")).thenAnswer(invocation -> { + allConfigReads.incrementAndGet(); + Map current = latest.get(); + Map> configs = new HashMap<>(); + if (current != null) { + configs.put(key, new HashMap<>(current)); + } + return configs; + }); + Mockito.doAnswer(invocation -> { + latest.set(null); + return null; + }).when(meta).removeGraphConfig("DEFAULT", name); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + Whitebox.setInternalState(manager, "pdPeers", "127.0.0.1:8686"); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + spaces.put("DEFAULT", space); + if (lateSelfAdd) { + // Local creation is still waiting for its own PD acknowledgement. + java.util.Set creating = Whitebox.getInternalState(manager, "creatingGraphs"); + creating.add(key); + } + String threadName = Thread.currentThread().getName(); + if (peer) { + try { + Thread.currentThread().setName("grpc-peer-removal-control"); + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphRemoveHandler", manager, "remove-configuration"); + } finally { + Thread.currentThread().setName(threadName); + } + Assert.assertThrows(HugeException.class, () -> new org.apache.hugegraph.api.profile.GraphsAPI().drop( + manager, "DEFAULT", name, "I'm sure to drop the graph")); + } else { + Assert.assertThrows(HugeException.class, () -> new org.apache.hugegraph.api.profile.GraphsAPI().drop( + manager, "DEFAULT", name, "I'm sure to drop the graph"), + caught -> Assert.assertSame(failure, caught.getCause())); + } + Assert.assertTrue(graph.closed()); + Assert.assertSame(graph, manager.graph(key)); + Assert.assertSame(graph, factories.get(key)); + Assert.assertNull(latest.get()); + Map newConfig = new HashMap<>(); + newConfig.put(CoreOptions.BACKEND.name(), "memory"); + newConfig.put(CoreOptions.STORE.name(), name); + // This is a latest-config marker; Memory does not implement a physical HStore GraphID. + newConfig.put(CoreOptions.ALIAS_NAME.name(), "new-config-marker"); + newConfig.put("nickname", "new_generation"); + latest.set(newConfig); + if (addDuringCleanup) { + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphAddHandler", manager, "new-configuration"); + Assert.assertSame(graph, manager.graph(key)); + Assert.assertEquals(0, singleConfigReads.get()); + } + if (replayFailure == 1) { + latest.set(null); + } else if (replayFailure == 2) { + rejectReplay.set(true); + } + reject.set(false); + if (lateSelfAdd) { + java.util.Set failed = Whitebox.getInternalState(manager, "failedPeerRemovals"); + Assert.assertTrue(failed.contains(graph)); + } + // A replacement read failure must not turn completed old-owner deletion into a failed DELETE. + if (peer) { + try { + Thread.currentThread().setName("grpc-peer-add-control"); + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphAddHandler", manager, "new-configuration"); + } finally { + Thread.currentThread().setName(threadName); + } + Mockito.verify(meta, Mockito.never()).removeGraphConfig("DEFAULT", name); + Mockito.verify(meta, Mockito.never()).notifyGraphRemove("DEFAULT", name); + Assert.assertEquals(5, space.graphNumberUsed()); + } else { + new org.apache.hugegraph.api.profile.GraphsAPI().drop( + manager, "DEFAULT", name, "I'm sure to drop the graph"); + } + Assert.assertEquals(peer && addDuringCleanup ? 3 : 2, cleanupAttempts.get()); + Assert.assertNotSame(graph, factories.get(key)); + java.util.Set pending = Whitebox.getInternalState(manager, "pendingGraphAdds"); + if (replayFailure == 1) { + Assert.assertNull(manager.graph(key)); + Assert.assertNull(manager.graph("DEFAULT", name)); + Assert.assertFalse(factories.containsKey(key)); + Assert.assertFalse(pending.contains(key)); + return; + } + Assert.assertSame(newConfig, latest.get()); + if (replayFailure == 2) { + Assert.assertNull(manager.graph(key)); + Assert.assertFalse(factories.containsKey(key)); + Assert.assertTrue(pending.contains(key)); + Assert.assertEquals(1, singleConfigReads.get()); + rejectReplay.set(false); + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphAddHandler", manager, "new-configuration"); + } + if (!addDuringCleanup && !peer) { + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphAddHandler", manager, "new-configuration"); + } + HugeGraph automatic = manager.graph(key); + boolean listedBeforePublicRead = manager.graphs().contains(key); + int configReadsBeforePublicRead = singleConfigReads.get(); + // The public two-argument lookup has an existing lazy metadata loading fallback. + replacement = manager.graph("DEFAULT", name); + Assert.assertNotNull(replacement); + Assert.assertNotSame(graph, replacement); + Assert.assertSame(replacement, manager.graph(key)); + Assert.assertSame(replacement, factories.get(key)); + Assert.assertEquals("new-config-marker", replacement.configuration().get(CoreOptions.ALIAS_NAME)); + Assert.assertEquals("new_generation", replacement.nickname()); + Assert.assertEquals(0L, replacement.traversal().V().count().next().longValue()); + Assert.assertTrue(replacement.started()); + Assert.assertFalse(pending.contains(key)); + Assert.assertSame(replacement, bindings.getGraph(key)); + Assert.assertNotNull(bindings.getTraversalSource("__g_" + key)); + Assert.assertSame(replacement, bindings.getAsBindings().get(key)); + replacement.schema().vertexLabel("replacement_marker").useAutomaticId().create(); + replacement.addVertex(org.apache.tinkerpop.gremlin.structure.T.label, "replacement_marker"); + replacement.tx().commit(); + // The old asynchronous DROP may arrive after the new CREATE; native bindings check identity. + hub.notifySync(Events.GRAPH_DROP, graph); + Assert.assertSame(replacement, bindings.getGraph(key)); + Assert.assertNotNull(bindings.getTraversalSource("__g_" + key)); + Assert.assertSame(replacement, bindings.getAsBindings().get(key)); + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphAddHandler", manager, "new-configuration"); + Assert.assertSame(replacement, manager.graph(key)); + Assert.assertEquals(1L, replacement.traversal().V().count().next().longValue()); + assertRpcResources(server, client, 0); + String facts = String.format("same-name add during cleanup=%s automaticOwner=%s listedBeforeRead=%s " + + "getConfigBeforeRead=%s graphConfigsReads=%s publicLazyLoadedLatest=%s replacementStarted=%s " + + "rpcProviderConfigs=%s", addDuringCleanup, automatic == null ? "absent" : "new", + listedBeforePublicRead, configReadsBeforePublicRead, allConfigReads.get(), + replacement != graph, replacement.started(), server.config().configs().size()); + Assert.assertNotNull(facts, automatic); + Assert.assertTrue(facts, listedBeforePublicRead); + } finally { + reject.set(false); + rejectReplay.set(false); + manager.close(); + gremlin.stop().get(30, TimeUnit.SECONDS); + if (owner != null) { + owner.close(); + HugeFactory.remove(owner); + } + if (replacement != null) { + replacement.close(); + HugeFactory.remove(replacement); + } + } + } + + @Test + public void testReloadAfterRestCreateWithoutSelfAdd() throws Exception { + this.checkReloadAfterRestCreate(false, false); + } + + @Test + public void testReloadAfterRestCreateAndSelfAdd() throws Exception { + this.checkReloadAfterRestCreate(true, false); + } + + @Test + public void testReloadAfterHealthyDuplicateAdds() throws Exception { + this.checkReloadAfterRestCreate(true, true); + } + + private void checkReloadAfterRestCreate(boolean selfAdd, boolean duplicateAdd) throws Exception { + String name = duplicateAdd ? "duplicate_add_reload" : selfAdd ? "self_add_reload" : "reload_without_add"; + String other = name + "_other"; + String key = "DEFAULT-" + name; + GraphManager manager = this.dropManager(name); + EventHub hub = Whitebox.getInternalState(manager, "eventHub"); + Settings settings = new Settings(); + settings.gremlinPool = 1; + settings.threadPoolBoss = 1; + settings.threadPoolWorker = 1; + ContextGremlinServer gremlin = new ContextGremlinServer(settings, hub); + org.apache.tinkerpop.gremlin.server.GraphManager bindings = + gremlin.getServerGremlinExecutor().getGraphManager(); + Map> configs = new HashMap<>(); + MetaManager meta = Mockito.mock(MetaManager.class); + GraphSpace space = new GraphSpace("DEFAULT"); + Mockito.when(meta.graphSpace("DEFAULT")).thenReturn(space); + Mockito.when(meta.extractGraphsFromResponse("self-add")).thenReturn(List.of(key, "DEFAULT-" + other)); + Mockito.when(meta.extractGraphsFromResponse("foreign-add")).thenReturn(List.of(key, "DEFAULT-" + other)); + Mockito.when(meta.graphConfigs("DEFAULT")).thenAnswer(invocation -> new HashMap<>(configs)); + Mockito.when(meta.getGraphConfig(Mockito.eq("DEFAULT"), Mockito.anyString())) + .thenAnswer(invocation -> { + Map config = configs.get("DEFAULT-" + invocation.getArgument(1)); + return config == null ? null : new HashMap<>(config); + }); + Mockito.doAnswer(invocation -> { + Map config = invocation.getArgument(2); + configs.put("DEFAULT-" + invocation.getArgument(1), new HashMap<>(config)); + return null; + }).when(meta).addGraphConfig(Mockito.eq("DEFAULT"), Mockito.anyString(), Mockito.anyMap()); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + Whitebox.setInternalState(manager, "pdPeers", "127.0.0.1:8686"); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + spaces.put("DEFAULT", space); + try { + org.apache.hugegraph.api.profile.GraphsAPI api = new org.apache.hugegraph.api.profile.GraphsAPI(); + for (String graphName : List.of(name, other)) { + Map config = new HashMap<>(); + config.put(CoreOptions.BACKEND.name(), "memory"); + config.put(CoreOptions.SERIALIZER.name(), "text"); + api.create(manager, "DEFAULT", graphName, null, config); + } + HugeGraph owner = manager.graph(key); + HugeGraph otherOwner = manager.graph("DEFAULT-" + other); + Assert.assertSame(owner, bindings.getGraph(key)); + if (selfAdd) { + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphAddHandler", manager, "self-add"); + } + java.util.Set pending = Whitebox.getInternalState(manager, "pendingGraphAdds"); + if (duplicateAdd) { + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphAddHandler", manager, "foreign-add"); + Assert.assertSame(owner, manager.graph(key)); + Assert.assertSame(otherOwner, manager.graph("DEFAULT-" + other)); + Assert.assertTrue(pending.contains(key)); + Assert.assertTrue(pending.contains("DEFAULT-" + other)); + } + Mockito.clearInvocations(meta); + long begin = System.nanoTime(); + System.out.println(String.format("self-add reload before: selfAdd=%s pending=%s graphs=%s", + selfAdd, pending.contains(key), manager.graphs().size())); + try { + api.reload(manager, Map.of("action", "reload")); + } finally { + System.out.println(String.format("self-add reload after: selfAdd=%s durationMs=%s pending=%s graphs=%s", + selfAdd, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin), + pending.contains(key), manager.graphs().size())); + } + Assert.assertTrue(owner.closed()); + Assert.assertTrue(otherOwner.closed()); + Assert.assertNotNull(manager.graph(key)); + Assert.assertNotNull(manager.graph("DEFAULT-" + other)); + Assert.assertNotSame(owner, manager.graph(key)); + Assert.assertNotSame(otherOwner, manager.graph("DEFAULT-" + other)); + Assert.assertEquals(2, manager.graphs().size()); + Assert.assertFalse(pending.contains(key)); + Assert.assertFalse(pending.contains("DEFAULT-" + other)); + Mockito.verify(meta, Mockito.never()).getGraphConfig(Mockito.anyString(), Mockito.anyString()); + Mockito.verify(meta).graphConfigs("DEFAULT"); + Assert.assertSame(manager.graph(key), bindings.getGraph(key)); + Assert.assertSame(manager.graph("DEFAULT-" + other), bindings.getGraph("DEFAULT-" + other)); + Mockito.verify(meta, Mockito.never()).removeGraphConfig(Mockito.anyString(), Mockito.anyString()); + } finally { + List owners = new ArrayList<>(); + for (String graphName : manager.graphs()) { + owners.add(manager.graph(graphName)); + } + manager.close(); + gremlin.stop().get(30, TimeUnit.SECONDS); + for (HugeGraph owner : owners) { + HugeFactory.remove(owner); + } + } + } + + @Test + public void testHealthyOwnerAddBeforePeerRemoveLoadsLatestConfig() throws Exception { + this.checkAddBeforePeerRemove(false, true); + } + + @Test + public void testHealthyOwnerAddBeforePeerRemoveDoesNotReviveMissingConfig() throws Exception { + this.checkAddBeforePeerRemove(true, true); + } + + @Test + public void testDuplicateAddWithoutRemoveKeepsHealthyOwner() throws Exception { + this.checkAddBeforePeerRemove(false, false); + } + + private void checkAddBeforePeerRemove(boolean missingConfig, boolean removeReceived) throws Exception { + String name = missingConfig ? "add_before_remove_missing" : + removeReceived ? "add_before_remove" : "healthy_duplicate_add"; + String key = "DEFAULT-" + name; + GraphManager manager = this.dropManager(name); + EventHub hub = Whitebox.getInternalState(manager, "eventHub"); + Settings settings = new Settings(); + settings.gremlinPool = 1; + settings.threadPoolBoss = 1; + settings.threadPoolWorker = 1; + ContextGremlinServer gremlin = new ContextGremlinServer(settings, hub); + org.apache.tinkerpop.gremlin.server.GraphManager bindings = + gremlin.getServerGremlinExecutor().getGraphManager(); + HugeGraph owner = null; + HugeGraph replacement = null; + try { + owner = manager.createGraphLocal(name, "gremlin.graph=" + HugeFactory.class.getName() + + "\nbackend=memory\nstore=" + name + "\nalias.graph.id=old-before-remove\n"); + Map factories = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + owner.schema().vertexLabel("old_marker").useAutomaticId().create(); + owner.addVertex(org.apache.tinkerpop.gremlin.structure.T.label, "old_marker"); + owner.tx().commit(); + Map latest = new HashMap<>(); + latest.put(CoreOptions.BACKEND.name(), "memory"); + latest.put(CoreOptions.STORE.name(), name); + latest.put(CoreOptions.ALIAS_NAME.name(), "new-before-remove"); + AtomicInteger configReads = new AtomicInteger(); + MetaManager meta = Mockito.mock(MetaManager.class); + GraphSpace space = new GraphSpace("DEFAULT"); + space.graphNumberUsed(5); + Mockito.when(meta.graphSpace("DEFAULT")).thenReturn(space); + Mockito.when(meta.extractGraphsFromResponse("new-add")).thenReturn(List.of(key)); + Mockito.when(meta.extractGraphsFromResponse("old-remove")).thenReturn(List.of(key)); + Mockito.when(meta.getGraphConfig("DEFAULT", name)).thenAnswer(invocation -> { + configReads.incrementAndGet(); + return missingConfig ? null : new HashMap<>(latest); + }); + Mockito.when(meta.graphConfigs("DEFAULT")).thenReturn( + missingConfig ? java.util.Collections.emptyMap() : Map.of(key, latest)); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + Whitebox.setInternalState(manager, "pdPeers", "127.0.0.1:8686"); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + spaces.put("DEFAULT", space); + // Independent key watches can deliver the new ADD before the old REMOVE. + for (int i = 0; i < 2; i++) { + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphAddHandler", manager, "new-add"); + Assert.assertSame(owner, manager.graph(key)); + Assert.assertSame(owner, factories.get(key)); + Assert.assertFalse(owner.closed()); + Assert.assertEquals(0, configReads.get()); + Assert.assertEquals(1L, owner.traversal().V().count().next().longValue()); + owner.tx().close(); + } + if (!removeReceived) { + Assert.assertSame(owner, bindings.getGraph(key)); + return; + } + String threadName = Thread.currentThread().getName(); + try { + Thread.currentThread().setName("grpc-add-before-remove-control"); + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphRemoveHandler", manager, "old-remove"); + } finally { + Thread.currentThread().setName(threadName); + } + Assert.assertTrue(owner.closed()); + Mockito.verify(meta, Mockito.never()).removeGraphConfig("DEFAULT", name); + Mockito.verify(meta, Mockito.never()).notifyGraphRemove("DEFAULT", name); + Assert.assertEquals(5, space.graphNumberUsed()); + if (missingConfig) { + Assert.assertNull(manager.graph(key)); + Assert.assertFalse(factories.containsKey(key)); + hub.notifySync(Events.GRAPH_DROP, owner); + Assert.assertNull(bindings.getGraph(key)); + return; + } + replacement = manager.graph(key); + Assert.assertNotNull("The received new ADD was lost before the old REMOVE", replacement); + Assert.assertNotSame(owner, replacement); + Assert.assertSame(replacement, factories.get(key)); + Assert.assertSame(replacement, bindings.getGraph(key)); + Assert.assertSame(replacement, bindings.getAsBindings().get(key)); + Assert.assertTrue(replacement.started()); + Assert.assertEquals("new-before-remove", replacement.configuration().get(CoreOptions.ALIAS_NAME)); + replacement.schema().vertexLabel("new_marker").useAutomaticId().create(); + replacement.addVertex(org.apache.tinkerpop.gremlin.structure.T.label, "new_marker"); + replacement.tx().commit(); + hub.notifySync(Events.GRAPH_DROP, owner); + Assert.assertSame(replacement, bindings.getGraph(key)); + Assert.assertNotNull(bindings.getTraversalSource("__g_" + key)); + Assert.assertEquals(1L, replacement.traversal().V().count().next().longValue()); + } finally { + manager.close(); + gremlin.stop().get(30, TimeUnit.SECONDS); + if (owner != null) { + owner.close(); + HugeFactory.remove(owner); + } + if (replacement != null) { + replacement.close(); + HugeFactory.remove(replacement); + } + } + } + + @Test + public void testRpcStartupDoesNotRepublishDeletingGraph() throws Exception { + this.checkRpcStartupDuringDeletion(false, false); + } + + @Test + public void testRpcStartupSkipsOwnerAfterRemovalTokenConsumed() throws Exception { + this.checkRpcStartupDuringDeletion(true, false); + } + + @Test + public void testRpcStartupPublishesOtherOwnerDuringDeletion() throws Exception { + this.checkRpcStartupDuringDeletion(true, true); + } + + private void checkRpcStartupDuringDeletion(boolean consumeToken, boolean keepOther) throws Exception { + String name = keepOther ? "startup_retains_peer" : + consumeToken ? "startup_consumed_removal" : "startup_deleting_graph"; + GraphManager manager = this.dropManager(name); + HugeGraph owner = null; + HugeGraph keeper = null; + ExecutorService deletion = Executors.newSingleThreadExecutor( + runnable -> new Thread(runnable, "grpc-startup-delete-proof")); + CountDownLatch metadataEntered = new CountDownLatch(1); + CountDownLatch metadataRelease = new CountDownLatch(1); + Future dropped = null; + try { + if (keepOther) { + keeper = manager.createGraphLocal(name + "_peer", sharedMemoryConfig(name + "_peer")); + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", keeper); + } + owner = manager.createGraphLocal(name, sharedMemoryConfig(name)); + HugeGraph graph = owner; + // The existing Memory feature fixture exercises real cache RPC registration, not shared DML. + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", graph); + MetaManager meta = Mockito.mock(MetaManager.class); + Mockito.doAnswer(invocation -> { + metadataEntered.countDown(); + E.checkState(metadataRelease.await(10, TimeUnit.SECONDS), "metadata proof barrier timed out"); + return null; + }).when(meta).removeGraphConfig("DEFAULT", name); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + GraphSpace space = new GraphSpace("DEFAULT"); + space.graphNumberUsed(5); + spaces.put("DEFAULT", space); + dropped = deletion.submit(() -> new org.apache.hugegraph.api.profile.GraphsAPI().drop( + manager, "DEFAULT", name, "I'm sure to drop the graph")); + Assert.assertTrue(metadataEntered.await(5, TimeUnit.SECONDS)); + java.util.Set removing = Whitebox.getInternalState(manager, "removingGraphs"); + Assert.assertTrue(removing.contains(graph.spaceGraphName())); + if (consumeToken) { + Mockito.when(meta.extractGraphsFromResponse(Mockito.any())).thenReturn( + java.util.Collections.singletonList(graph.spaceGraphName())); + Whitebox.invoke(GraphManager.class, "graphRemoveHandler", manager, new Object()); + Assert.assertFalse(removing.contains(graph.spaceGraphName())); + } + Assert.assertFalse(graph.closed()); + Assert.assertSame(graph, manager.graph(graph.spaceGraphName())); + RpcServer server = Whitebox.getInternalState(manager, "rpcServer"); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + assertRpcResources(server, client, 0); + // Run actual manager startup while the same owner's placement metadata removal is blocked. + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + Assert.assertFalse(dropped.isDone()); + Assert.assertEquals("RPC startup exported a removing graph during metadata deletion", + keepOther ? 2 : 0, server.config().configs().size()); + if (keepOther) { + Assert.assertFalse(keeper.closed()); + Assert.assertSame(keeper, manager.graph(keeper.spaceGraphName())); + } + } finally { + metadataRelease.countDown(); + try { + if (dropped != null) { + dropped.get(20, TimeUnit.SECONDS); + } + } finally { + deletion.shutdown(); + Assert.assertTrue(deletion.awaitTermination(20, TimeUnit.SECONDS)); + manager.close(); + if (owner != null) { + owner.close(); + HugeFactory.remove(owner); + } + if (keeper != null) { + HugeFactory.remove(keeper); + } + } + } + } + + @Test + public void testNonClearingPeerCloseFailureKeepsRpcAtInitialStartup() throws Exception { + String name = "peer_close_open_rpc"; + GraphManager manager = this.dropManager(name); + HugeGraph graph = null; + ExecutorService worker = Executors.newSingleThreadExecutor(); + CountDownLatch opened = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + try { + graph = manager.createGraphLocal(name, sharedMemoryConfig(name)); + HugeGraph owner = graph; + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", owner); + RpcServer server = Whitebox.getInternalState(manager, "rpcServer"); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + assertRpcResources(server, client, 0); + MetaManager meta = Mockito.mock(MetaManager.class); + Mockito.when(meta.extractGraphsFromResponse("peer-remove")).thenReturn(List.of(owner.spaceGraphName())); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + Future transaction = worker.submit(() -> { + try { + owner.tx().open(); + opened.countDown(); + E.checkState(release.await(15, TimeUnit.SECONDS), "Foreign transaction release timed out"); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(failure); + } finally { + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", owner); + } + }); + Assert.assertTrue(opened.await(5, TimeUnit.SECONDS)); + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphRemoveHandler", manager, "peer-remove"); + Assert.assertFalse(owner.closed()); + Assert.assertSame(owner, manager.graph(owner.spaceGraphName())); + assertRpcResources(server, client, 0); + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + assertRpcResources(server, client, 2); + release.countDown(); + transaction.get(5, TimeUnit.SECONDS); + String threadName = Thread.currentThread().getName(); + try { + Thread.currentThread().setName("grpc-peer-close-retry"); + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphRemoveHandler", manager, "peer-remove"); + } finally { + Thread.currentThread().setName(threadName); + } + Assert.assertTrue(owner.closed()); + Assert.assertFalse(manager.graphs().contains(owner.spaceGraphName())); + assertRpcResources(server, client, 0); + Mockito.verify(meta, Mockito.never()).removeGraphConfig(Mockito.anyString(), Mockito.anyString()); + } finally { + release.countDown(); + worker.shutdown(); + Assert.assertTrue(worker.awaitTermination(5, TimeUnit.SECONDS)); + manager.close(); + if (graph != null) { + graph.close(); + HugeFactory.remove(graph); + } + } + } + + @Test + public void testNonPdOrdinaryClosedOwnerCannotCertifyBackendClear() throws Exception { + GraphManager manager = this.dropManager("non_pd_uncleared_close"); + HugeGraph owner = null; + HugeGraph keeper = null; + try { + String config = "gremlin.graph=" + HugeFactory.class.getName() + "\nbackend=memory\nstore="; + keeper = manager.createGraphLocal("uncleared_keeper", config + "uncleared_keeper\n"); + owner = manager.createGraphLocal("uncleared_owner", config + "uncleared_owner\n"); + HugeGraph graph = owner; + owner.close(); + Assert.assertThrows(IllegalStateException.class, () -> + new org.apache.hugegraph.api.profile.GraphsAPI().drop( + manager, "DEFAULT", "uncleared_owner", "I'm sure to drop the graph")); + Map factories = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + Assert.assertSame(owner, factories.get(owner.spaceGraphName())); + Assert.assertSame(owner, manager.graph(owner.spaceGraphName())); + Assert.assertTrue(graph.configuration().file().isFile()); + } finally { + manager.close(); + if (owner != null) { + owner.close(); + HugeFactory.remove(owner); + } + if (keeper != null) { + HugeFactory.remove(keeper); + } + } + } + + @Test + public void testRpcStartupSkipsOrdinaryClosedOwner() throws Exception { + GraphManager manager = this.dropManager("startup_closed_owner"); + HugeGraph owner = null; + try { + owner = manager.createGraphLocal("startup_closed", sharedMemoryConfig("startup_closed")); + owner.close(); + RuntimeException startup = null; + try { + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + } catch (RuntimeException failure) { + startup = failure; + } + Assert.assertNull("RPC startup must skip a closed owner: " + startup, startup); + RpcServer server = Whitebox.getInternalState(manager, "rpcServer"); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + assertRpcResources(server, client, 0); + } finally { + manager.close(); + if (owner != null) { + owner.close(); + HugeFactory.remove(owner); + } + } + } + + @Test + public void testNonPdFailedClearPreservesExistingRpc() throws Exception { + GraphManager manager = this.dropManager("non_pd_clear_failure"); + HugeGraph owner = null; + HugeGraph keeper = null; + BackendStoreProvider provider = null; + try { + owner = manager.createGraphLocal("non_pd_clear", sharedMemoryConfig("non_pd_clear")); + keeper = manager.createGraphLocal("non_pd_clear_peer", sharedMemoryConfig("non_pd_clear_peer")); + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + RpcServer server = Whitebox.getInternalState(manager, "rpcServer"); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + assertRpcResources(server, client, 4); + provider = Mockito.spy(owner.storeProvider()); + Whitebox.setInternalState(owner, "storeProvider", provider); + IllegalStateException failure = new IllegalStateException("clear rejected before destructive action"); + Mockito.doThrow(failure).when(provider).clear(); + Assert.assertThrows(IllegalStateException.class, () -> + new org.apache.hugegraph.api.profile.GraphsAPI().drop( + manager, "DEFAULT", "non_pd_clear", "I'm sure to drop the graph"), + caught -> Assert.assertSame(failure, caught)); + Assert.assertFalse(owner.closed()); + Assert.assertSame(owner, manager.graph(owner.spaceGraphName())); + Assert.assertTrue(owner.configuration().file().isFile()); + java.util.Set pending = Whitebox.getInternalState(manager, "rpcRemovingOwners"); + Assert.assertFalse(pending.contains(owner)); + assertRpcResources(server, client, 4); + } finally { + if (provider != null) { + Mockito.doCallRealMethod().when(provider).clear(); + } + manager.close(); + if (owner != null) { + HugeFactory.remove(owner); + } + if (keeper != null) { + HugeFactory.remove(keeper); + } + } + } + + @Test + public void testNonPdClearFailureDoesNotUnregisterRpcDuringStartup() throws Exception { + String name = "non_pd_clear_startup"; + GraphManager manager = this.dropManager(name); + HugeGraph owner = null; + HugeGraph keeper = null; + BackendStoreProvider provider = null; + CountDownLatch clearEntered = new CountDownLatch(1); + CountDownLatch clearRelease = new CountDownLatch(1); + ExecutorService deletion = Executors.newSingleThreadExecutor(); + Future dropped = null; + try { + owner = manager.createGraphLocal(name, sharedMemoryConfig(name)); + keeper = manager.createGraphLocal(name + "_peer", sharedMemoryConfig(name + "_peer")); + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", owner); + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", keeper); + provider = Mockito.spy(owner.storeProvider()); + Whitebox.setInternalState(owner, "storeProvider", provider); + IllegalStateException failure = new IllegalStateException("clear fails before destructive action"); + Mockito.doAnswer(invocation -> { + clearEntered.countDown(); + E.checkState(clearRelease.await(10, TimeUnit.SECONDS), "local clear proof barrier timed out"); + throw failure; + }).when(provider).clear(); + dropped = deletion.submit(() -> new org.apache.hugegraph.api.profile.GraphsAPI().drop( + manager, "DEFAULT", name, "I'm sure to drop the graph")); + Assert.assertTrue(clearEntered.await(5, TimeUnit.SECONDS)); + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + clearRelease.countDown(); + Future failedDrop = dropped; + Assert.assertThrows(ExecutionException.class, () -> failedDrop.get(20, TimeUnit.SECONDS), + caught -> Assert.assertSame(failure, caught.getCause())); + dropped = null; + Assert.assertFalse(owner.closed()); + Assert.assertSame(owner, manager.graph(owner.spaceGraphName())); + Assert.assertTrue(owner.configuration().file().isFile()); + RpcServer server = Whitebox.getInternalState(manager, "rpcServer"); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + assertRpcResources(server, client, 4); + } finally { + clearRelease.countDown(); + try { + if (dropped != null) { + try { + dropped.get(20, TimeUnit.SECONDS); + } catch (ExecutionException expected) { + // The controlled clear fails before destructive work. + } + } + } finally { + if (provider != null) { + Mockito.doCallRealMethod().when(provider).clear(); + } + deletion.shutdown(); + Assert.assertTrue(deletion.awaitTermination(20, TimeUnit.SECONDS)); + manager.close(); + if (owner != null) { + HugeFactory.remove(owner); + } + if (keeper != null) { + HugeFactory.remove(keeper); + } + } + } + } + + @Test + public void testMetadataFailureRestoresRpcBeforeClearingRemovalMarker() throws Exception { + this.checkMetadataRpcRestoration(false); + } + + @Test + public void testFailedRpcRestorationRetainsRemovalOwner() throws Exception { + this.checkMetadataRpcRestoration(true); + } + + private void checkMetadataRpcRestoration(boolean rejectRestore) throws Exception { + String name = rejectRestore ? "metadata_restore_failure" : "metadata_restore_success"; + GraphManager manager = this.dropManager(name); + HugeGraph owner = null; + RpcServer server = null; + try { + owner = manager.createGraphLocal(name, sharedMemoryConfig(name)); + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + RpcServer original = Whitebox.getInternalState(manager, "rpcServer"); + server = Mockito.spy(original); + Whitebox.setInternalState(manager, "rpcServer", server); + IllegalStateException metadata = new IllegalStateException("placement removal failed"); + IllegalStateException restoration = new IllegalStateException("RPC restoration failed"); + if (rejectRestore) { + Mockito.doThrow(restoration).when(server).exportAll(); + } + MetaManager meta = Mockito.mock(MetaManager.class); + Mockito.doThrow(metadata).when(meta).removeGraphConfig("DEFAULT", name); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + GraphSpace space = new GraphSpace("DEFAULT"); + space.graphNumberUsed(5); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + spaces.put("DEFAULT", space); + Assert.assertThrows(HugeException.class, () -> + new org.apache.hugegraph.api.profile.GraphsAPI().drop( + manager, "DEFAULT", name, "I'm sure to drop the graph"), + caught -> Assert.assertSame(metadata, caught.getCause())); + java.util.Set pending = Whitebox.getInternalState(manager, "rpcRemovingOwners"); + Assert.assertEquals(rejectRestore, pending.contains(owner)); + Assert.assertFalse(owner.closed()); + Assert.assertSame(owner, manager.graph(owner.spaceGraphName())); + Assert.assertTrue(owner.configuration().file().isFile()); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + assertRpcResources(server, client, rejectRestore ? 0 : 2); + if (rejectRestore) { + Assert.assertEquals(1, metadata.getSuppressed().length); + Assert.assertSame(restoration, metadata.getSuppressed()[0]); + } + } finally { + if (server != null) { + Mockito.doCallRealMethod().when(server).exportAll(); + } + manager.close(); + if (owner != null) { + HugeFactory.remove(owner); + } + } + } + + @Test + public void testStartupDoesNotExportPendingOwnerAfterUnexportFailure() throws Exception { + String name = "startup_unexport_failure"; + GraphManager manager = this.dropManager(name); + HugeGraph owner = null; + RpcServer server = null; + com.alipay.sofa.rpc.config.ProviderConfig retained = null; + try { + owner = manager.createGraphLocal(name, sharedMemoryConfig(name)); + RpcServer original = Whitebox.getInternalState(manager, "rpcServer"); + server = Mockito.spy(original); + Whitebox.setInternalState(manager, "rpcServer", server); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + owner.registerRpcServices(server.config(), client.config()); + assertRpcResources(server, client, 2); + Map> configs = server.config().configs(); + Map.Entry> entry = + configs.entrySet().iterator().next(); + retained = Mockito.spy(entry.getValue()); + configs.put(entry.getKey(), retained); + IllegalStateException failure = new IllegalStateException("provider unexport rejected"); + Mockito.doThrow(failure).when(retained).unExport(); + Whitebox.setInternalState(manager, "PDExist", true); + MetaManager meta = Mockito.mock(MetaManager.class); + Whitebox.setInternalState(manager, "metaManager", meta); + Assert.assertThrows(IllegalStateException.class, () -> + new org.apache.hugegraph.api.profile.GraphsAPI().drop( + manager, "DEFAULT", name, "I'm sure to drop the graph")); + Assert.assertEquals(1, server.config().configs().size()); + Mockito.verify(meta, Mockito.never()).removeGraphConfig(Mockito.any(), Mockito.any()); + Assert.assertThrows(IllegalStateException.class, () -> + Whitebox.invoke(GraphManager.class, "startRpcServer", manager)); + Mockito.verify(server, Mockito.never()).exportAll(); + Assert.assertFalse(owner.closed()); + Assert.assertSame(owner, manager.graph(owner.spaceGraphName())); + java.util.Set pending = Whitebox.getInternalState(manager, "rpcRemovingOwners"); + Assert.assertTrue(pending.contains(owner)); + } finally { + if (retained != null) { + Mockito.doCallRealMethod().when(retained).unExport(); + } + manager.close(); + if (owner != null) { + owner.close(); + HugeFactory.remove(owner); + } + } + } + + @Test + public void testNonPdHealthyPublicDeleteCompletesCleanup() throws Exception { + this.checkNonPdPublicDeleteRetry(false, false); + } + + @Test + public void testNonPdPublicDeleteRetriesFailedProviderCleanup() throws Exception { + this.checkNonPdPublicDeleteRetry(true, false); + } + + @Test + public void testNonPdPublicDeleteRetriesFailedRpcUnregistration() throws Exception { + this.checkNonPdPublicDeleteRetry(true, true); + } + + @Test + public void testNonPdPublicDeletePreservesFatalCleanupIdentity() throws Exception { + this.checkNonPdPublicDeleteRetry(true, false, true); + } + + private void checkNonPdPublicDeleteRetry(boolean failCleanup, boolean rpcCleanup) throws Exception { + this.checkNonPdPublicDeleteRetry(failCleanup, rpcCleanup, false); + } + + private void checkNonPdPublicDeleteRetry(boolean failCleanup, boolean rpcCleanup, boolean fatal) throws Exception { + String name = !failCleanup ? "non_pd_healthy" : rpcCleanup ? "non_pd_rpc_retry" : + fatal ? "non_pd_fatal_retry" : "non_pd_provider_retry"; + GraphManager manager = this.dropManager(name); + HugeGraph owner = null; + HugeGraph keeper = null; + AtomicBoolean reject = new AtomicBoolean(failCleanup); + AtomicInteger attempts = new AtomicInteger(); + try { + String config = "gremlin.graph=" + HugeFactory.class.getName() + "\nbackend=memory\nstore="; + keeper = manager.createGraphLocal(name + "_keeper", config + name + "_keeper\n"); + owner = manager.createGraphLocal(name, config + name + "\n"); + HugeGraph graph = owner; + Map factories = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + Map managed = Whitebox.getInternalState(manager, "graphs"); + List retained = Whitebox.getInternalState(owner, "closeResources"); + List rpc = Whitebox.getInternalState(owner, "rpcServiceCleanup"); + File file = owner.configuration().file(); + Assert.assertSame(owner, factories.get(owner.spaceGraphName())); + Assert.assertSame(owner, managed.get(owner.spaceGraphName())); + Assert.assertTrue(file.isFile()); + Throwable controlled = fatal ? new AssertionError("fatal native cleanup boundary") : + new IllegalStateException("persistent native cleanup boundary"); + BackendStoreProvider provider = Mockito.spy(owner.storeProvider()); + Whitebox.setInternalState(owner, "storeProvider", provider); + AtomicInteger clears = new AtomicInteger(); + Mockito.doAnswer(invocation -> { + clears.incrementAndGet(); + invocation.callRealMethod(); + return null; + }).when(provider).clear(); + if (rpcCleanup) { + // Only the RPC cleanup callback is controlled; actual graph close/unregister queues run. + rpc.add(() -> { + attempts.incrementAndGet(); + if (reject.get()) { + throw (RuntimeException) controlled; + } + }); + } else { + Mockito.doAnswer(invocation -> { + invocation.callRealMethod(); + attempts.incrementAndGet(); + if (reject.get()) { + throw controlled; + } + return null; + }).when(provider).close(); + } + org.apache.hugegraph.api.profile.GraphsAPI api = new org.apache.hugegraph.api.profile.GraphsAPI(); + Throwable first = null; + try { + api.drop(manager, "DEFAULT", name, "I'm sure to drop the graph"); + } catch (RuntimeException | Error failure) { + first = failure; + } + boolean configRetained = file.isFile(); + boolean factoryRetained = factories.get(owner.spaceGraphName()) == owner; + boolean managerRetained = managed.get(owner.spaceGraphName()) == owner; + boolean completed = Whitebox.getInternalState(owner, "closeCompleted"); + int pending = retained.size(); + int rpcPending = rpc.size(); + int firstAttempts = attempts.get(); + reject.set(false); + Throwable retry = null; + if (failCleanup) { + try { + api.drop(manager, "DEFAULT", name, "I'm sure to drop the graph"); + } catch (RuntimeException | Error failure) { + retry = failure; + } + } + String facts = String.format("native non-PD DELETE: rpc=%s first=%s retry=%s closed=%s " + + "closeCompleted=%s config=%s factory=%s manager=%s retained=%s rpcPending=%s " + + "attemptsBeforeRetry=%s attemptsAfterRetry=%s", rpcCleanup, first, retry, graph.closed(), + completed, configRetained, factoryRetained, managerRetained, pending, rpcPending, + firstAttempts, attempts.get()); + if (failCleanup) { + Assert.assertTrue(facts, first != null); + if (fatal) { + Assert.assertSame(controlled, first); + } + Assert.assertTrue(facts, configRetained); + Assert.assertTrue(facts, factoryRetained); + Assert.assertTrue(facts, managerRetained); + Assert.assertFalse(facts, completed); + Assert.assertTrue(facts, pending > 0); + Assert.assertNull(facts, retry); + Assert.assertTrue(facts, attempts.get() > firstAttempts); + } else { + Assert.assertNull(facts, first); + Assert.assertTrue(facts, completed); + Assert.assertEquals(facts, 0, pending); + } + Assert.assertEquals("A successful drop clear must not repeat during cleanup retry", 1, clears.get()); + Assert.assertTrue(Whitebox.getInternalState(owner, "closeCompleted")); + Assert.assertEquals(0, retained.size()); + Assert.assertFalse(file.exists()); + Assert.assertFalse(factories.containsKey(owner.spaceGraphName())); + Assert.assertFalse(managed.containsKey(owner.spaceGraphName())); + Assert.assertSame(keeper, manager.graph(keeper.spaceGraphName())); + } finally { + reject.set(false); + if (owner != null) { + owner.close(); + HugeFactory.remove(owner); + } + manager.close(); + if (keeper != null) { + HugeFactory.remove(keeper); + } + } + } + + private GraphManager dropManager(String name) throws Exception { + OptionSpace.register("core", CoreOptions.instance()); + OptionSpace.register("rpc", RpcOptions.instance()); + OptionSpace.register("server", ServerOptions.instance()); + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(ServerOptions.GRAPHS.name(), this.temporary.newFolder(name).getPath()); + properties.setProperty(ServerOptions.USE_PD.name(), false); + properties.setProperty(ServerOptions.GRAPH_LOAD_FROM_LOCAL_CONFIG.name(), false); + properties.setProperty(ServerOptions.ENABLE_DYNAMIC_CREATE_DROP.name(), true); + properties.setProperty(RpcOptions.RPC_SERVER_HOST.name(), "127.0.0.1"); + properties.setProperty(RpcOptions.RPC_SERVER_PORT.name(), 0); + properties.setProperty(RpcOptions.RPC_REMOTE_URL.name(), "127.0.0.1:1"); + return new GraphManager(new HugeConfig(properties), new EventHub(name)); + } + + private void checkPostCloseDrop(boolean failCleanup) throws Exception { + String name = failCleanup ? "closed_rpc_retry" : "closed_failure"; + GraphManager manager = this.dropManager(name); + HugeGraph owner = null; + try { + owner = manager.createGraphLocal(name, sharedMemoryConfig(name)); + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + HugeGraph delegate = owner; + Map factoryOwners = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + factoryOwners.put(owner.spaceGraphName(), owner); + IllegalStateException failure = new IllegalStateException("closed owner cleanup failed"); + AtomicInteger clears = new AtomicInteger(); + AtomicInteger rpcCleanup = new AtomicInteger(); + BackendStoreProvider provider = Mockito.spy(owner.storeProvider()); + if (!failCleanup) { + Whitebox.setInternalState(owner, "storeProvider", provider); + Mockito.doAnswer(invocation -> { + invocation.callRealMethod(); + throw failure; + }).when(provider).close(); + } + HugeGraph graph = (HugeGraph) Proxy.newProxyInstance(HugeGraph.class.getClassLoader(), + new Class[]{HugeGraph.class}, (proxy, method, args) -> { + if ("clearBackend".equals(method.getName())) { + clears.incrementAndGet(); + } + if ("unregisterRpcServices".equals(method.getName()) && + rpcCleanup.incrementAndGet() == 2 && failCleanup) { + throw failure; + } + try { + return method.invoke(delegate, args); + } catch (InvocationTargetException error) { + throw error.getCause(); + } + }); + Map graphs = Whitebox.getInternalState(manager, "graphs"); + graphs.put(graph.spaceGraphName(), graph); + DefaultGraphManager bindings = new DefaultGraphManager(new Settings()); + bindings.putGraph(graph.spaceGraphName(), graph); + EventHub hub = Whitebox.getInternalState(manager, "eventHub"); + CountDownLatch dropped = new CountDownLatch(1); + hub.listen(Events.GRAPH_DROP, event -> { + try { + bindings.removeGraph(graph.spaceGraphName()); + } catch (Exception failureToUnbind) { + throw new IllegalStateException(failureToUnbind); + } + dropped.countDown(); + return true; + }); + MetaManager meta = Mockito.mock(MetaManager.class); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + GraphSpace space = new GraphSpace("DEFAULT"); + space.graphNumberUsed(5); + spaces.put("DEFAULT", space); + if (failCleanup) { + Assert.assertThrows(IllegalStateException.class, () -> manager.dropGraph( + "DEFAULT", name, true), caught -> Assert.assertSame(failure, caught)); + } else { + Assert.assertThrows(HugeException.class, () -> manager.dropGraph( + "DEFAULT", name, true), caught -> Assert.assertSame(failure, caught.getCause())); + Mockito.doCallRealMethod().when(provider).close(); + } + Assert.assertTrue(owner.closed()); + Assert.assertSame(graph, manager.graph(graph.spaceGraphName())); + Assert.assertSame(graph, bindings.getGraph(graph.spaceGraphName())); + Map retainedFactory = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + Assert.assertSame(owner, retainedFactory.get(graph.spaceGraphName())); + Assert.assertEquals(5, space.graphNumberUsed()); + Assert.assertEquals(1L, dropped.getCount()); + manager.dropGraph("DEFAULT", name, true); + Assert.assertTrue(dropped.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(owner.closed()); + Assert.assertEquals(1, clears.get()); + Mockito.verify(meta).removeGraphConfig("DEFAULT", name); + Mockito.verify(meta).notifyGraphRemove("DEFAULT", name); + Mockito.verify(meta).updateGraphSpaceConfig("DEFAULT", space); + Assert.assertEquals(4, space.graphNumberUsed()); + Assert.assertNull(bindings.getGraph(graph.spaceGraphName())); + Assert.assertFalse(manager.graphs().contains(graph.spaceGraphName())); + Map factory = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + Assert.assertFalse(factory.containsKey(graph.spaceGraphName())); + RpcServer server = Whitebox.getInternalState(manager, "rpcServer"); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + assertRpcResources(server, client, 0); + } finally { + manager.close(); + if (owner != null) { + HugeFactory.remove(owner); + } + } + } + + @Test + public void testQuotaUpdateFailureWithClearRetriesClosedOwner() throws Exception { + this.checkLegacyDrop(false, true, true); + } + + @Test + public void testRaftCloseFailuresKeepOwnerAndPreserveSuppressed() throws Exception { + this.checkRaftCloseFailures(false); + } + + @Test + public void testRaftCloseFailuresDoNotSuppressTheSameException() throws Exception { + this.checkRaftCloseFailures(true); + } + + @Test + public void testRaftClosePrioritizesFatalError() throws Exception { + this.checkRaftCloseFailures(false, true); + } + + private void checkRaftCloseFailures(boolean sameFailure) throws Exception { + this.checkRaftCloseFailures(sameFailure, false); + } + + private void checkRaftCloseFailures(boolean sameFailure, boolean fatalNode) throws Exception { + String name = fatalNode ? "raft_fatal_node" : sameFailure ? "raft_same_failure" : "raft_close_failures"; + GraphManager manager = this.dropManager(name); + HugeGraph graph = null; + com.alipay.sofa.jraft.rpc.RpcServer nativeRpc = null; + RaftContext context = null; + String threadName = Thread.currentThread().getName(); + AtomicBoolean failProvider = new AtomicBoolean(true); + AtomicBoolean failNode = new AtomicBoolean(true); + AtomicBoolean failRpc = new AtomicBoolean(true); + try { + graph = manager.createGraphLocal(name, sharedMemoryConfig(name)); + Map factory = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + // The shared-memory GraphFactory fixture bypasses HugeFactory.open(). + factory.put(graph.spaceGraphName(), graph); + nativeRpc = startNativeRaftRpc(); + int port = nativeRpc.boundPort(); + Assert.assertTrue(nativeRpcListening(port)); + graph.configuration().setProperty("raft.group_peers", "127.0.0.1:" + port); + BackendStoreProvider provider = Mockito.spy(graph.storeProvider()); + IllegalStateException primary = new IllegalStateException("provider failed before release"); + Throwable nodeFailure = fatalNode ? new AssertionError("fatal node close") : sameFailure ? primary : + new IllegalStateException("node failed before release"); + IllegalStateException rpcFailure = sameFailure ? primary : + new IllegalStateException("RPC failed before release"); + AtomicInteger providerCalls = new AtomicInteger(); + AtomicInteger nodeCalls = new AtomicInteger(); + AtomicInteger rpcCalls = new AtomicInteger(); + Mockito.doAnswer(call -> { + providerCalls.incrementAndGet(); + if (failProvider.get()) { + throw primary; + } + return call.callRealMethod(); + }).when(provider).close(); + HugeGraphParams params = Whitebox.getInternalState(graph, "params"); + RaftBackendStoreProvider raft = new RaftBackendStoreProvider(params, provider); + context = Whitebox.getInternalState(raft, "context"); + RaftNode node = Mockito.mock(RaftNode.class); + Mockito.doAnswer(call -> { + nodeCalls.incrementAndGet(); + if (failNode.get()) { + throw nodeFailure; + } + return null; + }).when(node).shutdown(); + com.alipay.sofa.jraft.rpc.RpcServer rpc = Mockito.spy(nativeRpc); + Mockito.doAnswer(call -> { + rpcCalls.incrementAndGet(); + if (failRpc.get()) { + throw rpcFailure; + } + return call.callRealMethod(); + }).when(rpc).shutdown(); + Whitebox.setInternalState(context, "raftNode", node); + Whitebox.setInternalState(context, "raftRpcServer", rpc); + Whitebox.setInternalState(context, "endpoint", new PeerId("127.0.0.1", port)); + Whitebox.setInternalState(graph, "storeProvider", raft); + MetaManager meta = Mockito.mock(MetaManager.class); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + Thread.currentThread().setName("grpc-raft-close-failures"); + if (fatalNode) { + AssertionError fatal = Assert.assertThrows(AssertionError.class, + () -> manager.dropGraph("DEFAULT", name, false)); + Assert.assertSame(nodeFailure, fatal); + Assert.assertEquals(2, fatal.getSuppressed().length); + Assert.assertSame(rpcFailure, fatal.getSuppressed()[0]); + Assert.assertSame(primary, fatal.getSuppressed()[1]); + } else { + HugeException failure = Assert.assertThrows(HugeException.class, + () -> manager.dropGraph("DEFAULT", name, false)); + Assert.assertSame(primary, failure.getCause()); + Assert.assertEquals(sameFailure ? 0 : 1, primary.getSuppressed().length); + if (!sameFailure) { + Assert.assertSame(nodeFailure, primary.getSuppressed()[0]); + Assert.assertSame(rpcFailure, nodeFailure.getSuppressed()[0]); + } + } + Assert.assertTrue(graph.closed()); + Assert.assertSame(graph, manager.graph(graph.spaceGraphName())); + Assert.assertSame(graph, factory.get(graph.spaceGraphName())); + Assert.assertTrue(nativeRpcListening(port)); + Assert.assertFalse(context.snapshotExecutor().isShutdown()); + Assert.assertFalse(context.backendExecutor().isShutdown()); + failProvider.set(false); + failNode.set(false); + // A remaining listener failure must retain the logically closed owner too. + // Retain the original non-clearing removal contract on every retry. + HugeException failure = Assert.assertThrows(HugeException.class, + () -> manager.dropGraph("DEFAULT", name, false)); + Assert.assertSame(rpcFailure, failure.getCause()); + assertRaftExecutorsTerminated(context); + Assert.assertSame(graph, manager.graph(graph.spaceGraphName())); + Assert.assertSame(graph, factory.get(graph.spaceGraphName())); + Assert.assertTrue(nativeRpcListening(port)); + failRpc.set(false); + manager.dropGraph("DEFAULT", name, false); + Assert.assertFalse(manager.graphs().contains(graph.spaceGraphName())); + Assert.assertFalse(factory.containsKey(graph.spaceGraphName())); + Assert.assertEquals(2, providerCalls.get()); + Assert.assertEquals(2, nodeCalls.get()); + Assert.assertEquals(3, rpcCalls.get()); + Mockito.verify(meta, Mockito.never()).removeGraphConfig("DEFAULT", name); + Mockito.verify(meta, Mockito.never()).notifyGraphRemove("DEFAULT", name); + Mockito.verify(meta, Mockito.never()).updateGraphSpaceConfig(Mockito.eq("DEFAULT"), + Mockito.any(GraphSpace.class)); + awaitNativeRpcClosed(port); + } finally { + failProvider.set(false); + failNode.set(false); + failRpc.set(false); + Thread.currentThread().setName(threadName); + if (nativeRpc != null && nativeRpcListening(nativeRpc.boundPort())) { + nativeRpc.shutdown(); + } + stopRaftFixtureExecutors(context); + manager.close(); + if (graph != null) { + HugeFactory.remove(graph); + } + } + } + + @Test + public void testProviderFailureClosesRealRaftNodeBeforeOwnerRemoval() throws Exception { + OptionSpace.register("rocksdb", RocksDBOptions.instance()); + BackendProviderFactory.register("rocksdb", RocksDBStoreProvider.class.getName()); + String name = "raft_real_close"; + GraphManager manager = this.dropManager(name); + HugeGraph graph = null; + com.alipay.sofa.jraft.rpc.RpcServer rpc = null; + RaftContext context = null; + RaftNode raftNode = null; + String threadName = Thread.currentThread().getName(); + try { + String data = this.temporary.newFolder("raft_data").getPath(); + String wal = this.temporary.newFolder("raft_wal").getPath(); + graph = manager.createGraphLocal(name, "gremlin.graph=" + HugeFactory.class.getName() + + "\nbackend=rocksdb\nserializer=binary\nstore=" + name + + "\nrocksdb.data_path=" + data + "\nrocksdb.wal_path=" + wal + "\n"); + rpc = startNativeRaftRpc(); + int port = rpc.boundPort(); + Assert.assertTrue(nativeRpcListening(port)); + HugeConfig config = graph.configuration(); + config.setProperty("raft.group_peers", "127.0.0.1:" + port); + config.setProperty(CoreOptions.RAFT_PATH.name(), this.temporary.newFolder("raft_log").getPath()); + config.setProperty(CoreOptions.RAFT_ELECTION_TIMEOUT.name(), 300); + config.setProperty(CoreOptions.RAFT_SNAPSHOT_INTERVAL.name(), 0); + config.setProperty(CoreOptions.RAFT_RPC_THREADS.name(), 2); + config.setProperty(CoreOptions.RAFT_QUEUE_SIZE.name(), 2048); + config.setProperty(CoreOptions.RAFT_SAFE_READ.name(), true); + BackendStoreProvider provider = Mockito.spy(graph.storeProvider()); + AtomicInteger providerCalls = new AtomicInteger(); + IllegalStateException primary = new IllegalStateException("provider failed before real node release"); + Mockito.doAnswer(call -> { + providerCalls.incrementAndGet(); + throw primary; + }).when(provider).close(); + HugeGraphParams params = Whitebox.getInternalState(graph, "params"); + RaftBackendStoreProvider raft = new RaftBackendStoreProvider(params, provider); + raft.loadSchemaStore(config); + raft.loadGraphStore(config); + raft.loadSystemStore(config); + context = Whitebox.getInternalState(raft, "context"); + Whitebox.setInternalState(context, "raftRpcServer", rpc); + Whitebox.setInternalState(context, "endpoint", new PeerId("127.0.0.1", port)); + NodeManager.getInstance().addAddress(new Endpoint("127.0.0.1", port)); + raftNode = new RaftNode(context); + Whitebox.setInternalState(context, "raftNode", raftNode); + Node node = Whitebox.invoke(RaftNode.class, "node", raftNode); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while (node.getNodeState() != State.STATE_LEADER && System.nanoTime() < deadline) { + Thread.sleep(10); + } + Assert.assertEquals(State.STATE_LEADER, node.getNodeState()); + Whitebox.setInternalState(graph, "storeProvider", raft); + Whitebox.setInternalState(manager, "metaManager", Mockito.mock(MetaManager.class)); + Whitebox.setInternalState(manager, "PDExist", true); + Thread.currentThread().setName("grpc-real-raft-close"); + HugeException failure = Assert.assertThrows(HugeException.class, + () -> manager.dropGraph("DEFAULT", name, false)); + Assert.assertSame(primary, failure.getCause()); + Assert.assertTrue(graph.closed()); + Assert.assertSame(graph, manager.graph(graph.spaceGraphName())); + Map factory = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + Assert.assertSame(graph, factory.get(graph.spaceGraphName())); + Assert.assertEquals(State.STATE_SHUTDOWN, node.getNodeState()); + assertRaftExecutorsTerminated(context); + awaitNativeRpcClosed(port); + try (ServerSocket reused = new ServerSocket(port)) { + Assert.assertEquals(port, reused.getLocalPort()); + } + Mockito.doAnswer(call -> { + providerCalls.incrementAndGet(); + return call.callRealMethod(); + }).when(provider).close(); + manager.dropGraph("DEFAULT", name, false); + Assert.assertEquals(2, providerCalls.get()); + Assert.assertFalse(manager.graphs().contains(graph.spaceGraphName())); + Assert.assertFalse(factory.containsKey(graph.spaceGraphName())); + Assert.assertEquals(State.STATE_SHUTDOWN, node.getNodeState()); + } finally { + Thread.currentThread().setName(threadName); + if (raftNode != null) { + Node node = Whitebox.invoke(RaftNode.class, "node", raftNode); + if (node.getNodeState() != State.STATE_SHUTDOWN) { + raftNode.shutdown(); + } + } + if (rpc != null && nativeRpcListening(rpc.boundPort())) { + rpc.shutdown(); + } + stopRaftFixtureExecutors(context); + manager.close(); + if (graph != null) { + HugeFactory.remove(graph); + } + } + } + + @Test + public void testSharedRaftListenerSurvivesGraphDropAndOwnerRestart() throws Exception { + GraphManager first = this.raftOwnerManager("shared_owner_one", 0); + GraphManager second = null; + List graphs = new ArrayList<>(); + List contexts = new ArrayList<>(); + CliClientServiceImpl client = new CliClientServiceImpl(); + client.init(new CliOptions()); + String threadName = Thread.currentThread().getName(); + try { + com.alipay.remoting.rpc.RpcServer nativeFirst = Whitebox.invoke(GraphManager.class, + "remotingRpcServer", first); + int port = nativeFirst.port(); + Endpoint address = new Endpoint(nativeFirst.ip(), port); + Endpoint connect = new Endpoint("127.0.0.1", port); + HugeGraph a = this.attachRaftGraph(first, "shared_real_a", nativeFirst, contexts); + HugeGraph b = this.attachRaftGraph(first, "shared_real_b", nativeFirst, contexts); + graphs.add(a); + graphs.add(b); + startSharedRaftPair(contexts, nativeFirst); + Node nodeA = nativeNode(contexts.get(0)); + Node nodeB = nativeNode(contexts.get(1)); + assertRaftLeader(client, connect, b.spaceGraphName()); + Whitebox.setInternalState(first, "metaManager", Mockito.mock(MetaManager.class)); + Whitebox.setInternalState(first, "PDExist", true); + Thread.currentThread().setName("grpc-shared-real-owner"); + first.dropGraph("DEFAULT", a.name(), false); + Assert.assertEquals(State.STATE_SHUTDOWN, nodeA.getNodeState()); + assertRaftExecutorsTerminated(contexts.get(0)); + Assert.assertEquals(State.STATE_LEADER, nodeB.getNodeState()); + Assert.assertTrue(nativeRpcListening(port)); + Assert.assertTrue(NodeManager.getInstance().serverExists(address)); + assertRaftLeader(client, connect, b.spaceGraphName()); + client.shutdown(); + client = null; + RpcServer server = Mockito.spy((RpcServer) Whitebox.getInternalState(first, "rpcServer")); + IllegalStateException destroyFailure = new IllegalStateException("owner listener release failed"); + Mockito.doThrow(destroyFailure).when(server).destroy(); + Whitebox.setInternalState(first, "rpcServer", server); + Assert.assertThrows(IllegalStateException.class, first::close, + error -> Assert.assertSame(destroyFailure, error)); + Assert.assertEquals(State.STATE_SHUTDOWN, nodeB.getNodeState()); + Assert.assertTrue(nativeRpcListening(port)); + Assert.assertTrue(NodeManager.getInstance().serverExists(address)); + Assert.assertEquals(address, Whitebox.getInternalState(first, "raftRpcEndpoint")); + Mockito.doCallRealMethod().when(server).destroy(); + first.close(); + Mockito.verify(server, Mockito.times(2)).destroy(); + Assert.assertNull(Whitebox.getInternalState(first, "raftRpcEndpoint")); + Assert.assertEquals(State.STATE_SHUTDOWN, nodeB.getNodeState()); + awaitNativeRpcClosed(port); + Assert.assertFalse(NodeManager.getInstance().serverExists(address)); + List stopped = NodeManager.getInstance().getNodesByGroupId(b.spaceGraphName()); + Assert.assertTrue(stopped == null || stopped.isEmpty()); + try (ServerSocket reused = new ServerSocket(port)) { + Assert.assertEquals(port, reused.getLocalPort()); + } + second = this.raftOwnerManager("shared_owner_two", port); + com.alipay.remoting.rpc.RpcServer nativeSecond = Whitebox.invoke(GraphManager.class, + "remotingRpcServer", second); + Assert.assertNotSame(nativeFirst, nativeSecond); + Assert.assertEquals(port, nativeSecond.port()); + HugeGraph restarted = this.attachRaftGraph(second, b.name(), nativeSecond, contexts); + graphs.add(restarted); + contexts.get(2).initRaftNode(nativeSecond); + Node fresh = nativeNode(contexts.get(2)); + awaitRaftLeader(fresh); + Assert.assertNotSame(nodeB, fresh); + client = new CliClientServiceImpl(); + client.init(new CliOptions()); + assertRaftLeader(client, connect, restarted.spaceGraphName()); + client.shutdown(); + client = null; + second.close(); + Assert.assertEquals(State.STATE_SHUTDOWN, fresh.getNodeState()); + assertRaftExecutorsTerminated(contexts.get(2)); + awaitNativeRpcClosed(port); + Assert.assertFalse(NodeManager.getInstance().serverExists(address)); + } finally { + Thread.currentThread().setName(threadName); + if (client != null) { + client.shutdown(); + } + first.close(); + if (second != null) { + second.close(); + } + for (RaftContext context : contexts) { + stopRaftFixtureExecutors(context); + } + for (HugeGraph graph : graphs) { + HugeFactory.remove(graph); + } + } + } + + @Test + public void testInterruptedRaftJoinRetainsOwnerUntilRetry() throws Exception { + GraphManager manager = this.raftOwnerManager("interrupt_shared_owner", 0); + List graphs = new ArrayList<>(); + List contexts = new ArrayList<>(); + CliClientServiceImpl client = new CliClientServiceImpl(); + client.init(new CliOptions()); + String threadName = Thread.currentThread().getName(); + try { + com.alipay.remoting.rpc.RpcServer shared = Whitebox.invoke(GraphManager.class, + "remotingRpcServer", manager); + Endpoint address = new Endpoint(shared.ip(), shared.port()); + Endpoint connect = new Endpoint("127.0.0.1", shared.port()); + HugeGraph a = this.attachRaftGraph(manager, "interrupt_real_a", shared, contexts); + HugeGraph b = this.attachRaftGraph(manager, "interrupt_real_b", shared, contexts); + graphs.add(a); + graphs.add(b); + startSharedRaftPair(contexts, shared); + RaftContext context = contexts.get(0); + Node nodeA = nativeNode(context); + Node nodeB = nativeNode(contexts.get(1)); + RaftNode owner = Mockito.spy(context.node()); + AtomicBoolean interruptJoin = new AtomicBoolean(true); + AtomicReference joinFailure = new AtomicReference<>(); + // Inject an interrupted wait at Node.join(); retry delegates to the same native node. + // Setting the flag alone is insufficient: native component joins may consume it. + Node joining = (Node) Proxy.newProxyInstance(Node.class.getClassLoader(), + new Class[]{Node.class}, (proxy, method, args) -> { + if ("join".equals(method.getName()) && interruptJoin.get()) { + Thread.currentThread().interrupt(); + try { + new CountDownLatch(1).await(); + } catch (InterruptedException interrupted) { + joinFailure.set(interrupted); + throw interrupted; + } + } + try { + return method.invoke(nodeA, args); + } catch (InvocationTargetException failure) { + throw failure.getCause(); + } + }); + Whitebox.setInternalState(owner, "node", joining); + // Keep the baseline group-service join on the identical native join boundary too. + Object group = Whitebox.getInternalState(owner, "raftGroupService"); + Whitebox.setInternalState(group, "node", joining); + Whitebox.setInternalState(context, "raftNode", owner); + Whitebox.setInternalState(manager, "metaManager", Mockito.mock(MetaManager.class)); + Whitebox.setInternalState(manager, "PDExist", true); + Thread.currentThread().setName("grpc-interrupted-raft-join"); + HugeException failure = Assert.assertThrows(HugeException.class, + () -> manager.dropGraph("DEFAULT", a.name(), false)); + Assert.assertNotNull(joinFailure.get()); + Assert.assertSame(joinFailure.get(), HugeException.rootCause(failure)); + Assert.assertTrue(Thread.currentThread().isInterrupted()); + Assert.assertTrue(Thread.interrupted()); + Assert.assertTrue(a.closed()); + Assert.assertSame(a, manager.graph(a.spaceGraphName())); + Map factory = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + Assert.assertSame(a, factory.get(a.spaceGraphName())); + Assert.assertTrue(nativeRpcListening(shared.port())); + Assert.assertTrue(NodeManager.getInstance().serverExists(address)); + Assert.assertFalse(context.snapshotExecutor().isShutdown()); + Assert.assertFalse(context.backendExecutor().isShutdown()); + assertRaftLeader(client, connect, b.spaceGraphName()); + Mockito.verify(owner).shutdown(); + interruptJoin.set(false); + manager.dropGraph("DEFAULT", a.name(), false); + Assert.assertEquals(State.STATE_SHUTDOWN, nodeA.getNodeState()); + assertRaftExecutorsTerminated(context); + Assert.assertFalse(manager.graphs().contains(a.spaceGraphName())); + Mockito.verify(owner, Mockito.times(2)).shutdown(); + a.close(); + context.close(); + Mockito.verify(owner, Mockito.times(2)).shutdown(); + Assert.assertEquals(State.STATE_LEADER, nodeB.getNodeState()); + assertRaftLeader(client, connect, b.spaceGraphName()); + client.shutdown(); + client = null; + manager.close(); + awaitNativeRpcClosed(shared.port()); + Assert.assertFalse(NodeManager.getInstance().serverExists(address)); + } finally { + Thread.interrupted(); + Thread.currentThread().setName(threadName); + if (client != null) { + client.shutdown(); + } + manager.close(); + for (RaftContext context : contexts) { + stopRaftFixtureExecutors(context); + } + for (HugeGraph graph : graphs) { + HugeFactory.remove(graph); + } + } + } + + private GraphManager raftOwnerManager(String name, int port) throws Exception { + OptionSpace.register("rocksdb", RocksDBOptions.instance()); + BackendProviderFactory.register("rocksdb", RocksDBStoreProvider.class.getName()); + GraphManager manager = this.dropManager(name); + RpcServer rpc = Whitebox.getInternalState(manager, "rpcServer"); + ServerConfig config = Whitebox.getInternalState(rpc, "serverConfig"); + config.setPort(port); + return manager; + } + + private HugeGraph attachRaftGraph(GraphManager manager, String name, + com.alipay.remoting.rpc.RpcServer shared, + List contexts) throws Exception { + String suffix = name + "_" + contexts.size(); + String data = this.temporary.newFolder(suffix + "_data").getPath(); + String wal = this.temporary.newFolder(suffix + "_wal").getPath(); + HugeGraph graph = manager.createGraphLocal(name, "gremlin.graph=" + HugeFactory.class.getName() + + "\nbackend=rocksdb\nserializer=binary\nstore=" + name + + "\nrocksdb.data_path=" + data + "\nrocksdb.wal_path=" + wal + "\n"); + HugeConfig config = graph.configuration(); + config.setProperty("raft.group_peers", shared.ip() + ":" + shared.port()); + config.setProperty(CoreOptions.RAFT_PATH.name(), this.temporary.newFolder(suffix + "_raft").getPath()); + config.setProperty(CoreOptions.RAFT_ELECTION_TIMEOUT.name(), 300); + config.setProperty(CoreOptions.RAFT_SNAPSHOT_INTERVAL.name(), 0); + config.setProperty(CoreOptions.RAFT_RPC_THREADS.name(), 2); + config.setProperty(CoreOptions.RAFT_QUEUE_SIZE.name(), 2048); + config.setProperty(CoreOptions.RAFT_SAFE_READ.name(), false); + HugeGraphParams params = Whitebox.getInternalState(graph, "params"); + RaftBackendStoreProvider raft = new RaftBackendStoreProvider(params, graph.storeProvider()); + raft.loadSchemaStore(config); + raft.loadGraphStore(config); + raft.loadSystemStore(config); + contexts.add(Whitebox.getInternalState(raft, "context")); + Whitebox.setInternalState(graph, "storeProvider", raft); + return graph; + } + + private static void startSharedRaftPair(List contexts, + com.alipay.remoting.rpc.RpcServer shared) throws Exception { + contexts.get(0).initRaftNode(shared); + RaftContext second = contexts.get(1); + // Production second-context init currently repeats processor registration and fails. + // Bypass only that independent startup defect to exercise real shared-node close ownership. + Whitebox.setInternalState(second, "raftRpcServer", new BoltRpcServer(shared)); + Whitebox.setInternalState(second, "sharedRpcServer", true); + Whitebox.setInternalState(second, "endpoint", new PeerId(shared.ip(), shared.port())); + Whitebox.setInternalState(second, "raftNode", new RaftNode(second)); + awaitRaftLeader(nativeNode(contexts.get(0))); + awaitRaftLeader(nativeNode(second)); + } + + private static Node nativeNode(RaftContext context) { + return Whitebox.invoke(RaftNode.class, "node", context.node()); + } + + private static void awaitRaftLeader(Node node) throws Exception { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while (node.getNodeState() != State.STATE_LEADER && System.nanoTime() < deadline) { + Thread.sleep(10); + } + Assert.assertEquals(State.STATE_LEADER, node.getNodeState()); + } + + private static void assertRaftLeader(CliClientServiceImpl client, Endpoint endpoint, + String graph) throws Exception { + com.google.protobuf.Message response = client.getLeader(endpoint, + CliRequests.GetLeaderRequest.newBuilder().setGroupId(graph).build(), null) + .get(5, TimeUnit.SECONDS); + Assert.assertTrue(response instanceof CliRequests.GetLeaderResponse); + Assert.assertFalse(((CliRequests.GetLeaderResponse) response).getLeaderId().isEmpty()); + } + + @Test + public void testFatalCloseErrorEscapesPeerRemovalBatch() throws Exception { + GraphManager manager = this.dropManager("fatal_batch"); + HugeGraph first = null; + HugeGraph next = null; + BackendStoreProvider provider = null; + try { + first = manager.createGraphLocal("fatal_first", sharedMemoryConfig("fatal_first")); + next = manager.createGraphLocal("fatal_next", sharedMemoryConfig("fatal_next")); + Map factory = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + factory.put(first.spaceGraphName(), first); + factory.put(next.spaceGraphName(), next); + provider = Mockito.spy(first.storeProvider()); + AssertionError fatal = new AssertionError("fatal provider close"); + Mockito.doThrow(fatal).when(provider).close(); + Whitebox.setInternalState(first, "storeProvider", provider); + MetaManager meta = Mockito.mock(MetaManager.class); + Mockito.when(meta.extractGraphsFromResponse("fatal")).thenReturn( + List.of(first.spaceGraphName(), next.spaceGraphName())); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + java.lang.reflect.Method method = GraphManager.class.getDeclaredMethod("graphRemoveHandler", Object.class); + method.setAccessible(true); + AssertionError caught = Assert.assertThrows(AssertionError.class, () -> { + try { + method.invoke(manager, "fatal"); + } catch (InvocationTargetException failure) { + // Whitebox.invoke wraps Error, so preserve the actual handler result here. + throw failure.getCause(); + } + }); + Assert.assertSame(fatal, caught); + Assert.assertSame(first, manager.graph(first.spaceGraphName())); + Assert.assertSame(next, manager.graph(next.spaceGraphName())); + Assert.assertSame(first, factory.get(first.spaceGraphName())); + Assert.assertFalse(next.closed()); + } finally { + if (provider != null) { + Mockito.doCallRealMethod().when(provider).close(); + } + manager.close(); + if (first != null) { + HugeFactory.remove(first); + } + if (next != null) { + HugeFactory.remove(next); + } + } + } + + @Test + public void testEarlierRuntimeFailureDoesNotHideFatalCloseError() throws Exception { + this.checkFatalClosePriority(false); + } + + @Test + public void testRepeatedFatalCloseFailureKeepsIdentity() throws Exception { + this.checkFatalClosePriority(true); + } + + private void checkFatalClosePriority(boolean same) throws Exception { + String name = same ? "fatal_repeated" : "fatal_priority"; + GraphManager manager = this.dropManager(name); + HugeGraph first = null; + HugeGraph next = null; + BackendStoreProvider provider = null; + org.apache.hugegraph.auth.AuthManager auth = null; + try { + first = manager.createGraphLocal(name, sharedMemoryConfig(name)); + next = manager.createGraphLocal(name + "_next", sharedMemoryConfig(name + "_next")); + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", first); + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", next); + AssertionError fatal = new AssertionError("fatal native release"); + RuntimeException earlier = new IllegalStateException("earlier close failed"); + auth = Mockito.spy(first.authManager()); + Mockito.doThrow(same ? fatal : earlier).when(auth).close(); + first.switchAuthManager(auth); + provider = Mockito.spy(first.storeProvider()); + Mockito.doThrow(fatal).when(provider).close(); + Whitebox.setInternalState(first, "storeProvider", provider); + MetaManager meta = Mockito.mock(MetaManager.class); + Mockito.when(meta.extractGraphsFromResponse("fatal-priority")).thenReturn( + List.of(first.spaceGraphName(), next.spaceGraphName())); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + GraphSpace space = new GraphSpace("DEFAULT"); + space.graphNumberUsed(5); + spaces.put("DEFAULT", space); + java.lang.reflect.Method method = GraphManager.class.getDeclaredMethod("graphRemoveHandler", Object.class); + method.setAccessible(true); + AssertionError caught = Assert.assertThrows(AssertionError.class, () -> { + try { + method.invoke(manager, "fatal-priority"); + } catch (InvocationTargetException wrapped) { + throw wrapped.getCause(); + } + }); + Assert.assertSame(fatal, caught); + Assert.assertEquals(same ? 0 : 1, fatal.getSuppressed().length); + if (!same) { + Assert.assertSame(earlier, fatal.getSuppressed()[0]); + } + Assert.assertSame(first, manager.graph(first.spaceGraphName())); + Assert.assertSame(next, manager.graph(next.spaceGraphName())); + Assert.assertFalse(next.closed()); + } finally { + if (auth != null) { + Mockito.doCallRealMethod().when(auth).close(); + } + if (provider != null) { + Mockito.doCallRealMethod().when(provider).close(); + } + manager.close(); + if (first != null) { + HugeFactory.remove(first); + } + if (next != null) { + HugeFactory.remove(next); + } + } + } + + @Test + public void testClosedUnclearedOwnerRejectsRestClearRetry() throws Exception { + String name = "closed_uncleared"; + GraphManager manager = this.dropManager(name); + HugeGraph graph = null; + BackendStoreProvider provider = null; + try { + OptionSpace.register("rocksdb", RocksDBOptions.instance()); + BackendProviderFactory.register("rocksdb", RocksDBStoreProvider.class.getName()); + String config = "gremlin.graph=" + HugeFactory.class.getName() + + "\nbackend=rocksdb\nserializer=binary\nstore=" + name + + "\nrocksdb.data_path=" + this.temporary.newFolder("data").getPath() + + "\nrocksdb.wal_path=" + this.temporary.newFolder("wal").getPath() + "\n"; + graph = manager.createGraphLocal(name, config); + graph.schema().vertexLabel("kept").useCustomizeStringId().create(); + graph.addVertex(org.apache.tinkerpop.gremlin.structure.T.label, "kept", + org.apache.tinkerpop.gremlin.structure.T.id, "uncleared"); + graph.tx().commit(); + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", graph); + provider = Mockito.spy(graph.storeProvider()); + IllegalStateException first = new IllegalStateException("provider release failed"); + Mockito.doThrow(first).when(provider).close(); + Whitebox.setInternalState(graph, "storeProvider", provider); + MetaManager meta = Mockito.mock(MetaManager.class); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + HugeException failedClose = Assert.assertThrows(HugeException.class, + () -> manager.dropGraph("DEFAULT", name, false)); + Assert.assertSame(first, failedClose.getCause()); + Assert.assertTrue(graph.closed()); + Map certificates = Whitebox.getInternalState(manager, "clearedGraphs"); + Assert.assertFalse(certificates.containsKey(graph.spaceGraphName())); + Mockito.doCallRealMethod().when(provider).close(); + + HugeException refused = Assert.assertThrows(HugeException.class, () -> + new org.apache.hugegraph.api.profile.GraphsAPI().drop( + manager, "DEFAULT", name, "I'm sure to drop the graph")); + Assert.assertContains("without a successful backend clear", refused.getMessage()); + Assert.assertSame(graph, manager.graph(graph.spaceGraphName())); + Mockito.verifyNoInteractions(meta); + } finally { + if (provider != null) { + Mockito.doCallRealMethod().when(provider).close(); + } + manager.close(); + if (graph != null) { + HugeFactory.remove(graph); + } + } + } + + @Test + public void testClearDeleteRetriesAfterForeignTransactionCloses() throws Exception { + this.checkClearedOwnerRetry(0); + } + + @Test + public void testClearedOwnerPeerRetryDoesNotClearReplacement() throws Exception { + this.checkClearedOwnerRetry(1); + } + + @Test + public void testManagerStopKeepsUnfinishedClearProgress() throws Exception { + this.checkClearedOwnerRetry(2); + } + + private void checkClearedOwnerRetry(int retryMode) throws Exception { + String name = "clear_idle_retry_" + retryMode; + GraphManager manager = this.dropManager(name); + HugeGraph graph = null; + ExecutorService worker = Executors.newSingleThreadExecutor(); + CountDownLatch opened = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicInteger clears = new AtomicInteger(); + try { + graph = manager.createGraphLocal(name, sharedMemoryConfig(name)); + HugeGraph owner = graph; + Map factory = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + factory.put(owner.spaceGraphName(), owner); + HugeGraph proxy = (HugeGraph) Proxy.newProxyInstance(HugeGraph.class.getClassLoader(), + new Class[]{HugeGraph.class}, (delegate, method, args) -> { + if ("clearBackend".equals(method.getName())) { + clears.incrementAndGet(); + } + try { + return method.invoke(owner, args); + } catch (InvocationTargetException failure) { + throw failure.getCause(); + } + }); + Map graphs = Whitebox.getInternalState(manager, "graphs"); + graphs.put(graph.spaceGraphName(), proxy); + MetaManager meta = Mockito.mock(MetaManager.class); + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + GraphSpace space = new GraphSpace("DEFAULT"); + space.graphNumberUsed(5); + spaces.put("DEFAULT", space); + Future transaction = worker.submit(() -> { + try { + owner.tx().open(); + opened.countDown(); + E.checkState(release.await(15, TimeUnit.SECONDS), "Foreign transaction release timed out"); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(failure); + } finally { + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", owner); + } + }); + Assert.assertTrue(opened.await(5, TimeUnit.SECONDS)); + HugeException refusal = Assert.assertThrows(HugeException.class, + () -> manager.dropGraph("DEFAULT", name, true)); + Assert.assertTrue(refusal.getCause() instanceof IllegalStateException); + Assert.assertFalse(graph.closed()); + Assert.assertEquals(1, clears.get()); + Map progress = Whitebox.getInternalState(manager, "clearedGraphs"); + Assert.assertSame(proxy, progress.get(graph.spaceGraphName())); + Assert.assertSame(proxy, manager.graph(graph.spaceGraphName())); + if (retryMode == 1) { + Assert.assertThrows(HugeException.class, () -> manager.dropGraph("DEFAULT", name, false)); + Assert.assertSame(proxy, progress.get(graph.spaceGraphName())); + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + RpcServer server = Whitebox.getInternalState(manager, "rpcServer"); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + assertRpcResources(server, client, 0); + } else if (retryMode == 2) { + manager.close(); + Assert.assertSame(proxy, progress.get(graph.spaceGraphName())); + } + release.countDown(); + transaction.get(5, TimeUnit.SECONDS); + if (retryMode == 2) { + manager.close(); + } else { + // This is the same public path used by REST graph DELETE and by PD peer removal. + manager.dropGraph("DEFAULT", name, retryMode == 0); + Assert.assertFalse(manager.graphs().contains(graph.spaceGraphName())); + Assert.assertFalse(factory.containsKey(graph.spaceGraphName())); + } + Assert.assertTrue(graph.closed()); + Assert.assertFalse(progress.containsKey(graph.spaceGraphName())); + Assert.assertEquals(1, clears.get()); + Mockito.verify(meta).removeGraphConfig("DEFAULT", name); + Mockito.verify(meta).notifyGraphRemove("DEFAULT", name); + if (retryMode == 1) { + HugeGraph replacement = manager.createGraphLocal(name, sharedMemoryConfig(name)); + factory.put(replacement.spaceGraphName(), replacement); + BackendStoreProvider replacementProvider = Mockito.spy(replacement.storeProvider()); + Whitebox.setInternalState(replacement, "storeProvider", replacementProvider); + // Even a stale certificate for the same name must not certify this new identity. + progress.put(replacement.spaceGraphName(), proxy); + manager.dropGraph("DEFAULT", name, true); + Mockito.verify(replacementProvider).clear(); + Assert.assertTrue(replacement.closed()); + Assert.assertFalse(progress.containsKey(replacement.spaceGraphName())); + Mockito.verify(meta, Mockito.times(2)).removeGraphConfig("DEFAULT", name); + Mockito.verify(meta, Mockito.times(2)).notifyGraphRemove("DEFAULT", name); + } + } finally { + release.countDown(); + worker.shutdownNow(); + Assert.assertTrue(worker.awaitTermination(5, TimeUnit.SECONDS)); + manager.close(); + if (graph != null) { + HugeFactory.remove(graph); + } + } + } + + @Test + public void testRaftContextClosesOwnedExecutorsAfterWork() throws Exception { + this.checkRaftExecutorDrain(0); + } + + @Test + public void testRaftExecutorTimeoutRetainsOwnerAndQueuedWork() throws Exception { + this.checkRaftExecutorDrain(1); + } + + @Test + public void testInterruptedRaftExecutorDrainRetainsOwner() throws Exception { + this.checkRaftExecutorDrain(2); + } + + private void checkRaftExecutorDrain(int failureMode) throws Exception { + String name = "raft_executor_drain_" + failureMode; + GraphManager manager = this.dropManager(name); + HugeGraph graph = null; + RaftContext context = null; + CountDownLatch started = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicBoolean interruptedWorker = new AtomicBoolean(); + AtomicBoolean interruptAwait = new AtomicBoolean(true); + try { + graph = manager.createGraphLocal(name, sharedMemoryConfig(name)); + Map factory = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + factory.put(graph.spaceGraphName(), graph); + com.alipay.sofa.jraft.rpc.RpcServer rpc = startNativeRaftRpc(); + graph.configuration().setProperty("raft.group_peers", "127.0.0.1:" + rpc.boundPort()); + graph.configuration().setProperty(CoreOptions.RAFT_SAFE_READ.name(), true); + graph.configuration().setProperty(CoreOptions.RAFT_BACKEND_THREADS.name(), 1); + graph.configuration().setProperty(CoreOptions.TASK_WAIT_TIMEOUT.name(), failureMode == 1 ? 0L : 1L); + RaftBackendStoreProvider raft = new RaftBackendStoreProvider( + Whitebox.getInternalState(graph, "params"), graph.storeProvider()); + context = Whitebox.getInternalState(raft, "context"); + Whitebox.setInternalState(context, "raftRpcServer", rpc); + Whitebox.setInternalState(context, "endpoint", new PeerId("127.0.0.1", rpc.boundPort())); + Whitebox.setInternalState(graph, "storeProvider", raft); + for (ExecutorService executor : new ExecutorService[]{context.readIndexExecutor(), + context.snapshotExecutor(), + context.backendExecutor()}) { + executor.submit(() -> { }).get(5, TimeUnit.SECONDS); + } + Future running = context.backendExecutor().submit(() -> { + started.countDown(); + try { + E.checkState(release.await(10, TimeUnit.SECONDS), "Backend executor release timed out"); + } catch (InterruptedException interrupted) { + interruptedWorker.set(true); + Thread.currentThread().interrupt(); + } + }); + Assert.assertTrue(started.await(5, TimeUnit.SECONDS)); + AtomicInteger completed = new AtomicInteger(); + Future queued = context.backendExecutor().submit(completed::incrementAndGet); + Whitebox.setInternalState(manager, "metaManager", Mockito.mock(MetaManager.class)); + Whitebox.setInternalState(manager, "PDExist", true); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + GraphSpace space = new GraphSpace("DEFAULT"); + space.graphNumberUsed(5); + spaces.put("DEFAULT", space); + if (failureMode == 0) { + release.countDown(); + manager.dropGraph("DEFAULT", name, false); + } else { + if (failureMode == 2) { + // Begin the interrupt only at executor await, after graph's caller-TX cleanup. + ExecutorService backend = context.backendExecutor(); + ExecutorService awaiting = (ExecutorService) Proxy.newProxyInstance( + ExecutorService.class.getClassLoader(), new Class[]{ExecutorService.class}, + (proxy, method, args) -> { + if ("awaitTermination".equals(method.getName()) && interruptAwait.get()) { + Thread.currentThread().interrupt(); + } + try { + return method.invoke(backend, args); + } catch (InvocationTargetException failure) { + throw failure.getCause(); + } + }); + Whitebox.setInternalState(context, "backendExecutor", awaiting); + } + HugeException failure = Assert.assertThrows(HugeException.class, + () -> manager.dropGraph("DEFAULT", name, false)); + if (failureMode == 2) { + Assert.assertTrue(HugeException.rootCause(failure) instanceof InterruptedException); + Assert.assertTrue(Thread.interrupted()); + interruptAwait.set(false); + } + Assert.assertTrue(graph.closed()); + Assert.assertSame(graph, manager.graph(graph.spaceGraphName())); + Assert.assertSame(graph, factory.get(graph.spaceGraphName())); + Assert.assertTrue(context.backendExecutor().isShutdown()); + Assert.assertFalse(context.backendExecutor().isTerminated()); + Assert.assertFalse(queued.isDone()); + Assert.assertFalse(interruptedWorker.get()); + release.countDown(); + running.get(5, TimeUnit.SECONDS); + queued.get(5, TimeUnit.SECONDS); + graph.configuration().setProperty(CoreOptions.TASK_WAIT_TIMEOUT.name(), 1L); + manager.dropGraph("DEFAULT", name, false); + } + running.get(5, TimeUnit.SECONDS); + queued.get(5, TimeUnit.SECONDS); + Assert.assertEquals(1, completed.get()); + Assert.assertFalse(interruptedWorker.get()); + assertRaftExecutorsTerminated(context); + Assert.assertFalse(manager.graphs().contains(graph.spaceGraphName())); + awaitNativeRpcClosed(rpc.boundPort()); + } finally { + Thread.interrupted(); + release.countDown(); + stopRaftFixtureExecutors(context); + manager.close(); + if (graph != null) { + HugeFactory.remove(graph); + } + } + } + + private static void assertRaftExecutorsTerminated(RaftContext context) { + for (ExecutorService executor : new ExecutorService[]{context.readIndexExecutor(), + context.snapshotExecutor(), + context.backendExecutor()}) { + if (executor != null) { + Assert.assertTrue(executor.isShutdown()); + Assert.assertTrue(executor.isTerminated()); + } + } + } + + private static com.alipay.sofa.jraft.rpc.RpcServer startNativeRaftRpc() throws Exception { + int port; + try (ServerSocket reservation = new ServerSocket(0)) { + port = reservation.getLocalPort(); + } + return RaftRpcServerFactory.createAndStartRaftRpcServer(new Endpoint("127.0.0.1", port)); + } + + private static boolean nativeRpcListening(int port) { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress("127.0.0.1", port), 200); + return true; + } catch (Exception refused) { + return false; + } + } + + private static void awaitNativeRpcClosed(int port) throws Exception { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + while (nativeRpcListening(port) && System.nanoTime() < deadline) { + Thread.sleep(10); + } + Assert.assertFalse(nativeRpcListening(port)); + } + + private static void stopRaftFixtureExecutors(RaftContext context) throws Exception { + if (context == null) { + return; + } + for (ExecutorService pool : new ExecutorService[]{context.readIndexExecutor(), context.snapshotExecutor(), + context.backendExecutor()}) { + if (pool != null) { + pool.shutdownNow(); + Assert.assertTrue(pool.awaitTermination(5, TimeUnit.SECONDS)); + } + } + } + + @Test + public void testMetadataDeleteFailureRestoresRetainedOwnerRpc() throws Exception { + this.checkMetadataDeleteFailure(false, false); + } + + @Test + public void testMetadataDeleteFailureDoesNotSkipPeerRemove() throws Exception { + this.checkMetadataDeleteFailure(false, false, true); + } + + @Test + public void testRemoveNotificationFailureRestoresRetainedOwnerRpc() throws Exception { + this.checkMetadataDeleteFailure(true, false); + } + + @Test + public void testMetadataDeleteFailurePreservesRpcRestoreFailure() throws Exception { + this.checkMetadataDeleteFailure(false, true); + } + + private void checkMetadataDeleteFailure(boolean notify, boolean failRestore) throws Exception { + this.checkMetadataDeleteFailure(notify, failRestore, false); + } + + private void checkMetadataDeleteFailure(boolean notify, boolean failRestore, boolean peerRemove) throws Exception { + OptionSpace.register("core", CoreOptions.instance()); + OptionSpace.register("rpc", RpcOptions.instance()); + OptionSpace.register("server", ServerOptions.instance()); + String name = notify ? "rpc_notify_fail" : failRestore ? "rpc_restore_fail" : "rpc_metadata_fail"; + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(ServerOptions.GRAPHS.name(), this.temporary.newFolder(name).getPath()); + properties.setProperty(ServerOptions.USE_PD.name(), false); + properties.setProperty(ServerOptions.GRAPH_LOAD_FROM_LOCAL_CONFIG.name(), false); + properties.setProperty(ServerOptions.ENABLE_DYNAMIC_CREATE_DROP.name(), true); + properties.setProperty(RpcOptions.RPC_SERVER_HOST.name(), "127.0.0.1"); + properties.setProperty(RpcOptions.RPC_SERVER_PORT.name(), 0); + properties.setProperty(RpcOptions.RPC_REMOTE_URL.name(), "127.0.0.1:1"); + EventHub hub = new EventHub(name); + GraphManager manager = new GraphManager(new HugeConfig(properties), hub); + HugeGraph graph = null; + String threadName = Thread.currentThread().getName(); + try { + graph = manager.createGraphLocal(name, sharedMemoryConfig(name)); + Whitebox.invoke(GraphManager.class, "startRpcServer", manager); + RpcServer original = Whitebox.getInternalState(manager, "rpcServer"); + RpcServer server = Mockito.spy(original); + Whitebox.setInternalState(manager, "rpcServer", server); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + assertRpcResources(server, client, 2); + IllegalStateException failure = new IllegalStateException("metadata fault before clear"); + IllegalStateException restoration = new IllegalStateException("RPC restoration export failed"); + if (failRestore) { + Mockito.doThrow(restoration).when(server).exportAll(); + } + MetaManager meta = Mockito.mock(MetaManager.class); + if (notify) { + Mockito.doThrow(failure).doNothing().when(meta).notifyGraphRemove("DEFAULT", name); + } else { + Mockito.doThrow(failure).doNothing().when(meta).removeGraphConfig("DEFAULT", name); + } + Whitebox.setInternalState(manager, "metaManager", meta); + Whitebox.setInternalState(manager, "PDExist", true); + AtomicReference rejected = new AtomicReference<>(); + Assert.assertThrows(HugeException.class, () -> manager.dropGraph("DEFAULT", name, true), rejected::set); + Mockito.verify(meta).removeGraphConfig("DEFAULT", name); + if (notify) { + Mockito.verify(meta).notifyGraphRemove("DEFAULT", name); + } else { + Mockito.verify(meta, Mockito.never()).notifyGraphRemove("DEFAULT", name); + } + Assert.assertFalse(graph.closed()); + Assert.assertSame(graph, manager.graph(graph.spaceGraphName())); + assertRpcResources(server, client, failRestore ? 0 : 2); + Assert.assertSame(failure, rejected.get().getCause()); + Mockito.verify(server).exportAll(); + Assert.assertEquals(failRestore ? 1 : 0, failure.getSuppressed().length); + if (failRestore) { + Assert.assertSame(restoration, failure.getSuppressed()[0]); + } + Thread.currentThread().setName("grpc-rpc-metadata-retry"); + if (peerRemove) { + // Another server deleted the config after this local metadata attempt failed. + Mockito.when(meta.extractGraphsFromResponse(Mockito.any())) + .thenReturn(java.util.Collections.singletonList(graph.spaceGraphName())); + Whitebox.invoke(GraphManager.class, new Class[]{Object.class}, + "graphRemoveHandler", manager, "peer-remove"); + } else { + // Retry the fenced deletion; successful clear must not re-export the removed owner. + if (notify) { + java.util.Set removing = Whitebox.getInternalState(manager, "removingGraphs"); + Assert.assertTrue(removing.contains(graph.spaceGraphName())); + } + manager.dropGraph("DEFAULT", name, true); + } + Assert.assertTrue(graph.closed()); + Assert.assertFalse(manager.graphs().contains(graph.spaceGraphName())); + assertRpcResources(server, client, 0); + Mockito.verify(server).exportAll(); + } finally { + Thread.currentThread().setName(threadName); + manager.close(); + if (graph != null) { + HugeFactory.remove(graph); + } + } + } + + @Test + public void testLocalGraphGremlinPublicationWaitsForRpc() throws Exception { + this.checkLocalGraphGremlinPublication(false); + } + + @Test + public void testLocalGraphRpcFailureNeverPublishesToGremlin() throws Exception { + this.checkLocalGraphGremlinPublication(true); + } + + private void checkLocalGraphGremlinPublication(boolean failExport) throws Exception { + OptionSpace.register("core", CoreOptions.instance()); + OptionSpace.register("rpc", RpcOptions.instance()); + OptionSpace.register("server", ServerOptions.instance()); + String name = failExport ? "rpc_export_failure" : "rpc_export_success"; + String graphName = "DEFAULT-" + name; + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(ServerOptions.GRAPHS.name(), this.temporary.newFolder(name).getPath()); + properties.setProperty(ServerOptions.USE_PD.name(), false); + properties.setProperty(ServerOptions.GRAPH_LOAD_FROM_LOCAL_CONFIG.name(), false); + properties.setProperty(ServerOptions.ENABLE_DYNAMIC_CREATE_DROP.name(), true); + properties.setProperty(RpcOptions.RPC_SERVER_HOST.name(), "127.0.0.1"); + properties.setProperty(RpcOptions.RPC_SERVER_PORT.name(), 0); + properties.setProperty(RpcOptions.RPC_REMOTE_URL.name(), "127.0.0.1:1"); + EventHub hub = new EventHub(name); + Settings settings = new Settings(); + settings.gremlinPool = 1; + settings.threadPoolBoss = 1; + settings.threadPoolWorker = 1; + // Production constructs Gremlin before REST/GraphManager, registering its listener first. + ContextGremlinServer gremlin = new ContextGremlinServer(settings, hub); + GraphManager manager = new GraphManager(new HugeConfig(properties), hub); + RpcServer original = Whitebox.getInternalState(manager, "rpcServer"); + RpcServer server = Mockito.spy(original); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + Whitebox.setInternalState(manager, "rpcServer", server); + Whitebox.setInternalState(manager, "rpcInitialized", true); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + IllegalStateException failure = new IllegalStateException("RPC export rejected"); + Mockito.doAnswer(invocation -> { + entered.countDown(); + E.checkState(release.await(10, TimeUnit.SECONDS), "RPC export barrier timed out"); + if (failExport) { + throw failure; + } + return invocation.callRealMethod(); + }).when(server).exportAll(); + ExecutorService initializer = Executors.newSingleThreadExecutor(); + AtomicReference candidate = new AtomicReference<>(); + org.apache.tinkerpop.gremlin.server.GraphManager bindings = + gremlin.getServerGremlinExecutor().getGraphManager(); + try { + Future publication = initializer.submit(() -> { + try { + HugeGraph graph = manager.createGraphLocal(name, sharedMemoryConfig(name)); + candidate.set(graph); + return graph; + } finally { + Map owners = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + HugeGraph graph = owners.get(graphName); + if (graph != null) { + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", graph); + } + } + }); + Assert.assertTrue(entered.await(5, TimeUnit.SECONDS)); + Assert.assertNull(bindings.getGraph(graphName)); + Assert.assertNull(bindings.getTraversalSource("__g_" + graphName)); + Assert.assertFalse(bindings.getAsBindings().containsKey(graphName)); + Assert.assertFalse(manager.graphs().contains(graphName)); + assertRpcResources(server, client, 2); + release.countDown(); + if (failExport) { + Assert.assertThrows(ExecutionException.class, () -> publication.get(5, TimeUnit.SECONDS), + caught -> Assert.assertSame(failure, caught.getCause())); + Assert.assertNull(bindings.getGraph(graphName)); + Assert.assertNull(bindings.getTraversalSource("__g_" + graphName)); + Assert.assertFalse(manager.graphs().contains(graphName)); + Map owners = Whitebox.getInternalState(HugeFactory.class, "GRAPHS"); + Assert.assertFalse(owners.containsKey(graphName)); + assertRpcResources(server, client, 0); + } else { + HugeGraph graph = publication.get(5, TimeUnit.SECONDS); + Assert.assertSame(graph, bindings.getGraph(graphName)); + Assert.assertNotNull(bindings.getTraversalSource("__g_" + graphName)); + Assert.assertSame(graph, manager.graph(graphName)); + assertRpcResources(server, client, 2); + } + } finally { + release.countDown(); + initializer.shutdown(); + Assert.assertTrue(initializer.awaitTermination(10, TimeUnit.SECONDS)); + manager.close(); + gremlin.stop().get(30, TimeUnit.SECONDS); + if (candidate.get() != null) { + HugeFactory.remove(candidate.get()); + } + HugeGraphAuthProxy.resetContext(); + } + } + + @Test + public void testLateGraphRpcPublicationRollbackAndReplacement() throws Exception { + OptionSpace.register("core", CoreOptions.instance()); + OptionSpace.register("rpc", RpcOptions.instance()); + OptionSpace.register("server", ServerOptions.instance()); + PropertiesConfiguration properties = new PropertiesConfiguration(); + properties.setProperty(ServerOptions.GRAPHS.name(), this.temporary.newFolder("graphs").getPath()); + properties.setProperty(ServerOptions.USE_PD.name(), false); + properties.setProperty(ServerOptions.GRAPH_LOAD_FROM_LOCAL_CONFIG.name(), false); + properties.setProperty(ServerOptions.ENABLE_DYNAMIC_CREATE_DROP.name(), true); + properties.setProperty(RpcOptions.RPC_SERVER_HOST.name(), "127.0.0.1"); + properties.setProperty(RpcOptions.RPC_SERVER_PORT.name(), 0); + try (ServerSocket reservation = new ServerSocket(0)) { + properties.setProperty(RpcOptions.RPC_REMOTE_URL.name(), "127.0.0.1:" + reservation.getLocalPort()); + } + EventHub hub = new EventHub("rpc-publication-test"); + GraphManager manager = new GraphManager(new HugeConfig(properties), hub); + BarrierRpcServer server = new BarrierRpcServer(new HugeConfig(properties)); + Whitebox.setInternalState(manager, "rpcServer", server); + RpcClientProvider client = Whitebox.getInternalState(manager, "rpcClient"); + ExecutorService initializer = Executors.newFixedThreadPool(2); + HugeGraph startup = null; + HugeGraph replacement = null; + try { + startup = manager.createGraphLocal("rpc_startup", sharedMemoryConfig("rpc_startup")); + Assert.assertEquals(0, server.config().configs().size()); + HugeGraph startupOwner = startup; + AtomicReference initThread = new AtomicReference<>(); + Future initialization = initializer.submit(() -> { + initThread.set(Thread.currentThread()); + try { + Whitebox.invoke(manager.getClass(), "startRpcServer", manager); + } finally { + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", startupOwner); + } + }); + Assert.assertTrue(server.entered.await(5, TimeUnit.SECONDS)); + Assert.assertEquals(2, server.config().configs().size()); + AtomicReference publisher = new AtomicReference<>(); + Future publication = initializer.submit(() -> { + publisher.set(Thread.currentThread()); + HugeGraph graph = manager.createGraphLocal("rpc_late", sharedMemoryConfig("rpc_late")); + Whitebox.invoke(StandardHugeGraph.class, "closeCurrentThreadTransaction", graph); + return graph; + }); + awaitManagerGate(publisher, initThread.get()); + Assert.assertFalse(manager.graphs().contains("DEFAULT-rpc_late")); + assertRpcResources(server, client, 2); + server.release.countDown(); + initialization.get(5, TimeUnit.SECONDS); + HugeGraph original = publication.get(5, TimeUnit.SECONDS); + Assert.assertEquals(2, server.exports.get()); + assertRpcResources(server, client, 4); + + EventListener failure = event -> { + throw new IllegalStateException("reject candidate after RPC registration"); + }; + hub.listen(Events.GRAPH_CREATE, failure); + try { + Assert.assertThrows(HugeException.class, () -> manager.createGraphLocal( + "rpc_rejected", sharedMemoryConfig("rpc_rejected"))); + } finally { + hub.unlisten(Events.GRAPH_CREATE, failure); + } + Assert.assertFalse(manager.graphs().contains("DEFAULT-rpc_rejected")); + assertRpcResources(server, client, 4); + + manager.dropGraphLocal("rpc_late"); + assertRpcResources(server, client, 2); + replacement = manager.createGraphLocal("rpc_late", sharedMemoryConfig("rpc_late")); + hub.notifySync(Events.GRAPH_DROP, original); + Assert.assertSame(replacement, manager.graph("DEFAULT-rpc_late")); + assertRpcResources(server, client, 4); + manager.close(); + List bootstraps = Whitebox.getInternalState(client.config(), "bootstraps"); + Assert.assertEquals(0, server.config().configs().size()); + Assert.assertEquals(0, bootstraps.size()); + } finally { + server.release.countDown(); + initializer.shutdownNow(); + initializer.awaitTermination(5, TimeUnit.SECONDS); + manager.close(); + if (startup != null) { + HugeFactory.remove(startup); + } + if (replacement != null) { + HugeFactory.remove(replacement); + } + } + } + + private static void awaitManagerGate(AtomicReference publisher, Thread initializer) throws Exception { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + while (System.nanoTime() < deadline) { + Thread thread = publisher.get(); + ThreadInfo info = thread == null ? null : ManagementFactory.getThreadMXBean().getThreadInfo(thread.getId()); + if (info != null && info.getThreadState() == Thread.State.BLOCKED && + info.getLockOwnerId() == initializer.getId()) { + Assert.assertContains(GraphManager.class.getName(), info.getLockName()); + return; + } + Thread.sleep(5); + } + Assert.fail("Graph publisher did not block on the startup manager gate"); + } + + private static class BarrierRpcServer extends RpcServer { + + private final CountDownLatch entered = new CountDownLatch(1); + private final CountDownLatch release = new CountDownLatch(1); + private final AtomicInteger exports = new AtomicInteger(); + + BarrierRpcServer(HugeConfig config) { + super(config); + } + + @Override + public void exportAll() { + if (this.exports.incrementAndGet() == 1) { + this.entered.countDown(); + try { + E.checkState(this.release.await(5, TimeUnit.SECONDS), "RPC export barrier timed out"); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(failure); + } + } + super.exportAll(); + } + } + + private static void assertRpcResources(RpcServer server, RpcClientProvider client, int count) { + Assert.assertEquals(count, server.config().configs().size()); + Map configs = Whitebox.getInternalState(client.config(), "configs"); + List bootstraps = Whitebox.getInternalState(client.config(), "bootstraps"); + Assert.assertEquals(count, configs.size()); + Assert.assertEquals(count, bootstraps.size()); + } + + private static String sharedMemoryConfig(String name) { + return "gremlin.graph=" + SharedMemoryGraph.class.getName() + "\nbackend=memory\nstore=" + name + "\n"; + } + + // Exercises cache RPC registration, not shared-backend DML or consistency. + public static class SharedMemoryGraph extends StandardHugeGraph { + + public SharedMemoryGraph(HugeConfig config) { + super(config); + } + + public static SharedMemoryGraph open(Configuration config) { + return new SharedMemoryGraph(new HugeConfig(config)); + } + + @Override + public BackendFeatures backendStoreFeatures() { + BackendFeatures features = super.backendStoreFeatures(); + return (BackendFeatures) Proxy.newProxyInstance(BackendFeatures.class.getClassLoader(), + new Class[]{BackendFeatures.class}, (proxy, method, args) -> + "supportsSharedStorage".equals(method.getName()) ? true : method.invoke(features, args)); + } + } + @Test public void testAttachExplicitLocalResourceLimits() { PropertiesConfiguration properties = new PropertiesConfiguration(); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerLocalGraphBindTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerLocalGraphBindTest.java new file mode 100644 index 0000000000..91a5ea2bed --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerLocalGraphBindTest.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.hugegraph.unit.core; + +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 java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.commons.io.FileUtils; +import org.apache.hugegraph.HugeFactory; +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.auth.ContextGremlinServer; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.event.EventHub; +import org.apache.hugegraph.event.EventListener; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.util.Events; +import org.apache.tinkerpop.gremlin.server.Settings; +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +/** + * Gremlin Server opens conf/graphs on its own, once, and drops a graph whose + * open fails there. The REST load of the same graph has to offer it to + * Gremlin again, or that Server answers REST while every Gremlin call on the + * graph fails until it restarts (#3228). + */ +public class GraphManagerLocalGraphBindTest { + + private File graphsDir; + private EventHub hub; + private GraphManager manager; + private ContextGremlinServer gremlin; + private List created; + private EventListener listener; + + @Before + public void setup() throws IOException { + this.graphsDir = Files.createTempDirectory("local-graph-bind").toFile(); + this.hub = new EventHub("local-graph-bind-test"); + this.manager = new GraphManager( + new HugeConfig(new PropertiesConfiguration()), this.hub); + this.created = new CopyOnWriteArrayList<>(); + this.listener = event -> { + event.checkArgs(HugeGraph.class); + this.created.add(((HugeGraph) event.args()[0]).spaceGraphName()); + return null; + }; + this.hub.listen(Events.GRAPH_CREATE, this.listener); + } + + @After + public void teardown() throws Exception { + List graphs = new ArrayList<>(); + for (String name : this.manager.graphs()) { + graphs.add(this.manager.graph(name)); + } + try { + if (this.gremlin != null) { + this.gremlin.stop().get(30, TimeUnit.SECONDS); + } + this.hub.unlisten(Events.GRAPH_CREATE, this.listener); + this.manager.close(); + } finally { + graphs.forEach(HugeFactory::remove); + FileUtils.deleteQuietly(this.graphsDir); + } + } + + @Test + public void testLocalLoadOffersGraphToGremlin() throws IOException { + String path = this.writeGraphConf("bind_local", "bind_local"); + + this.manager.loadGraphsFromLocal(ImmutableMap.of("bind_local", path)); + + Assert.assertEquals(1, this.created.size()); + Assert.assertEquals("DEFAULT-bind_local", this.created.get(0)); + Assert.assertNotNull(this.manager.graph("DEFAULT", "bind_local")); + } + + @Test + public void testLoadingTwiceRebindsTheSameGraph() throws IOException { + String path = this.writeGraphConf("bind_twice", "bind_twice"); + Map confs = ImmutableMap.of("bind_twice", path); + + this.manager.loadGraphsFromLocal(confs); + HugeGraph first = this.manager.graph("DEFAULT", "bind_twice"); + this.manager.loadGraphsFromLocal(confs); + + Assert.assertEquals(2, this.created.size()); + Assert.assertSame(first, this.manager.graph("DEFAULT", "bind_twice")); + } + + @Test + public void testGraphNamedDifferentlyByConfigIsNotRebound() throws IOException { + String path = this.writeGraphConf("bind_file", "bind_store"); + + this.manager.loadGraphsFromLocal(ImmutableMap.of("bind_file", path)); + + Assert.assertTrue(this.created.isEmpty()); + Assert.assertNotNull(this.manager.graph("DEFAULT", "bind_file")); + } + + @Test + public void testFailedBindKeepsGraphLoaded() throws IOException { + String path = this.writeGraphConf("bind_failed", "bind_failed"); + EventListener failing = event -> { + throw new IllegalStateException("gremlin binding failed"); + }; + this.hub.listen(Events.GRAPH_CREATE, failing); + + try { + this.manager.loadGraphsFromLocal(ImmutableMap.of("bind_failed", path)); + } finally { + this.hub.unlisten(Events.GRAPH_CREATE, failing); + } + + Assert.assertEquals(1, this.created.size()); + Assert.assertNotNull(this.manager.graph("DEFAULT", "bind_failed")); + } + + @Test + public void testGraphMissingFromGremlinGetsBound() throws IOException { + String path = this.writeGraphConf("bind_missing", "bind_missing"); + // No settings.graphs entry stands in for a failed static open + this.gremlin = new ContextGremlinServer(newSettings(), this.hub); + org.apache.tinkerpop.gremlin.server.GraphManager bindings = + this.gremlin.getServerGremlinExecutor().getGraphManager(); + Assert.assertNull(bindings.getGraph("DEFAULT-bind_missing")); + + this.manager.loadGraphsFromLocal(ImmutableMap.of("bind_missing", path)); + + Assert.assertSame(this.manager.graph("DEFAULT", "bind_missing"), + bindings.getGraph("DEFAULT-bind_missing")); + Assert.assertNotNull(bindings.getTraversalSource("__g_DEFAULT-bind_missing")); + } + + @Test + public void testGraphOpenedByGremlinKeepsItsInstance() throws IOException { + String path = this.writeGraphConf("bind_static", "bind_static"); + Settings settings = newSettings(); + settings.graphs.put("DEFAULT-bind_static", path); + this.gremlin = new ContextGremlinServer(settings, this.hub); + org.apache.tinkerpop.gremlin.server.GraphManager bindings = + this.gremlin.getServerGremlinExecutor().getGraphManager(); + Graph opened = bindings.getGraph("DEFAULT-bind_static"); + Assert.assertNotNull(opened); + + this.manager.loadGraphsFromLocal(ImmutableMap.of("bind_static", path)); + + // The rebind ran and handed Gremlin the instance it already held + Assert.assertEquals(ImmutableList.of("DEFAULT-bind_static"), this.created); + Assert.assertSame(opened, bindings.getGraph("DEFAULT-bind_static")); + Assert.assertSame(opened, this.manager.graph("DEFAULT", "bind_static")); + Assert.assertSame(opened, bindings.getTraversalSource("__g_DEFAULT-bind_static") + .getGraph()); + } + + private String writeGraphConf(String fileName, String store) + throws IOException { + File conf = new File(this.graphsDir, fileName + ".properties"); + String content = "gremlin.graph=org.apache.hugegraph.HugeFactory\n" + + "backend=memory\n" + + "serializer=text\n" + + "store=" + store + "\n"; + FileUtils.writeStringToFile(conf, content, StandardCharsets.UTF_8); + return conf.getPath(); + } + + private static Settings newSettings() { + Settings settings = new Settings(); + settings.host = "127.0.0.1"; + settings.port = 0; + settings.gremlinPool = 1; + settings.threadPoolBoss = 1; + settings.threadPoolWorker = 1; + return settings; + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerServiceNamesTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerServiceNamesTest.java new file mode 100644 index 0000000000..242e769724 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerServiceNamesTest.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.hugegraph.unit.core; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.core.GraphManager; +import org.apache.hugegraph.event.EventHub; +import org.apache.hugegraph.meta.MetaDriver; +import org.apache.hugegraph.meta.MetaManager; +import org.apache.hugegraph.meta.lock.LockResult; +import org.apache.hugegraph.meta.managers.ServiceMetaManager; +import org.apache.hugegraph.pd.client.DiscoveryClientImpl; +import org.apache.hugegraph.pd.grpc.discovery.NodeInfo; +import org.apache.hugegraph.pd.grpc.discovery.NodeInfos; +import org.apache.hugegraph.pd.grpc.discovery.Query; +import org.apache.hugegraph.space.GraphSpace; +import org.apache.hugegraph.space.Service; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +public class GraphManagerServiceNamesTest { + + @Test + public void testServiceUrlsPreservesRawIdentityAndRegisterType() throws Exception { + DiscoveryClientImpl client = Mockito.mock(DiscoveryClientImpl.class); + Mockito.when(client.getNodeInfos(Mockito.any(Query.class))) + .thenReturn(NodeInfos.newBuilder() + .addInfo(NodeInfo.newBuilder().setAddress("http://server:8080")) + .build()); + GraphManager manager = new GraphManager( + new HugeConfig(new PropertiesConfiguration()), + new EventHub("service-query-test")); + try { + setField(manager, "pdClient", client); + Class registerType = Class.forName(GraphManager.class.getName() + "$PdRegisterType"); + Object nodePort = Whitebox.getInternalState(registerType, "NODE_PORT"); + Set urls = Whitebox.invoke(GraphManager.class, + new Class[]{String.class, String.class, registerType}, + "getServiceUrls", manager, "team_a", "team/Analytics-prod", nodePort); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + Mockito.verify(client).getNodeInfos(query.capture()); + Assert.assertEquals(Map.of("GRAPHSPACE", "team_a", + "SERVICE_NAME", "team/Analytics-prod", + "REGISTER_TYPE", "NODE_PORT"), query.getValue().getLabelsMap()); + Assert.assertEquals(Set.of("http://server:8080"), urls); + } finally { + manager.close(); + } + } + + @Test + public void testReloadPreservesCompleteServiceNames() { + MetaDriver driver = storageDriver(); + ServiceMetaManager first = new ServiceMetaManager(driver, "cluster"); + for (String name : new String[]{"analytics", "team/analytics", + "analytics-prod", "team/analytics-prod"}) { + first.addServiceConfig("team_a", service(name)); + } + first.addServiceConfig("other", service("analytics")); + + ServiceMetaManager reloaded = new ServiceMetaManager(driver, "cluster"); + Map services = reloaded.serviceConfigs("team_a"); + Assert.assertEquals(4, services.size()); + for (String name : new String[]{"analytics", "team/analytics", + "analytics-prod", "team/analytics-prod"}) { + Assert.assertEquals(name, services.get(name).name()); + Assert.assertEquals(name, reloaded.service("team_a", name).name()); + } + Assert.assertEquals(1, reloaded.serviceConfigs("other").size()); + + reloaded.removeServiceConfig("team_a", "team/analytics"); + Assert.assertNull(reloaded.service("team_a", "team/analytics")); + Assert.assertNotNull(reloaded.service("team_a", "analytics")); + Assert.assertEquals(3, reloaded.serviceConfigs("team_a").size()); + } + + @Test + public void testClearSpaceDropsFullServiceNames() throws Exception { + MetaDriver driver = storageDriver(); + Constructor constructor = MetaManager.class.getDeclaredConstructor(); + constructor.setAccessible(true); + MetaManager metadata = constructor.newInstance(); + setField(metadata, "metaDriver", driver); + setField(metadata, "cluster", "cluster"); + Whitebox.invoke(MetaManager.class, new Class[]{String.class}, + "initManagers", metadata, "cluster"); + GraphManager manager = new GraphManager( + new HugeConfig(new PropertiesConfiguration()), + new EventHub("service-name-test")); + try { + setField(manager, "metaManager", metadata); + // Exercise the metadata-backed clear path without bootstrapping + // a PD/HStore cluster; the real managers use a storage-only seam. + setField(manager, "PDExist", true); + Map spaces = Whitebox.getInternalState(manager, "graphSpaces"); + spaces.put("team_a", new GraphSpace("team_a")); + spaces.put("other", new GraphSpace("other")); + Map cache = Whitebox.getInternalState(manager, "services"); + for (String name : new String[]{"analytics", "analytics-prod", + "team/analytics-prod"}) { + Service service = service(name); + metadata.addServiceConfig("team_a", service); + String key = Whitebox.invoke(GraphManager.class, + new Class[]{String.class, String.class}, "serviceName", + null, "team_a", name); + cache.put(key, service); + } + metadata.addServiceConfig("other", service("analytics")); + cache.put("other-analytics", service("analytics")); + + manager.clearGraphSpace("team_a"); + + Assert.assertTrue(metadata.serviceConfigs("team_a").isEmpty()); + Assert.assertEquals(1, metadata.serviceConfigs("other").size()); + Assert.assertEquals(1, cache.size()); + Assert.assertTrue(cache.containsKey("other-analytics")); + } finally { + manager.close(); + } + } + + private static Service service(String name) { + return new Service(name, "admin", Service.ServiceType.OLTP, + Service.DeploymentType.MANUAL); + } + + private static void setField(Object target, String name, Object value) + throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } + + private static MetaDriver storageDriver() { + Map storage = new LinkedHashMap<>(); + MetaDriver driver = Mockito.mock(MetaDriver.class); + Mockito.doAnswer(call -> { + storage.put(call.getArgument(0), call.getArgument(1)); + return null; + }).when(driver).put(Mockito.anyString(), Mockito.anyString()); + Mockito.when(driver.get(Mockito.anyString())) + .thenAnswer(call -> storage.get(call.getArgument(0))); + Mockito.doAnswer(call -> { + storage.remove(call.getArgument(0)); + return null; + }).when(driver).delete(Mockito.anyString()); + Mockito.doAnswer(call -> { + storage.keySet().removeIf(key -> key.startsWith(call.getArgument(0))); + return null; + }).when(driver).deleteWithPrefix(Mockito.anyString()); + Mockito.when(driver.scanWithPrefix(Mockito.anyString())).thenAnswer(call -> + storage.entrySet().stream() + .filter(entry -> entry.getKey().startsWith(call.getArgument(0))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + Mockito.when(driver.tryLock(Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong())) + .thenAnswer(call -> { + LockResult lock = new LockResult(); + lock.lockSuccess(true); + return lock; + }); + return driver; + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerStoresWaitTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerStoresWaitTest.java index c9d991cee0..e5e060e2d3 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerStoresWaitTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GraphManagerStoresWaitTest.java @@ -30,7 +30,7 @@ import io.grpc.ServerBuilder; import io.grpc.stub.StreamObserver; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.core.GraphManager.Readiness; import org.apache.hugegraph.pd.client.PDConfig; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GroovyScriptEngineCompatibilityTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GroovyScriptEngineCompatibilityTest.java new file mode 100644 index 0000000000..3db519db8f --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/GroovyScriptEngineCompatibilityTest.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.hugegraph.unit.core; + +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.unit.BaseUnitTest; +import org.junit.Test; + +public class GroovyScriptEngineCompatibilityTest extends BaseUnitTest { + + @Test + public void testGroovyJsr223EngineCanCompileAndEvaluate() + throws ScriptException { + ScriptEngine engine = new ScriptEngineManager().getEngineByName( + "groovy"); + + Assert.assertNotNull(engine); + Assert.assertEquals("org.codehaus.groovy.jsr223." + + "GroovyScriptEngineImpl", + engine.getClass().getName()); + Assert.assertTrue(engine instanceof Compilable); + + CompiledScript script = ((Compilable) engine).compile( + "def add = { a, b -> a + b }; add(2, 3)"); + + Assert.assertEquals(5, script.eval()); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HstoreSessionsTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HstoreSessionsTest.java index c7879beb9b..e27533d292 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HstoreSessionsTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HstoreSessionsTest.java @@ -17,15 +17,71 @@ package org.apache.hugegraph.unit.core; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; +import org.apache.hugegraph.backend.store.BackendStoreProvider; import org.apache.hugegraph.backend.store.hstore.HstoreSessions; +import org.apache.hugegraph.backend.store.hstore.HstoreStore; +import org.apache.hugegraph.backend.store.hstore.HstoreStore.HstoreGraphStore; +import org.apache.hugegraph.backend.store.hstore.HstoreTables; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.OlapKey; +import org.apache.hugegraph.util.Bytes; +import org.apache.hugegraph.backend.store.hstore.HstoreSessionsImpl; +import org.apache.hugegraph.store.HgStoreSession; +import org.apache.hugegraph.store.HgStoreClient; +import org.apache.hugegraph.store.HgSessionProvider; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.unit.FakeObjects; import org.junit.Assert; import org.junit.Test; +import org.mockito.Mockito; public class HstoreSessionsTest { + @Test + public void testPoolRejectsBorrowAfterLastHstoreSessionCloses() { + Boolean initialized = Whitebox.getInternalState(HstoreSessionsImpl.class, "initializedNode"); + HgStoreClient previous = Whitebox.getInternalState(HstoreSessionsImpl.class, "hgStoreClient"); + HgStoreClient client = new HgStoreClient(); + HgSessionProvider provider = Mockito.mock(HgSessionProvider.class); + Mockito.when(provider.createSession(Mockito.anyString(), Mockito.isNull())) + .thenReturn(Mockito.mock(HgStoreSession.class)); + Whitebox.setInternalState(client, "sessionProvider", provider); + Whitebox.setInternalState(HstoreSessionsImpl.class, "initializedNode", Boolean.TRUE); + Whitebox.setInternalState(HstoreSessionsImpl.class, "hgStoreClient", client); + try { + HstoreSessionsImpl pool = new HstoreSessionsImpl(FakeObjects.newConfig(), "test", "closed"); + pool.getOrNewSession(); + Assert.assertTrue(pool.close()); + Assert.assertTrue(pool.closed()); + try { + pool.getOrNewSession(); + Assert.fail("closed HStore pool accepted a new borrower"); + } catch (IllegalStateException expected) { + Assert.assertEquals("Backend session pool is closed", expected.getMessage()); + } + Assert.assertTrue(pool.closed()); + Mockito.verify(provider, Mockito.times(2)).createSession(Mockito.anyString(), Mockito.isNull()); + } finally { + Whitebox.setInternalState(HstoreSessionsImpl.class, "hgStoreClient", previous); + Whitebox.setInternalState(HstoreSessionsImpl.class, "initializedNode", initialized); + } + } + @Test public void testOrderedScanDoesNotAddAbstractSubclassRequirement() throws Exception { @@ -36,4 +92,203 @@ public void testOrderedScanDoesNotAddAbstractSubclassRequirement() Assert.assertFalse(Modifier.isAbstract(method.getModifiers())); } + + @Test + public void testClearOlapDeletesOnlyMatchingPropertyAndCommits() throws Exception { + OlapFixture fixture = new OlapFixture(); + byte[] merged = fixture.add(5, IdGenerator.of("alice"), false); + byte[] legacy = fixture.add(5, IdGenerator.of("legacy"), true); + fixture.add(8, IdGenerator.of("alice"), false); + // The legacy vertex key equals the target property prefix but belongs to property 8. + fixture.add(8, IdGenerator.of(5), true); + fixture.store.clearOlapTable(IdGenerator.of(5)); + Assert.assertEquals(2, fixture.rows.size()); + Mockito.verify(fixture.session).delete(fixture.table, IdGenerator.of("alice").asBytes(), merged); + Mockito.verify(fixture.session).delete(fixture.table, IdGenerator.of("legacy").asBytes(), legacy); + Mockito.verify(fixture.session).commit(); + Mockito.verify(fixture.columns).close(); + Mockito.verify(fixture.sessions, Mockito.never()).dropTable(Mockito.any(String[].class)); + Mockito.verify(fixture.sessions, Mockito.never()).truncateTable(Mockito.anyString()); + } + + @Test + public void testRemoveOlapKeepsSharedTableAndOtherProperty() throws Exception { + OlapFixture fixture = new OlapFixture(); + fixture.add(5, IdGenerator.of("alice"), false); + fixture.add(8, IdGenerator.of("alice"), false); + fixture.store.removeOlapTable(IdGenerator.of(5)); + Assert.assertEquals(1, fixture.rows.size()); + Mockito.verify(fixture.session).commit(); + Mockito.verify(fixture.columns).close(); + Mockito.verify(fixture.sessions, Mockito.never()).dropTable(Mockito.any(String[].class)); + Assert.assertTrue(fixture.store.existOlapTable(IdGenerator.of(8))); + } + + @Test + public void testOlapCleanupCommitsBoundedBatches() throws Exception { + OlapFixture fixture = new OlapFixture(); + for (int i = 0; i < 1025; i++) { + fixture.add(5, IdGenerator.of(i + 1), false); + } + fixture.store.clearOlapTable(IdGenerator.of(5)); + Assert.assertTrue(fixture.rows.isEmpty()); + Mockito.verify(fixture.session, Mockito.atLeast(2)).commit(); + Mockito.verify(fixture.columns).close(); + } + + @Test + public void testOlapCleanupRollbackAndCloseOnCommitFailure() throws Exception { + OlapFixture fixture = new OlapFixture(); + fixture.add(5, IdGenerator.of("alice"), false); + RuntimeException failure = new IllegalStateException("commit refused"); + Mockito.doThrow(failure).when(fixture.session).commit(); + Assert.assertSame(failure, Assert.assertThrows(RuntimeException.class, + () -> fixture.store.clearOlapTable(IdGenerator.of(5)))); + Assert.assertEquals(1, fixture.rows.size()); + Mockito.verify(fixture.session).rollback(); + Mockito.verify(fixture.columns).close(); + } + + @Test + public void testOlapCleanupDoesNotCommitPendingGraphMutation() throws Exception { + OlapFixture fixture = new OlapFixture(); + fixture.pending.add("unrelated mutation"); + Assert.assertThrows(IllegalStateException.class, + () -> fixture.store.clearOlapTable(IdGenerator.of(5))); + Assert.assertEquals(1, fixture.pending.size()); + Mockito.verify(fixture.session, Mockito.never()).scan(Mockito.anyString()); + Mockito.verify(fixture.session, Mockito.never()).commit(); + Mockito.verify(fixture.session, Mockito.never()).rollback(); + } + + @Test + public void testOlapCleanupRejectsMismatchedCompoundKey() throws Exception { + OlapFixture fixture = new OlapFixture(); + byte[] key = fixture.add(8, IdGenerator.of("alice"), false); + fixture.rows.get(Base64.getEncoder().encodeToString(key)).value = + BytesBuffer.allocate(16).writeVInt(5).writeVInt(99).bytes(); + IllegalStateException failure = Assert.assertThrows(IllegalStateException.class, + () -> fixture.store.clearOlapTable(IdGenerator.of(5))); + Assert.assertTrue(failure.getMessage(), failure.getMessage().contains(Bytes.toHex(key))); + Assert.assertEquals(1, fixture.rows.size()); + Mockito.verify(fixture.session).rollback(); + Mockito.verify(fixture.columns).close(); + Mockito.verify(fixture.session, Mockito.never()).delete(Mockito.anyString(), + Mockito.any(byte[].class), Mockito.any(byte[].class)); + } + + @Test + public void testOlapCleanupRejectsMismatchedCompoundValue() throws Exception { + assertInvalidTargetValue(BytesBuffer.allocate(16).writeVInt(8).writeVInt(99).bytes()); + } + + @Test + public void testOlapCleanupRejectsEmptyTargetCompoundValue() throws Exception { + assertInvalidTargetValue(new byte[0]); + } + + @Test + public void testOlapCleanupRejectsUnreadableTargetCompoundValue() throws Exception { + assertInvalidTargetValue(new byte[]{(byte) 0x81}); + } + + @Test + public void testOlapCleanupBoundsInvalidKeyDiagnostic() throws Exception { + OlapFixture fixture = new OlapFixture(); + byte[] key = fixture.add(5, IdGenerator.of("alice".repeat(40)), false); + fixture.rows.get(Base64.getEncoder().encodeToString(key)).value = new byte[0]; + IllegalStateException failure = Assert.assertThrows(IllegalStateException.class, + () -> fixture.store.clearOlapTable(IdGenerator.of(5))); + String prefix = Bytes.toHex(Arrays.copyOf(key, 64)); + Assert.assertTrue(failure.getMessage(), failure.getMessage().contains(prefix + "...")); + Assert.assertFalse(failure.getMessage().contains(Bytes.toHex(key))); + Assert.assertEquals(1, fixture.rows.size()); + Mockito.verify(fixture.session).rollback(); + Mockito.verify(fixture.columns).close(); + } + + @Test + public void testOlapCleanupPreservesUnreadableOtherCompoundValue() throws Exception { + OlapFixture fixture = new OlapFixture(); + byte[] key = fixture.add(8, IdGenerator.of("alice"), false); + fixture.rows.get(Base64.getEncoder().encodeToString(key)).value = new byte[]{(byte) 0x81}; + fixture.store.clearOlapTable(IdGenerator.of(5)); + Assert.assertEquals(1, fixture.rows.size()); + Mockito.verify(fixture.columns).close(); + Mockito.verify(fixture.session, Mockito.never()).delete(Mockito.anyString(), + Mockito.any(byte[].class), Mockito.any(byte[].class)); + Mockito.verify(fixture.session, Mockito.never()).commit(); + } + + private static void assertInvalidTargetValue(byte[] value) throws Exception { + OlapFixture fixture = new OlapFixture(); + byte[] key = fixture.add(5, IdGenerator.of("alice"), false); + fixture.rows.get(Base64.getEncoder().encodeToString(key)).value = value; + IllegalStateException failure = Assert.assertThrows(IllegalStateException.class, + () -> fixture.store.clearOlapTable(IdGenerator.of(5))); + Assert.assertTrue(failure.getMessage(), failure.getMessage().contains(Bytes.toHex(key))); + Assert.assertEquals(1, fixture.rows.size()); + Mockito.verify(fixture.session).rollback(); + Mockito.verify(fixture.columns).close(); + Mockito.verify(fixture.session, Mockito.never()).delete(Mockito.anyString(), + Mockito.any(byte[].class), Mockito.any(byte[].class)); + Mockito.verify(fixture.session, Mockito.never()).commit(); + } + + @Test + public void testOlapExistsUsesSharedPhysicalTable() throws Exception { + OlapFixture fixture = new OlapFixture(); + Assert.assertTrue(fixture.store.existOlapTable(IdGenerator.of(5))); + Mockito.verify(fixture.sessions).existsTable(fixture.table); + } + + private static class OlapFixture { + + final HstoreSessions sessions = Mockito.mock(HstoreSessions.class); + final HstoreSessions.Session session = Mockito.mock(HstoreSessions.Session.class); + final BackendColumnIterator columns = Mockito.mock(BackendColumnIterator.class); + final HstoreGraphStore store = new HstoreGraphStore(Mockito.mock(BackendStoreProvider.class), "test", "g"); + final String table = new HstoreTables.OlapTable("g").table(); + final Map rows = new LinkedHashMap<>(); + final List pending = new ArrayList<>(); + + OlapFixture() throws Exception { + Field field = HstoreStore.class.getDeclaredField("sessions"); + field.setAccessible(true); + field.set(this.store, this.sessions); + Mockito.when(this.sessions.session()).thenReturn(this.session); + Mockito.when(this.session.opened()).thenReturn(true); + Mockito.when(this.sessions.existsTable(this.table)).thenReturn(true); + Mockito.when(this.session.scan(this.table)).thenAnswer(invocation -> { + Iterator iterator = new ArrayList<>(this.rows.values()).iterator(); + Mockito.when(this.columns.hasNext()).thenAnswer(ignored -> iterator.hasNext()); + Mockito.when(this.columns.next()).thenAnswer(ignored -> iterator.next()); + return this.columns; + }); + Mockito.when(this.session.hasChanges()).thenAnswer(invocation -> !this.pending.isEmpty()); + Mockito.doAnswer(invocation -> { + Assert.assertEquals(this.table, invocation.getArgument(0)); + this.pending.add(Base64.getEncoder().encodeToString(invocation.getArgument(2))); + return null; + }).when(this.session).delete(Mockito.anyString(), Mockito.any(byte[].class), Mockito.any(byte[].class)); + Mockito.when(this.session.commit()).thenAnswer(invocation -> { + int count = this.pending.size(); + this.pending.forEach(this.rows::remove); + this.pending.clear(); + return count; + }); + Mockito.doAnswer(invocation -> { + this.pending.clear(); + return null; + }).when(this.session).rollback(); + } + + byte[] add(int property, Id vertex, boolean legacy) { + byte[] key = legacy ? BytesBuffer.allocate(vertex.length() + 9).writeId(vertex).bytes() : + OlapKey.format(IdGenerator.of(property), vertex); + byte[] value = BytesBuffer.allocate(16).writeVInt(property).writeVInt(99).bytes(); + this.rows.put(Base64.getEncoder().encodeToString(key), BackendColumn.of(key, value)); + return key; + } + } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HstoreTableRoutingTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HstoreTableRoutingTest.java new file mode 100644 index 0000000000..c2fc108638 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HstoreTableRoutingTest.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.hugegraph.unit.core; + +import org.apache.hugegraph.backend.BinaryId; +import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; +import org.apache.hugegraph.backend.store.hstore.HstoreTable; +import org.apache.hugegraph.exception.NotSupportException; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.store.client.util.HgStoreClientConst; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.HugeKeys; +import org.junit.Test; + +/** + * Owner routing and argument validation of {@link HstoreTable} that can be + * exercised without a PD/Store cluster. The store client compares the + * all-partition owner by reference, so those cases use assertSame. + */ +public class HstoreTableRoutingTest { + + private static final Id MARKO = IdGenerator.of("1:marko"); + private static final Id JOSH = IdGenerator.of("1:josh"); + + private final RoutingTable table = new RoutingTable("Hugegraph", "G+V"); + + @Test + public void testTableNameCombinesDatabaseAndTableInLowerCase() { + Assert.assertEquals("hugegraph+g+v", this.table.table()); + } + + @Test + public void testOwnerOfVertexAndEdgeIds() { + Assert.assertArrayEquals(MARKO.asBytes(), this.table.ownerOf(MARKO)); + + // Both edge directions are routed to the vertex that owns the edge + Assert.assertArrayEquals(MARKO.asBytes(), this.table.ownerOf(edge(MARKO, JOSH, + Directions.OUT))); + Assert.assertArrayEquals(JOSH.asBytes(), this.table.ownerOf(edge(JOSH, MARKO, + Directions.IN))); + + // Serialized ids are unwrapped to their origin before routing + BinaryId binaryEdge = new BinaryId(new byte[]{1, 2, 3}, + edge(MARKO, JOSH, Directions.OUT)); + Assert.assertArrayEquals(MARKO.asBytes(), this.table.ownerOf(binaryEdge)); + + Assert.assertSame(HgStoreClientConst.ALL_PARTITION_OWNER, this.table.ownerOf(null)); + Assert.assertSame(HgStoreClientConst.ALL_PARTITION_OWNER, + this.table.ownerOf(new BinaryId(new byte[]{1}, null))); + } + + @Test + public void testOwnerByTypeRoutesOnlyGraphDataToSinglePartition() { + Id edgeId = edge(MARKO, JOSH, Directions.OUT); + Assert.assertArrayEquals(MARKO.asBytes(), this.table.ownerOf(HugeType.VERTEX, MARKO)); + Assert.assertArrayEquals(MARKO.asBytes(), this.table.ownerOf(HugeType.EDGE, edgeId)); + Assert.assertArrayEquals(MARKO.asBytes(), this.table.ownerOf(HugeType.EDGE_OUT, edgeId)); + Assert.assertArrayEquals(MARKO.asBytes(), this.table.ownerOf(HugeType.EDGE_IN, edgeId)); + Assert.assertArrayEquals(MARKO.asBytes(), this.table.ownerOf(HugeType.COUNTER, MARKO)); + + // Schema, index and system types must fan out to every partition + HugeType[] fanOut = {HugeType.VERTEX_LABEL, HugeType.PROPERTY_KEY, + HugeType.SECONDARY_INDEX, HugeType.VERTEX_LABEL_INDEX, + HugeType.RANGE_INT_INDEX, HugeType.TASK, HugeType.OLAP}; + for (HugeType type : fanOut) { + Assert.assertSame(type.name(), HgStoreClientConst.ALL_PARTITION_OWNER, + this.table.ownerOf(type, MARKO)); + } + } + + @Test + public void testInsertOwnerOfEntries() { + BinaryBackendEntry vertex = new BinaryBackendEntry(HugeType.VERTEX, + new BinaryId(new byte[]{9}, MARKO)); + Assert.assertArrayEquals(MARKO.asBytes(), this.table.getInsertOwner(vertex)); + + BinaryId edgeId = new BinaryId(new byte[]{8}, edge(JOSH, MARKO, Directions.IN)); + BinaryBackendEntry edge = new BinaryBackendEntry(HugeType.EDGE_IN, edgeId); + Assert.assertArrayEquals(JOSH.asBytes(), this.table.getInsertOwner(edge)); + Assert.assertArrayEquals(JOSH.asBytes(), this.table.getInsertEdgeOwner(edge)); + + // A label index entry with exactly one column is hashed by that column + Id indexId = IdGenerator.of("label-index"); + BinaryBackendEntry labelIndex = new BinaryBackendEntry(HugeType.VERTEX_LABEL_INDEX, + new BinaryId(new byte[]{7}, + indexId)); + labelIndex.column(new byte[]{1, 1}, new byte[0]); + Assert.assertArrayEquals(new byte[]{1, 1}, this.table.getInsertOwner(labelIndex)); + + // With more columns it falls back to the index id itself + labelIndex.column(new byte[]{2, 2}, new byte[0]); + Assert.assertArrayEquals(indexId.asBytes(), this.table.getInsertOwner(labelIndex)); + } + + @Test + public void testRemoveDirectionCondition() { + ConditionQuery onlyDirection = new ConditionQuery(HugeType.EDGE); + onlyDirection.eq(HugeKeys.DIRECTION, Directions.OUT); + Assert.assertNull(HstoreTable.removeDirectionCondition(onlyDirection)); + + ConditionQuery mixed = new ConditionQuery(HugeType.EDGE); + mixed.eq(HugeKeys.OWNER_VERTEX, MARKO); + mixed.eq(HugeKeys.DIRECTION, Directions.OUT); + ConditionQuery result = HstoreTable.removeDirectionCondition(mixed); + Assert.assertSame(mixed, result); + Assert.assertEquals(1, result.conditionsSize()); + Assert.assertEquals(MARKO, result.condition(HugeKeys.OWNER_VERTEX)); + Assert.assertNull(result.condition(HugeKeys.DIRECTION)); + + // A compound condition that also constrains another key is kept whole + ConditionQuery compound = new ConditionQuery(HugeType.EDGE); + compound.query(Condition.and(Condition.eq(HugeKeys.DIRECTION, Directions.IN), + Condition.eq(HugeKeys.LABEL, IdGenerator.of(1)))); + Assert.assertSame(compound, HstoreTable.removeDirectionCondition(compound)); + Assert.assertEquals(1, compound.conditionsSize()); + } + + @Test + public void testBytes2StringIsZeroPaddedLowerCaseHex() { + Assert.assertEquals("", HstoreTable.bytes2String(new byte[0])); + Assert.assertEquals("000f7f80ff", + HstoreTable.bytes2String(new byte[]{0x00, 0x0f, 0x7f, + (byte) 0x80, (byte) 0xff})); + } + + @Test + public void testSplitsMetadataValidatesArgumentsBeforeContactingPd() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + this.table.metaDispatcher().dispatchMetaHandler(null, "splits", new Object[0]); + }, e -> { + Assert.assertContains("args count of splits must be 1", e.getMessage()); + }); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + this.table.metaDispatcher().dispatchMetaHandler(null, "splits", + new Object[]{1024L}); + }, e -> { + Assert.assertContains("split-size must be >= 1048576 bytes, but got 1024", + e.getMessage()); + }); + + Assert.assertThrows(NotSupportException.class, () -> { + this.table.metaDispatcher().dispatchMetaHandler(null, "unknown-meta", + new Object[0]); + }); + } + + private static EdgeId edge(Id owner, Id other, Directions direction) { + return new EdgeId(owner, direction, IdGenerator.of(1), IdGenerator.of(1), "", other); + } + + private static class RoutingTable extends HstoreTable { + + RoutingTable(String database, String table) { + super(database, table); + } + + byte[] ownerOf(Id id) { + return this.getOwnerId(id); + } + + byte[] ownerOf(HugeType type, Id id) { + return this.getOwnerId(type, id); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugeElementViewTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugeElementViewTest.java new file mode 100644 index 0000000000..1255ac5754 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugeElementViewTest.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.unit.core; + +import java.util.Collection; +import java.util.Iterator; + +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.BaseEdge; +import org.apache.hugegraph.structure.BaseProperty; +import org.apache.hugegraph.structure.BaseVertex; +import org.apache.hugegraph.structure.HugeEdge; +import org.apache.hugegraph.structure.HugeProperty; +import org.apache.hugegraph.structure.HugeVertex; +import org.apache.hugegraph.structure.HugeVertexProperty; +import org.apache.hugegraph.type.define.Frequency; +import org.apache.hugegraph.type.define.IdStrategy; +import org.apache.hugegraph.unit.FakeObjects; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class HugeElementViewTest { + + @Test + public void testPropertyViewWrapsOnlyConsumedEntries() { + FakeObjects objects = new FakeObjects(); + PropertyKey name = objects.newPropertyKey(IdGenerator.of(1), "name"); + PropertyKey city = objects.newPropertyKey(IdGenerator.of(2), "city"); + VertexLabel label = objects.newVertexLabel(IdGenerator.of(1), "person", + IdStrategy.CUSTOMIZE_NUMBER, + name.id(), city.id()); + CountingVertex vertex = new CountingVertex(objects.graph(), label); + vertex.addProperty(name, "tom"); + vertex.wrapped = 0; + + Collection> properties = vertex.getProperties(); + Assert.assertEquals(1, properties.size()); + Assert.assertEquals(0, vertex.wrapped); + Iterator> iterator = properties.iterator(); + Assert.assertTrue(iterator.hasNext()); + Assert.assertEquals(0, vertex.wrapped); + Assert.assertEquals("tom", iterator.next().value()); + Assert.assertEquals(1, vertex.wrapped); + + vertex.addProperty(city, "Beijing"); + vertex.wrapped = 0; + Assert.assertEquals(2, properties.size()); + Assert.assertEquals(0, vertex.wrapped); + properties.clear(); + Assert.assertEquals(0, vertex.sizeOfProperties()); + Assert.assertEquals(0, vertex.wrapped); + } + + @Test + public void testPropertyViewIteratorRemovesSharedEntry() { + FakeObjects objects = new FakeObjects(); + PropertyKey name = objects.newPropertyKey(IdGenerator.of(1), "name"); + VertexLabel label = objects.newVertexLabel(IdGenerator.of(1), "person", + IdStrategy.CUSTOMIZE_NUMBER, name.id()); + CountingVertex vertex = new CountingVertex(objects.graph(), label); + vertex.addProperty(name, "tom"); + Iterator> iterator = vertex.getProperties().iterator(); + iterator.next(); + iterator.remove(); + Assert.assertFalse(vertex.hasProperty(name.id())); + Assert.assertFalse(vertex.element().hasProperty(name.id())); + } + + @Test + public void testFreshEngineStateViewsDoNotShareMutableEmptyProperties() { + FakeObjects objects = new FakeObjects(); + PropertyKey key = objects.newPropertyKey(IdGenerator.of(1), "name"); + VertexLabel label = objects.newVertexLabel(IdGenerator.of(1), "person", + IdStrategy.CUSTOMIZE_NUMBER, key.id()); + CountingVertex first = new CountingVertex(objects.graph(), label); + CountingVertex second = new CountingVertex(objects.graph(), label); + Collection> secondView = second.getProperties(); + Assert.assertEquals(0, secondView.size()); + Assert.assertFalse(secondView.iterator().hasNext()); + Assert.assertEquals(0, second.wrapped); + first.element().properties().put(1, new BaseProperty<>(key, "first")); + Assert.assertEquals("first", first.getPropertyValue(key.id())); + Assert.assertTrue(secondView.isEmpty()); + secondView.clear(); + Assert.assertEquals("first", first.getPropertyValue(key.id())); + Assert.assertEquals(0, second.wrapped); + } + + @Test + public void testAdoptedAdjacencyPreservesOwnerStateAndStableWrappers() { + FakeObjects objects = new FakeObjects(); + PropertyKey name = objects.newPropertyKey(IdGenerator.of(1), "name"); + VertexLabel label = objects.newVertexLabel(IdGenerator.of(1), "person", + IdStrategy.CUSTOMIZE_NUMBER, name.id()); + EdgeLabel link = objects.newEdgeLabel(IdGenerator.of(2), "link", Frequency.SINGLE, + label.id(), label.id()); + for (boolean out : new boolean[]{true, false}) { + BaseVertex owner = new BaseVertex(IdGenerator.of(1), label); + owner.addProperty(name, "tom"); + BaseEdge base = BaseEdge.constructEdge(objects.graph(), owner, out, link, "", IdGenerator.of(2)); + base.expiredTime(123); + HugeVertex vertex = new HugeVertex(objects.graph(), owner); + Assert.assertEquals(1, vertex.getEdges().size()); + HugeEdge edge = vertex.getEdges().iterator().next(); + Assert.assertSame(vertex, edge.ownerVertex()); + Assert.assertSame(base, edge.element()); + Assert.assertSame(owner.getProperty(name.id()), vertex.getProperty(name.id()).baseProperty()); + Assert.assertSame(edge, vertex.getEdges().iterator().next()); + Assert.assertEquals(out, edge.element().isOutEdge()); + Assert.assertEquals(123, edge.expiredTime()); + Assert.assertFalse(vertex.isPropLoaded()); + vertex.removeEdge(edge); + Assert.assertTrue(owner.edges().isEmpty()); + } + } + + @Test + public void testCanonicalSelfLoopAndCopyResetKeepMembershipConsistent() { + FakeObjects objects = new FakeObjects(); + VertexLabel label = objects.newVertexLabel(IdGenerator.of(1), "person", IdStrategy.CUSTOMIZE_NUMBER); + EdgeLabel link = objects.newEdgeLabel(IdGenerator.of(2), "link", Frequency.SINGLE, + label.id(), label.id()); + BaseVertex base = new BaseVertex(IdGenerator.of(1), label); + BaseEdge loop = new BaseEdge(link, true); + loop.vertices(true, base, base); + loop.name(""); + loop.assignId(); + base.addEdge(loop); + HugeVertex vertex = new HugeVertex(objects.graph(), base); + HugeEdge edge = vertex.getEdges().iterator().next(); + Assert.assertTrue(edge.selfLoop()); + Assert.assertSame(vertex, edge.sourceVertex()); + Assert.assertSame(vertex, edge.targetVertex()); + Assert.assertSame(vertex, edge.switchOwner().ownerVertex()); + HugeVertex copy = vertex.copy(); + copy.resetEdges(); + Assert.assertTrue(copy.getEdges().isEmpty()); + Assert.assertEquals(1, vertex.getEdges().size()); + Assert.assertEquals(1, base.edges().size()); + } + + @Test + public void testConstructedEdgeUsesOneAdjacencyMembershipAndOriginalOwner() { + FakeObjects objects = new FakeObjects(); + VertexLabel label = objects.newVertexLabel(IdGenerator.of(1), "person", IdStrategy.CUSTOMIZE_NUMBER); + EdgeLabel link = objects.newEdgeLabel(IdGenerator.of(2), "link", Frequency.SINGLE, + label.id(), label.id()); + HugeVertex vertex = new HugeVertex(objects.graph(), IdGenerator.of(1), label); + HugeEdge edge = HugeEdge.constructEdge(vertex, false, link, "", IdGenerator.of(2)); + Assert.assertSame(vertex, edge.ownerVertex()); + Assert.assertSame(edge, vertex.getEdges().iterator().next()); + Assert.assertEquals(1, vertex.element().edges().size()); + Assert.assertEquals(1, edge.otherVertex().element().edges().size()); + Assert.assertSame(edge.element(), vertex.element().edges().iterator().next()); + Assert.assertSame(edge.otherVertex(), edge.otherVertex().getEdges().iterator().next().ownerVertex()); + } + + @Test + public void testEdgeConstructionWithoutSchemaLookupRetainsEngineGraph() { + FakeObjects objects = new FakeObjects(); + VertexLabel label = objects.newVertexLabel(IdGenerator.of(1), "person", IdStrategy.CUSTOMIZE_NUMBER); + EdgeLabel link = objects.newEdgeLabel(IdGenerator.of(2), "link", Frequency.SINGLE, + label.id(), label.id()); + HugeVertex owner = new HugeVertex(objects.graph(), IdGenerator.of(1), label); + HugeEdge withoutLabel = HugeEdge.constructEdgeWithoutLabel(owner, true, "", IdGenerator.of(2)); + Assert.assertSame(owner, withoutLabel.ownerVertex()); + Assert.assertSame(label, owner.schemaLabel()); + Assert.assertTrue(withoutLabel.otherVertex().schemaLabel().undefined()); + Assert.assertEquals(VertexLabel.NONE.id(), withoutLabel.otherVertex().schemaLabel().id()); + Assert.assertSame(objects.graph(), withoutLabel.schemaLabel().graph()); + Assert.assertSame(objects.graph(), withoutLabel.otherVertex().schemaLabel().graph()); + Assert.assertEquals(1, owner.element().edges().size()); + + owner.resetEdges(); + HugeEdge withoutLookup = HugeEdge.constructEdgeWithoutGraph(owner, true, link, "", IdGenerator.of(2)); + Assert.assertSame(objects.graph(), withoutLookup.graph()); + Assert.assertSame(owner, withoutLookup.ownerVertex()); + Assert.assertTrue(withoutLookup.otherVertex().schemaLabel().undefined()); + Assert.assertSame(objects.graph(), withoutLookup.schemaLabel().graph()); + Assert.assertSame(objects.graph(), withoutLookup.ownerVertex().schemaLabel().graph()); + Assert.assertSame(objects.graph(), withoutLookup.otherVertex().schemaLabel().graph()); + Assert.assertEquals(1, owner.element().edges().size()); + Mockito.verify(objects.graph(), Mockito.never()).vertexLabelOrNone(Mockito.any()); + } + + private static final class CountingVertex extends HugeVertex { + + private int wrapped; + + private CountingVertex(HugeGraph graph, VertexLabel label) { + super(graph, IdGenerator.of(1), label); + } + + @Override + protected HugeVertexProperty wrapProperty(BaseProperty property) { + this.wrapped++; + return super.wrapProperty(property); + } + } +} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NotFoundException.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugeFeaturesTest.java similarity index 55% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NotFoundException.java rename to hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugeFeaturesTest.java index 7fb8c2317a..08f12e9fc8 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NotFoundException.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugeFeaturesTest.java @@ -15,23 +15,23 @@ * limitations under the License. */ -package org.apache.hugegraph.exception; +package org.apache.hugegraph.unit.core; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.structure.HugeFeatures; +import org.junit.Assert; +import org.junit.Test; -public class NotFoundException extends HugeException { +public class HugeFeaturesTest { - private static final long serialVersionUID = -5912665926327173032L; + @Test + public void testUniformListValueFeatureContract() { + HugeFeatures features = new HugeFeatures(null, true); - public NotFoundException(String message) { - super(message); - } - - public NotFoundException(String message, Object... args) { - super(message, args); - } - - public NotFoundException(String message, Throwable cause, Object... args) { - super(message, cause, args); + Assert.assertTrue(features.graph().variables() + .supportsUniformListValues()); + Assert.assertFalse(features.vertex().properties() + .supportsUniformListValues()); + Assert.assertFalse(features.edge().properties() + .supportsUniformListValues()); } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugePrimaryKeyTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugePrimaryKeyTest.java new file mode 100644 index 0000000000..d567fc922b --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/HugePrimaryKeyTest.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.hugegraph.unit.core; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.hugegraph.backend.tx.GraphTransaction; +import org.apache.hugegraph.config.CoreOptions; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.HugeVertex; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.hugegraph.type.define.IdStrategy; +import org.apache.hugegraph.unit.FakeObjects; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class HugePrimaryKeyTest { + + @Test + public void testEmptyPrimaryKeyStillRejected() { + Fixture fixture = new Fixture(true, ""); + IllegalArgumentException error = Assert.assertThrows(IllegalArgumentException.class, + () -> fixture.vertex.assignId(null)); + Assert.assertTrue(error.getMessage().contains("primary key can't be empty")); + } + + @Test + public void testReservedLeadingSymbolsStillRejected() { + for (char value = 0; value <= 3; value++) { + Fixture fixture = new Fixture(true, value + "value"); + IllegalArgumentException error = Assert.assertThrows(IllegalArgumentException.class, + () -> fixture.vertex.assignId(null)); + Assert.assertTrue(error.getMessage().contains("Illegal leading char")); + } + } + + @Test + public void testEnginePrimaryIdMatchesActualQueryOptimizer() { + Fixture escaped = new Fixture(true, "a!b", "left>right:tail`x"); + escaped.vertex.assignId(null); + Assert.assertEquals(escaped.vertex.id(), escaped.optimizedQueryId(false)); + Fixture numeric = new Fixture(true, 42); + numeric.vertex.assignId(null); + Assert.assertEquals(numeric.vertex.id(), numeric.optimizedQueryId(false)); + } + + @Test + public void testCompositeEmptyFieldRetainsLegacyEncodedName() { + Fixture fixture = new Fixture(true, "first", "", "a!b"); + fixture.vertex.assignId(null); + Assert.assertEquals("1:first!!a`!b", fixture.vertex.id().asString()); + Assert.assertEquals(fixture.vertex.id(), fixture.optimizedQueryId(true)); + } + + @Test + public void testNumberEncodingOptionRetainsPlainNumberMode() { + Fixture plain = new Fixture(false, 42); + plain.vertex.assignId(null); + Assert.assertEquals("1:42", plain.vertex.id().asString()); + Assert.assertEquals(plain.vertex.id(), plain.optimizedQueryId(true)); + Fixture encoded = new Fixture(true, 42); + encoded.vertex.assignId(null); + Assert.assertNotEquals(plain.vertex.id(), encoded.vertex.id()); + } + + private static final class Fixture { + + private final FakeObjects objects = new FakeObjects(); + private final List keys = new ArrayList<>(); + private final Object[] values; + private final boolean encodeNumber; + private final VertexLabel label; + private final HugeVertex vertex; + + private Fixture(boolean encodeNumber, Object... values) { + this.values = values; + this.encodeNumber = encodeNumber; + Mockito.doReturn(encodeNumber).when(this.objects.graph()).option(CoreOptions.VERTEX_ENCODE_PK_NUMBER); + for (int i = 0; i < values.length; i++) { + this.keys.add(this.objects.newPropertyKey(IdGenerator.of(i + 1), "key" + i, + values[i] instanceof Number ? DataType.INT : DataType.TEXT)); + } + Id[] ids = this.keys.stream().map(PropertyKey::id).toArray(Id[]::new); + this.label = this.objects.newVertexLabel(IdGenerator.of(1), "person", IdStrategy.PRIMARY_KEY, ids); + this.label.primaryKeys(ids); + this.vertex = new HugeVertex(this.objects.graph(), null, this.label); + for (int i = 0; i < values.length; i++) { + this.vertex.addProperty(this.keys.get(i), values[i]); + } + } + + private Id optimizedQueryId(boolean serializedValues) { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.eq(HugeKeys.LABEL, this.label.id()); + for (int i = 0; i < this.values.length; i++) { + Condition.Relation relation = Condition.eq(this.keys.get(i).id(), this.values[i]); + if (serializedValues) { + Object value = this.keys.get(i).serialValue(this.values[i], this.encodeNumber); + relation.serialValue("".equals(value) ? ConditionQuery.INDEX_VALUE_EMPTY : value); + } + query.query(relation); + } + GraphTransaction tx = Mockito.mock(GraphTransaction.class, Mockito.CALLS_REAL_METHODS); + Mockito.doReturn(this.objects.graph()).when(tx).graph(); + Query optimized = Whitebox.invoke(GraphTransaction.class, new Class[]{ConditionQuery.class}, + "optimizeQuery", tx, query); + return optimized.ids().iterator().next(); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/IdHolderTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/IdHolderTest.java index c6bdcc32d5..a4822307f5 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/IdHolderTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/IdHolderTest.java @@ -22,17 +22,17 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicReference; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.page.IdHolder.BatchIdHolder; import org.apache.hugegraph.backend.page.IdHolder.PagingIdHolder; import org.apache.hugegraph.backend.page.IdHolderList; import org.apache.hugegraph.backend.page.PageIds; import org.apache.hugegraph.backend.page.PageState; import org.apache.hugegraph.backend.page.QueryList; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.testutil.Assert; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/LoadingModeTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/LoadingModeTest.java new file mode 100644 index 0000000000..2431379c6e --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/LoadingModeTest.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.hugegraph.unit.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hugegraph.LoadingMode; +import org.apache.hugegraph.type.define.GraphMode; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.config.Property; +import org.junit.Assert; +import org.junit.Test; + +public class LoadingModeTest { + + @Test + public void testOwnerAuditRecordsOnlySuccessfulChangesWithoutTokens() { + List events = new ArrayList<>(); + AbstractAppender appender = new AbstractAppender("LoadingModeTestAppender", null, null, + true, Property.EMPTY_ARRAY) { + @Override + public void append(LogEvent event) { + events.add(event.toImmutable()); + } + }; + appender.start(); + LoggerContext context = (LoggerContext) LogManager.getContext(false); + org.apache.logging.log4j.core.config.Configuration configuration = context.getConfiguration(); + String loggerName = LoadingMode.class.getName(); + LoggerConfig previous = configuration.getLoggers().get(loggerName); + LoggerConfig logger = new LoggerConfig(loggerName, Level.INFO, false); + logger.addAppender(appender, Level.INFO, null); + configuration.addLogger(loggerName, logger); + context.updateLoggers(); + try { + AtomicReference graphName = new AtomicReference<>("test-space-test-graph"); + LoadingMode mode = new LoadingMode(graphName::get); + Map capability = mode.capability(); + String first = token(); + String second = token(); + update(mode, capability, first, "acquire"); + update(mode, capability, first, "acquire"); + update(mode, capability, second, "acquire"); + Assert.assertThrows(LoadingMode.ConflictException.class, + () -> mode.update("release", (String) capability.get("instance"), + 0, first, "other-user")); + update(mode, capability, first, "release"); + update(mode, capability, first, "release"); + update(mode, capability, second, "release"); + update(mode, capability, first, "acquire"); + Assert.assertEquals(4, events.size()); + Assert.assertTrue(message(events, 0).contains("mode NONE -> LOADING, epoch 0, activeOwners 1")); + Assert.assertTrue(message(events, 1).contains("mode LOADING -> LOADING, epoch 0, activeOwners 2")); + Assert.assertTrue(message(events, 2).contains("activeOwners 1, clearedOwners 1")); + Assert.assertTrue(message(events, 3).contains("mode LOADING -> NONE, epoch 0, activeOwners 0")); + update(mode, capability, token(), "acquire"); + update(mode, capability, token(), "acquire"); + Map current = mode.capability(); + mode.recover((String) current.get("instance"), 0, ((Number) current.get("revision")).longValue()); + Assert.assertTrue(message(events, 6).contains("mode LOADING -> NONE, epoch 1, " + + "activeOwners 0, clearedOwners 2")); + Assert.assertThrows(LoadingMode.ConflictException.class, + () -> mode.recover((String) capability.get("instance"), 0, 0)); + current = mode.capability(); + mode.recover((String) current.get("instance"), 1, ((Number) current.get("revision")).longValue()); + Assert.assertEquals(8, events.size()); + Assert.assertTrue(message(events, 7).contains("mode NONE -> NONE, epoch 2, " + + "activeOwners 0, clearedOwners 0")); + for (LogEvent event : events) { + Assert.assertEquals(Level.INFO, event.getLevel()); + Assert.assertTrue(event.getMessage().getFormattedMessage().contains( + "graph 'test-space-test-graph' instance " + capability.get("instance"))); + Assert.assertFalse(event.getMessage().getFormattedMessage().contains(first)); + Assert.assertFalse(event.getMessage().getFormattedMessage().contains(second)); + Assert.assertFalse(event.getMessage().getFormattedMessage().contains("other-user")); + } + graphName.set("renamed-space-test-graph"); + recover(mode, mode.capability()); + Assert.assertEquals(9, events.size()); + Assert.assertTrue(message(events, 8).contains("graph 'renamed-space-test-graph' instance " + + capability.get("instance"))); + } finally { + configuration.removeLogger(loggerName); + if (previous != null) { + configuration.addLogger(loggerName, previous); + } + context.updateLoggers(); + appender.stop(); + } + } + + private static String message(List events, int index) { + return events.get(index).getMessage().getFormattedMessage(); + } + + @Test + public void testTwoOwnersAndIdempotentRetry() { + LoadingMode mode = new LoadingMode(() -> "test-space-test-graph"); + Map capability = mode.capability(); + String a = token(); + String b = token(); + Assert.assertEquals("active", update(mode, capability, a, "acquire").get("state")); + update(mode, capability, a, "acquire"); + update(mode, capability, b, "acquire"); + update(mode, capability, a, "release"); + update(mode, capability, a, "release"); + Assert.assertEquals(GraphMode.LOADING, mode.mode()); + Assert.assertEquals("released", update(mode, capability, a, "acquire").get("state")); + update(mode, capability, b, "release"); + Assert.assertEquals(GraphMode.NONE, mode.mode()); + } + + @Test + public void testUnknownReleaseCancelsLateAcquire() { + LoadingMode mode = new LoadingMode(() -> "test-space-test-graph"); + Map capability = mode.capability(); + String token = token(); + update(mode, capability, token, "release"); + Assert.assertEquals("released", update(mode, capability, token, "acquire").get("state")); + Assert.assertEquals(GraphMode.NONE, mode.mode()); + } + + @Test + public void testTerminalCompactionKeepsOldActiveAndRejectsLateAcquire() { + LoadingMode mode = new LoadingMode(() -> "test-space-test-graph"); + Map original = mode.capability(); + String b = token(); + String cancelled = token(); + update(mode, original, b, "acquire"); + update(mode, original, cancelled, "release"); + for (int i = 0; i < 2200; i++) { + update(mode, mode.capability(), token(), "release"); + } + Assert.assertTrue(((Number) mode.capability().get("epoch")).longValue() >= 2); + Assert.assertEquals("released", update(mode, original, cancelled, "acquire").get("state")); + Assert.assertEquals("released", update(mode, original, token(), "acquire").get("state")); + Assert.assertEquals("active", update(mode, original, b, "acquire").get("state")); + Assert.assertEquals(GraphMode.LOADING, mode.mode()); + String c = token(); + Map current = mode.capability(); + update(mode, current, c, "acquire"); + update(mode, original, b, "release"); + Assert.assertEquals(GraphMode.LOADING, mode.mode()); + update(mode, current, c, "release"); + Assert.assertEquals(GraphMode.NONE, mode.mode()); + } + + @Test + public void testLegacySetterCannotOverwriteOwnersOrAdoptManualMode() { + LoadingMode mode = new LoadingMode(() -> "test-space-test-graph"); + Map capability = mode.capability(); + String a = token(); + for (GraphMode manual : GraphMode.values()) { + if (manual == GraphMode.NONE) { + continue; + } + mode.mode(manual); + Assert.assertThrows(IllegalStateException.class, () -> update(mode, capability, a, "acquire")); + Assert.assertEquals(manual, mode.mode()); + } + mode.mode(GraphMode.NONE); + update(mode, capability, a, "acquire"); + for (GraphMode legacy : GraphMode.values()) { + Assert.assertThrows(IllegalStateException.class, () -> mode.mode(legacy)); + Assert.assertEquals(GraphMode.LOADING, mode.mode()); + } + update(mode, capability, a, "release"); + mode.mode(GraphMode.NONE); + Assert.assertThrows(NullPointerException.class, () -> mode.mode(null)); + } + + @Test + public void testInstanceAndPrincipalBoundaries() { + LoadingMode mode = new LoadingMode(() -> "test-space-test-graph"); + Map capability = mode.capability(); + String a = token(); + update(mode, capability, a, "acquire"); + String instance = (String) capability.get("instance"); + Assert.assertThrows(IllegalStateException.class, + () -> mode.update("release", instance, 0, a, "another-user")); + Assert.assertEquals(GraphMode.LOADING, mode.mode()); + Assert.assertThrows(IllegalArgumentException.class, + () -> mode.update("acquire", token(), 0, a, "user")); + Assert.assertThrows(IllegalArgumentException.class, + () -> mode.update("acquire", instance, 1, a, "user")); + update(mode, capability, a, "release"); + } + + @Test + public void testRecoveryRetiresOwnersAndLateRequests() { + LoadingMode mode = new LoadingMode(() -> "test-space-test-graph"); + Map before = mode.capability(); + String lost = token(); + update(mode, before, lost, "acquire"); + + Map recovered = recover(mode, mode.capability()); + Assert.assertEquals("recovered", recovered.get("state")); + Assert.assertEquals(1L, recovered.get("epoch")); + Assert.assertFalse(recovered.containsKey("token")); + Assert.assertEquals(GraphMode.NONE, mode.mode()); + Assert.assertEquals("released", update(mode, before, lost, "acquire").get("state")); + Assert.assertEquals("released", update(mode, before, token(), "acquire").get("state")); + update(mode, before, lost, "release"); + + Map current = mode.capability(); + update(mode, current, token(), "acquire"); + Assert.assertThrows(IllegalStateException.class, + () -> recover(mode, before)); + Assert.assertEquals(GraphMode.LOADING, mode.mode()); + Assert.assertEquals(1L, mode.capability().get("epoch")); + } + + @Test + public void testRecoveryCannotClearOwnerAddedAfterInspection() { + LoadingMode mode = new LoadingMode(() -> "test-space-test-graph"); + Map capability = mode.capability(); + String first = token(); + String second = token(); + update(mode, capability, first, "acquire"); + Map inspected = mode.capability(); + update(mode, inspected, second, "acquire"); + Assert.assertThrows(IllegalStateException.class, + () -> recover(mode, inspected)); + Assert.assertEquals(GraphMode.LOADING, mode.mode()); + update(mode, capability, first, "release"); + Assert.assertEquals(GraphMode.LOADING, mode.mode()); + update(mode, capability, second, "release"); + Assert.assertEquals(GraphMode.NONE, mode.mode()); + } + + @Test + public void testRecoveryAllowsConfirmedManualModeReset() { + LoadingMode mode = new LoadingMode(() -> "test-space-test-graph"); + mode.mode(GraphMode.RESTORING); + Map before = mode.capability(); + recover(mode, before); + Assert.assertEquals(GraphMode.NONE, mode.mode()); + Assert.assertEquals("active", update(mode, mode.capability(), token(), "acquire").get("state")); + } + + @Test + public void testSuccessfulSameModeWriteInvalidatesRecoverySnapshot() { + LoadingMode mode = new LoadingMode(() -> "test-space-test-graph"); + for (GraphMode manual : GraphMode.values()) { + mode.mode(manual); + Map inspected = mode.capability(); + mode.mode(manual); + Assert.assertEquals(((Number) inspected.get("revision")).longValue() + 1, + mode.capability().get("revision")); + Assert.assertThrows(LoadingMode.ConflictException.class, () -> recover(mode, inspected)); + Assert.assertEquals(manual, mode.mode()); + } + } + + @Test + public void testOwnerAdmissionLimitPreservesExistingOwnersAndRecovery() { + LoadingMode mode = new LoadingMode(() -> "test-space-test-graph"); + Map capability = mode.capability(); + String first = token(); + update(mode, capability, first, "acquire"); + for (int i = 1; i < 1024; i++) { + update(mode, capability, token(), "acquire"); + } + Map full = mode.capability(); + String rejected = token(); + LoadingMode.ConflictException failure = Assert.assertThrows(LoadingMode.ConflictException.class, + () -> update(mode, capability, rejected, "acquire")); + Assert.assertTrue(failure.getMessage().contains("1024")); + Assert.assertEquals(full, mode.capability()); + Assert.assertEquals(GraphMode.LOADING, mode.mode()); + Assert.assertEquals("active", update(mode, capability, first, "acquire").get("state")); + Assert.assertEquals(full, mode.capability()); + Assert.assertEquals("released", update(mode, capability, first, "release").get("state")); + Assert.assertEquals("active", update(mode, capability, rejected, "acquire").get("state")); + Map recovered = recover(mode, mode.capability()); + Assert.assertEquals(GraphMode.NONE, recovered.get("mode")); + Assert.assertEquals("active", update(mode, mode.capability(), token(), "acquire").get("state")); + } + + @Test + public void testConcurrentAcquireAndLegacySetterAreAtomic() throws Exception { + LoadingMode mode = new LoadingMode(() -> "test-space-test-graph"); + Map capability = mode.capability(); + String a = token(); + CountDownLatch start = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future acquire = executor.submit(() -> { + Assert.assertTrue(start.await(5, TimeUnit.SECONDS)); + try { + update(mode, capability, a, "acquire"); + return true; + } catch (IllegalStateException expected) { + return false; + } + }); + Future legacy = executor.submit(() -> { + Assert.assertTrue(start.await(5, TimeUnit.SECONDS)); + try { + mode.mode(GraphMode.RESTORING); + return true; + } catch (IllegalStateException expected) { + return false; + } + }); + start.countDown(); + boolean acquired = acquire.get(10, TimeUnit.SECONDS); + Assert.assertNotEquals(acquired, legacy.get(10, TimeUnit.SECONDS)); + Assert.assertEquals(acquired ? GraphMode.LOADING : GraphMode.RESTORING, mode.mode()); + } finally { + executor.shutdownNow(); + Assert.assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } + } + + private static String token() { + return UUID.randomUUID().toString(); + } + + private static Map recover(LoadingMode mode, Map capability) { + return mode.recover((String) capability.get("instance"), + ((Number) capability.get("epoch")).longValue(), + ((Number) capability.get("revision")).longValue()); + } + + private static Map update(LoadingMode mode, Map capability, + String token, String action) { + return mode.update(action, (String) capability.get("instance"), + ((Number) capability.get("epoch")).longValue(), token, "user"); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/LocksTableTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/LocksTableTest.java index 00eb918211..2fded2bf6c 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/LocksTableTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/LocksTableTest.java @@ -22,8 +22,8 @@ import java.util.Set; import java.util.concurrent.locks.Lock; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.concurrent.LockManager; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/PageStateTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/PageStateTest.java index ba1b37f5c4..34f12f3a0f 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/PageStateTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/PageStateTest.java @@ -19,7 +19,7 @@ import java.util.Arrays; -import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.page.PageState; import org.apache.hugegraph.testutil.Assert; import org.junit.Test; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryResultsTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryResultsTest.java index 64c675f647..dc2c4f2233 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryResultsTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryResultsTest.java @@ -24,13 +24,13 @@ import java.util.List; import java.util.Set; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.page.PageInfo; import org.apache.hugegraph.backend.page.PageState; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.query.QueryBatch; import org.apache.hugegraph.backend.query.QueryResultContext; import org.apache.hugegraph.backend.query.QueryResults; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java index 5778ceba7c..49fc1b1810 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java @@ -17,16 +17,19 @@ package org.apache.hugegraph.unit.core; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.Aggregate.AggregateFunc; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.backend.query.IdPrefixQuery; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.query.IdRangeQuery; -import org.apache.hugegraph.backend.query.Query; -import org.apache.hugegraph.backend.query.Query.Order; +import java.lang.reflect.Modifier; +import java.util.List; + +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Aggregate.AggregateFunc; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdPrefixQuery; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.IdRangeQuery; +import org.apache.hugegraph.query.Query.Order; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.HugeKeys; @@ -38,6 +41,25 @@ public class QueryTest { + @Test + public void testContainsConditionOverloadVisibility() throws Exception { + Assert.assertThrows(NoSuchMethodException.class, + () -> ConditionQuery.class.getMethod("containsCondition", Object.class)); + Assert.assertTrue(Modifier.isPrivate(ConditionQuery.class + .getDeclaredMethod("containsCondition", Object.class).getModifiers())); + Assert.assertTrue(Modifier.isPublic(ConditionQuery.class + .getMethod("containsCondition", HugeKeys.class).getModifiers())); + Assert.assertTrue(Modifier.isPublic(ConditionQuery.class + .getMethod("containsCondition", Condition.RelationType.class).getModifiers())); + + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.eq(HugeKeys.LABEL, IdGenerator.of(1L)); + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertFalse(query.containsCondition(HugeKeys.NAME)); + Assert.assertTrue(query.containsCondition(Condition.RelationType.EQ)); + Assert.assertFalse(query.containsCondition(Condition.RelationType.IN)); + } + @Test public void testOrderBy() { Query query = new Query(HugeType.VERTEX); @@ -48,6 +70,19 @@ public void testOrderBy() { query.orders()); } + @Test + public void testConditionWithoutLabel() { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + + Assert.assertFalse(query.containsCondition(HugeKeys.LABEL)); + Assert.assertFalse(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.singleConditionValueOrNull(HugeKeys.LABEL)); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); + Assert.assertNull(query.condition(HugeKeys.LABEL)); + } + @Test public void testConditionWithEqAndIn() { Id label1 = IdGenerator.of(1); @@ -58,9 +93,67 @@ public void testConditionWithEqAndIn() { query.query(Condition.in(HugeKeys.LABEL, ImmutableList.of(label1, label2))); + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(label1), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertEquals(label1, + query.singleConditionValueOrNull(HugeKeys.LABEL)); + Assert.assertEquals(label1, query.conditionValue(HugeKeys.LABEL)); Assert.assertEquals(label1, query.condition(HugeKeys.LABEL)); } + @Test + public void testConditionWithSingleInValues() { + Id label1 = IdGenerator.of(1); + Id label2 = IdGenerator.of(2); + + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.query(Condition.in(HugeKeys.LABEL, + ImmutableList.of(label1, label2))); + + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(label1, label2), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.singleConditionValueOrNull(HugeKeys.LABEL)); + Assert.assertThrows(IllegalStateException.class, + () -> query.conditionValue(HugeKeys.LABEL), + e -> Assert.assertContains("Illegal key 'LABEL'", + e.getMessage())); + Assert.assertEquals(ImmutableList.of(label1, label2), + query.condition(HugeKeys.LABEL)); + } + + @Test + public void testConditionWithEmptyInValues() { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.query(Condition.in(HugeKeys.LABEL, ImmutableList.of())); + + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.singleConditionValueOrNull(HugeKeys.LABEL)); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableList.of(), + query.condition(HugeKeys.LABEL)); + } + + @Test + public void testConditionWithSingletonAndDuplicateInValues() { + Id label = IdGenerator.of(1); + for (List values : ImmutableList.of(ImmutableList.of(label), + ImmutableList.of(label, label))) { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.query(Condition.in(HugeKeys.LABEL, values)); + Assert.assertEquals(values, query.condition(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(label), query.conditionValues(HugeKeys.LABEL)); + Assert.assertEquals(label, query.conditionValue(HugeKeys.LABEL)); + Assert.assertEquals(label, query.singleConditionValueOrNull(HugeKeys.LABEL)); + } + } + @Test public void testConditionWithConflictingEqAndIn() { Id label1 = IdGenerator.of(1); @@ -73,6 +166,29 @@ public void testConditionWithConflictingEqAndIn() { query.query(Condition.in(HugeKeys.LABEL, ImmutableList.of(label1, label3))); + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.singleConditionValueOrNull(HugeKeys.LABEL)); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); + Assert.assertNull(query.condition(HugeKeys.LABEL)); + } + + @Test + public void testConditionWithNonEqInLabel() { + Id label = IdGenerator.of(1); + + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.neq(HugeKeys.LABEL, label); + + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertFalse(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertTrue(query.hasNeqCondition()); + Assert.assertEquals(ImmutableSet.of(), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.singleConditionValueOrNull(HugeKeys.LABEL)); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); Assert.assertNull(query.condition(HugeKeys.LABEL)); } @@ -89,6 +205,15 @@ public void testConditionWithMultipleMatchedInValues() { query.query(Condition.in(HugeKeys.LABEL, ImmutableList.of(label1, label2, label4))); + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(label1, label2), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.singleConditionValueOrNull(HugeKeys.LABEL)); + Assert.assertThrows(IllegalStateException.class, + () -> query.conditionValue(HugeKeys.LABEL), + e -> Assert.assertContains("Illegal key 'LABEL'", + e.getMessage())); Assert.assertThrows(IllegalStateException.class, () -> query.condition(HugeKeys.LABEL), e -> Assert.assertContains("Illegal key 'LABEL'", diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/RangeTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/RangeTest.java index 39b28784b9..d016fa07ab 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/RangeTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/RangeTest.java @@ -20,7 +20,7 @@ import java.util.List; import org.apache.hugegraph.backend.store.BackendTable.ShardSplitter.Range; -import org.apache.hugegraph.backend.store.Shard; +import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.testutil.Assert; import org.apache.logging.log4j.util.Strings; import org.junit.After; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/RolePermissionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/RolePermissionTest.java index 645b02e232..436b1aabb3 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/RolePermissionTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/RolePermissionTest.java @@ -17,7 +17,7 @@ package org.apache.hugegraph.unit.core; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.auth.HugePermission; import org.apache.hugegraph.auth.HugeProject; import org.apache.hugegraph.auth.HugeResource; @@ -27,8 +27,8 @@ import org.apache.hugegraph.auth.ResourceObject; import org.apache.hugegraph.auth.ResourceType; import org.apache.hugegraph.auth.RolePermission; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SchemaElementTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SchemaElementTest.java index 9853af107e..36f6f75f86 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SchemaElementTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SchemaElementTest.java @@ -17,23 +17,32 @@ package org.apache.hugegraph.unit.core; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.Userdata; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.EdgeLabelType; +import org.apache.hugegraph.type.define.Frequency; +import org.apache.hugegraph.type.define.IdStrategy; import org.apache.hugegraph.unit.FakeObjects; import org.apache.hugegraph.util.DateUtil; +import org.apache.hugegraph.util.JsonUtil; import org.junit.Test; public class SchemaElementTest { @@ -270,4 +279,73 @@ public void testBulkSetterRejectsNullUserdata() { Assert.assertContains("userdata", e.getMessage()); }); } + + @Test + public void testEdgeLabelMetadataJsonRoundTrip() { + for (EdgeLabelType type : new EdgeLabelType[]{EdgeLabelType.NORMAL, + EdgeLabelType.PARENT, + EdgeLabelType.SUB}) { + for (boolean multipleLinks : new boolean[]{false, true}) { + EdgeLabel label = new EdgeLabel(null, IdGenerator.of(3), "knows"); + label.edgeLabelType(type); + label.frequency(Frequency.SINGLE); + label.links(Pair.of(IdGenerator.of(1), IdGenerator.of(2))); + if (multipleLinks) { + label.links(Pair.of(IdGenerator.of(2), IdGenerator.of(4))); + } + if (type.sub()) { + label.fatherId(IdGenerator.of(5)); + } + String json = JsonUtil.toJson(label.asMap()); + Map decoded = JsonUtil.fromJson(json, Map.class); + Assert.assertEquals(!multipleLinks, decoded.containsKey(EdgeLabel.P.SOURCE_LABEL)); + Assert.assertEquals(!multipleLinks, decoded.containsKey(EdgeLabel.P.TARGET_LABEL)); + Assert.assertTrue(decoded.containsKey(EdgeLabel.P.LINKS)); + + // Metadata JSON key order must not select setter execution order. + List keys = new ArrayList<>(decoded.keySet()); + Collections.reverse(keys); + Map reordered = new LinkedHashMap<>(); + reordered.put(EdgeLabel.P.LINKS, decoded.get(EdgeLabel.P.LINKS)); + for (String key : keys) { + reordered.put(key, decoded.get(key)); + } + for (Map input : Arrays.asList(decoded, reordered)) { + EdgeLabel restored = EdgeLabel.fromMap(input, null); + Assert.assertEquals(label.links(), restored.links()); + Assert.assertEquals(label.edgeLabelType(), restored.edgeLabelType()); + Assert.assertEquals(label.fatherId(), restored.fatherId()); + Assert.assertEquals(label.frequency(), restored.frequency()); + Assert.assertEquals(label.status(), restored.status()); + Assert.assertEquals(label.asMap(), restored.asMap()); + } + } + } + } + + + @Test + public void testOrderedSchemaFieldsSurviveMetadataJson() { + for (int[] values : new int[][]{{17, 2}, {2, 17}, {2, 17, 2}}) { + VertexLabel vertex = new VertexLabel(null, IdGenerator.of(1), "person"); + vertex.idStrategy(IdStrategy.PRIMARY_KEY); + EdgeLabel edge = new EdgeLabel(null, IdGenerator.of(3), "knows"); + edge.frequency(Frequency.SINGLE); + edge.links(Pair.of(IdGenerator.of(1), IdGenerator.of(2))); + for (int value : values) { + vertex.primaryKey(IdGenerator.of(value)); + edge.sortKey(IdGenerator.of(value)); + } + String vertexJson = JsonUtil.toJson(vertex.asMap()); + String edgeJson = JsonUtil.toJson(edge.asMap()); + VertexLabel restoredVertex = VertexLabel.fromMap( + JsonUtil.fromJson(vertexJson, Map.class), null); + EdgeLabel restoredEdge = EdgeLabel.fromMap(JsonUtil.fromJson(edgeJson, Map.class), null); + Assert.assertEquals(vertex.primaryKeys(), restoredVertex.primaryKeys()); + Assert.assertEquals(edge.sortKeys(), restoredEdge.sortKeys()); + Assert.assertEquals(vertex.asMap(), restoredVertex.asMap()); + Assert.assertEquals(edge.asMap(), restoredEdge.asMap()); + } + } + } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SchemaTaskSecurityTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SchemaTaskSecurityTest.java new file mode 100644 index 0000000000..dcd9515d69 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SchemaTaskSecurityTest.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.hugegraph.unit.core; + +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.script.ScriptException; +import javax.script.SimpleBindings; + +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.backend.tx.SchemaTransactionV2; +import org.apache.hugegraph.config.CoreOptions; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.security.HugeSecurityManager; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.task.TaskScheduler; +import org.apache.tinkerpop.gremlin.groovy.jsr223.GremlinGroovyScriptEngine; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class SchemaTaskSecurityTest { + + @Test + public void testV2SchemaTasksCanCreateWorkerFromGremlin() throws Exception { + HugeGraph graph = Mockito.mock(HugeGraph.class); + TaskScheduler scheduler = Mockito.mock(TaskScheduler.class); + SchemaTransactionV2 transaction = Mockito.mock( + SchemaTransactionV2.class, Mockito.CALLS_REAL_METHODS); + Mockito.doReturn(graph).when(transaction).graph(); + Mockito.when(graph.taskScheduler()).thenReturn(scheduler); + Id taskId = IdGenerator.of(1L); + Mockito.when(graph.getNextId(Mockito.any())).thenReturn(taskId); + Mockito.when(graph.option(CoreOptions.TASK_SYNC_DELETION)).thenReturn(false); + AtomicInteger workers = new AtomicInteger(); + Mockito.doAnswer(invocation -> { + // Exercise the cold executor's thread-group and thread checks, + // without starting a worker or running a background schema job. + Thread worker = Executors.defaultThreadFactory().newThread(() -> { }); + Assert.assertEquals(Thread.State.NEW, worker.getState()); + workers.incrementAndGet(); + return null; + }).when(scheduler).schedule(Mockito.any()); + SimpleBindings bindings = new SimpleBindings(); + bindings.put("tx", transaction); + Id schemaId = IdGenerator.of(2L); + IndexLabel index = new IndexLabel(graph, schemaId, "by_age"); + Mockito.doReturn(new VertexLabel(graph, schemaId, "person")) + .when(transaction).getVertexLabel(schemaId); + Mockito.doReturn(new EdgeLabel(graph, schemaId, "knows")) + .when(transaction).getEdgeLabel(schemaId); + Mockito.doReturn(index).when(transaction).getIndexLabel(schemaId); + bindings.put("index", index); + bindings.put("schemaId", schemaId); + String[] scripts = {"tx.rebuildIndex(index)", "tx.removeVertexLabel(schemaId)", + "tx.removeEdgeLabel(schemaId)", "tx.removeIndexLabel(schemaId)"}; + GremlinGroovyScriptEngine engine = new GremlinGroovyScriptEngine(); + // Load Groovy/mock metadata before restricting filesystem access. + // Each schedule call still has to create a fresh worker Thread. + for (String script : scripts) { + engine.eval(script, bindings); + } + engine.eval("new Thread()"); + engine.eval("System.getProperties()"); + workers.set(0); + SecurityManager previous = System.getSecurityManager(); + String name = Thread.currentThread().getName(); + Thread.currentThread().setName("gremlin-server-exec-schema-security-test"); + System.setSecurityManager(new HugeSecurityManager()); + try { + for (String script : scripts) { + Assert.assertEquals(taskId, engine.eval(script, bindings)); + } + Assert.assertEquals(scripts.length, workers.get()); + ScriptException denied = Assert.assertThrows(ScriptException.class, + () -> engine.eval("new Thread()")); + Assert.assertTrue(denied.getMessage().contains( + "Not allowed to access thread group via Gremlin")); + denied = Assert.assertThrows(ScriptException.class, + () -> engine.eval("System.getProperties()")); + Assert.assertTrue(denied.getMessage().contains( + "Not allowed to access system properties via Gremlin")); + } finally { + System.setSecurityManager(previous); + Thread.currentThread().setName(name); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SecurityManagerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SecurityManagerTest.java index a30f35cb31..a39d1b138d 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SecurityManagerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SecurityManagerTest.java @@ -32,7 +32,7 @@ import java.util.Map; import java.util.concurrent.TimeoutException; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.auth.HugeFactoryAuthProxy; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ServerInfoManagerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ServerInfoManagerTest.java index 607d81de91..74cf096185 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ServerInfoManagerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/ServerInfoManagerTest.java @@ -17,10 +17,8 @@ package org.apache.hugegraph.unit.core; -import java.util.concurrent.ExecutorService; - import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.masterelection.GlobalMasterInfo; import org.apache.hugegraph.task.ServerInfoManager; import org.apache.hugegraph.testutil.Assert; @@ -44,24 +42,8 @@ public void setup() { Mockito.when(hugegraphParams.spaceGraphName()) .thenReturn("DEFAULT-hugegraph"); - ExecutorService executor = Mockito.mock(ExecutorService.class); - - this.sysGraphManager = new ServerInfoManager(sysGraphParams, executor); - this.hugegraphManager = new ServerInfoManager(hugegraphParams, executor); - } - - @Test - public void testInitDoesNotAccessBackendStore() { - HugeGraphParams graphParams = Mockito.mock(HugeGraphParams.class); - ExecutorService executor = Mockito.mock(ExecutorService.class); - ServerInfoManager manager = new ServerInfoManager(graphParams, executor); - - manager.init(); - - Mockito.verify(graphParams, Mockito.never()).systemTransaction(); - Mockito.verify(graphParams, Mockito.never()).backendStoreFeatures(); - Mockito.verify(graphParams, Mockito.never()).graph(); - Mockito.verify(graphParams, Mockito.never()).closeTx(); + this.sysGraphManager = new ServerInfoManager(sysGraphParams); + this.hugegraphManager = new ServerInfoManager(hugegraphParams); } @Test diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/StandardHugeGraphClearBackendTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/StandardHugeGraphClearBackendTest.java index f056c9291f..9aa9295f25 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/StandardHugeGraphClearBackendTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/StandardHugeGraphClearBackendTest.java @@ -17,7 +17,7 @@ package org.apache.hugegraph.unit.core; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.StandardHugeGraph; import org.apache.hugegraph.backend.cache.CachedSchemaTransactionV2; import org.apache.hugegraph.backend.store.BackendStore; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SystemSchemaStoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SystemSchemaStoreTest.java index 96a0ce791c..e345ed3a21 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SystemSchemaStoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/SystemSchemaStoreTest.java @@ -18,11 +18,11 @@ package org.apache.hugegraph.unit.core; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.store.SystemSchemaStore; -import org.apache.hugegraph.schema.SchemaElement; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; import org.junit.Test; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/TaskSchedulerServerInfoTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/TaskSchedulerServerInfoTest.java index 758b8c1f2d..8d6ea7db28 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/TaskSchedulerServerInfoTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/TaskSchedulerServerInfoTest.java @@ -23,8 +23,8 @@ import org.apache.commons.configuration2.PropertiesConfiguration; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.concurrent.PausableScheduledThreadPool; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; @@ -32,13 +32,20 @@ import org.apache.hugegraph.core.GraphManager; import org.apache.hugegraph.event.EventHub; import org.apache.hugegraph.exception.NotFoundException; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.task.DistributedTaskScheduler; import org.apache.hugegraph.task.HugeTask; import org.apache.hugegraph.task.TaskCallable; import org.apache.hugegraph.task.TaskStatus; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.IdStrategy; +import org.apache.hugegraph.type.define.NodeRole; +import org.apache.hugegraph.unit.FakeObjects; import org.apache.hugegraph.util.ExecutorUtil; +import org.apache.tinkerpop.gremlin.structure.Transaction; import org.junit.Test; import org.mockito.Mockito; @@ -63,13 +70,11 @@ public void testDistributedCheckRequirementDoesNotNeedServerInfo() { ExecutorService olapTaskExecutor = Executors.newSingleThreadExecutor(); ExecutorService gremlinTaskExecutor = Executors.newSingleThreadExecutor(); ExecutorService ephemeralTaskExecutor = Executors.newSingleThreadExecutor(); - ExecutorService serverInfoDbExecutor = Executors.newSingleThreadExecutor(); try { DistributedTaskScheduler scheduler = new DistributedTaskScheduler( params, schedulerExecutor, taskDbExecutor, schemaTaskExecutor, - olapTaskExecutor, gremlinTaskExecutor, ephemeralTaskExecutor, - serverInfoDbExecutor); + olapTaskExecutor, gremlinTaskExecutor, ephemeralTaskExecutor); scheduler.checkRequirement("schedule"); } finally { schedulerExecutor.shutdownNow(); @@ -78,7 +83,6 @@ public void testDistributedCheckRequirementDoesNotNeedServerInfo() { olapTaskExecutor.shutdownNow(); gremlinTaskExecutor.shutdownNow(); ephemeralTaskExecutor.shutdownNow(); - serverInfoDbExecutor.shutdownNow(); } } @@ -86,6 +90,7 @@ public void testDistributedCheckRequirementDoesNotNeedServerInfo() { public void testDistributedCancelTreatsMissingRetryTaskAsGone() { HugeGraph graph = Mockito.mock(HugeGraph.class); Mockito.when(graph.graphSpace()).thenReturn("DEFAULT"); + Mockito.when(graph.tx()).thenReturn(Mockito.mock(Transaction.class)); HugeGraphParams params = Mockito.mock(HugeGraphParams.class); Mockito.when(params.graph()).thenReturn(graph); @@ -101,13 +106,11 @@ public void testDistributedCancelTreatsMissingRetryTaskAsGone() { ExecutorService olapTaskExecutor = Executors.newSingleThreadExecutor(); ExecutorService gremlinTaskExecutor = Executors.newSingleThreadExecutor(); ExecutorService ephemeralTaskExecutor = Executors.newSingleThreadExecutor(); - ExecutorService serverInfoDbExecutor = Executors.newSingleThreadExecutor(); try { DistributedTaskScheduler scheduler = new DistributedTaskScheduler( params, schedulerExecutor, taskDbExecutor, schemaTaskExecutor, - olapTaskExecutor, gremlinTaskExecutor, ephemeralTaskExecutor, - serverInfoDbExecutor) { + olapTaskExecutor, gremlinTaskExecutor, ephemeralTaskExecutor) { @Override protected boolean updateStatus(Id id, TaskStatus prestatus, @@ -138,12 +141,11 @@ public Object call() { olapTaskExecutor.shutdownNow(); gremlinTaskExecutor.shutdownNow(); ephemeralTaskExecutor.shutdownNow(); - serverInfoDbExecutor.shutdownNow(); } } @Test - public void testGraphManagerDoesNotGenerateServerIdWhenElectionDisabled() { + public void testGraphManagerDoesNotGenerateServerId() { HugeConfig config = newConfig(); GraphManager manager = new GraphManager(config, new EventHub("test")); @@ -156,19 +158,40 @@ public void testGraphManagerDoesNotGenerateServerIdWhenElectionDisabled() { } @Test - public void testGraphManagerWarnsOnRoleElection() { + public void testGraphManagerIgnoresRemovedRoleElectionOptions() { + // Old rest-server.properties files may still set these keys PropertiesConfiguration conf = new PropertiesConfiguration(); - conf.setProperty(ServerOptions.ENABLE_SERVER_ROLE_ELECTION.name(), true); + conf.setProperty("server.role_election", true); + conf.setProperty("server.role.fail_count", 5); + conf.setProperty(ServerOptions.SERVER_ROLE.name(), "worker"); HugeConfig config = new HugeConfig(conf); GraphManager manager = new GraphManager(config, new EventHub("test")); try { - Assert.assertNotNull(manager); + Assert.assertEquals(NodeRole.WORKER, + manager.globalNodeRoleInfo().nodeRole()); } finally { manager.close(); } } + @Test + public void testLegacyServerLabelsKeepServerType() { + // Graphs created by older versions may still store these vertices + FakeObjects fakeObject = new FakeObjects(); + for (String name : new String[]{"~server", "~role_data"}) { + VertexLabel label = fakeObject.newVertexLabel(IdGenerator.of(name), name, + IdStrategy.CUSTOMIZE_STRING); + HugeVertex vertex = new HugeVertex(fakeObject.graph(), null, label); + Assert.assertEquals(HugeType.SERVER, vertex.type()); + } + + VertexLabel person = fakeObject.newVertexLabel(IdGenerator.of(1), "person", + IdStrategy.CUSTOMIZE_STRING); + HugeVertex vertex = new HugeVertex(fakeObject.graph(), null, person); + Assert.assertEquals(HugeType.VERTEX, vertex.type()); + } + @Test public void testGraphManagerDoesNotInjectPdPeersForStandaloneRocksDB() { HugeConfig serverConfig = newConfig(); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/TraversalUtilTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/TraversalUtilTest.java index 08322112b7..fe664e1003 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/TraversalUtilTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/TraversalUtilTest.java @@ -17,7 +17,7 @@ package org.apache.hugegraph.unit.core; -import org.apache.hugegraph.HugeException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.traversal.optimize.TraversalUtil; import org.apache.hugegraph.util.DateUtil; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/EdgeIdTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/EdgeIdTest.java index 0c917832f8..d36fe413a8 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/EdgeIdTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/EdgeIdTest.java @@ -19,9 +19,9 @@ import java.util.Set; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.unit.BaseUnitTest; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/IdTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/IdTest.java index 8bd772a3db..c695e80f26 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/IdTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/IdTest.java @@ -20,9 +20,9 @@ import java.nio.ByteBuffer; import java.util.UUID; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.Id.IdType; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.Id.IdType; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.unit.BaseUnitTest; import org.apache.hugegraph.util.NumericUtil; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/IdUtilTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/IdUtilTest.java index 473dc61891..38d1e4d876 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/IdUtilTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/IdUtilTest.java @@ -19,10 +19,10 @@ import java.nio.ByteBuffer; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.id.IdUtil; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.IdUtil; import org.junit.Assert; import org.junit.Test; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/SplicingIdGeneratorTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/SplicingIdGeneratorTest.java index bc7f97ccfb..50214b650b 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/SplicingIdGeneratorTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/id/SplicingIdGeneratorTest.java @@ -19,17 +19,14 @@ import java.util.List; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.id.SplicingIdGenerator; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; -import org.apache.hugegraph.structure.HugeVertex; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.BaseVertex; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.define.IdStrategy; -import org.apache.hugegraph.unit.FakeObjects; import org.junit.Test; -import org.mockito.Mockito; import com.google.common.collect.ImmutableList; @@ -37,15 +34,9 @@ public class SplicingIdGeneratorTest { @Test public void testGenerate() { - FakeObjects fakeObjects = new FakeObjects(); - PropertyKey name = fakeObjects.newPropertyKey(IdGenerator.of(1), - "name"); - VertexLabel vertexLabel = fakeObjects.newVertexLabel( - IdGenerator.of(1L), "fake", - IdStrategy.PRIMARY_KEY, name.id()); - HugeVertex vertex = Mockito.mock(HugeVertex.class); - Mockito.when(vertex.schemaLabel()).thenReturn(vertexLabel); - Mockito.when(vertex.name()).thenReturn("marko"); + VertexLabel vertexLabel = new VertexLabel(null, IdGenerator.of(1L), "fake"); + vertexLabel.idStrategy(IdStrategy.PRIMARY_KEY); + BaseVertex vertex = new BaseVertex(IdGenerator.of("1:marko"), vertexLabel); Id vid = SplicingIdGenerator.instance().generate(vertex); Assert.assertEquals(IdGenerator.of("1:marko"), vid); } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/opencypher/CypherGremlinPredicatesTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/opencypher/CypherGremlinPredicatesTest.java new file mode 100644 index 0000000000..5bf7d6d0d9 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/opencypher/CypherGremlinPredicatesTest.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.hugegraph.unit.opencypher; + +import java.util.Collections; +import java.util.List; + +import javax.script.Bindings; +import javax.script.ScriptException; + +import org.apache.hugegraph.opencypher.CypherGremlinPredicates; +import org.apache.hugegraph.opencypher.CypherPlugin; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.unit.BaseUnitTest; +import org.apache.tinkerpop.gremlin.groovy.jsr223.GremlinGroovyScriptEngine; +import org.apache.tinkerpop.gremlin.jsr223.Customizer; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; +import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph; +import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdge; +import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertex; +import org.junit.Test; +import org.opencypher.gremlin.translation.CypherAst; +import org.opencypher.gremlin.translation.StatementOption; +import org.opencypher.gremlin.translation.groovy.GroovyPredicate; +import org.opencypher.gremlin.translation.translator.Translator; + +public class CypherGremlinPredicatesTest extends BaseUnitTest { + + @Test + public void testRegexUsesWholeStringMatching() { + CypherGremlinPredicates predicates = new CypherGremlinPredicates(); + Assert.assertTrue(predicates.regexMatch("m.*o").test("marko")); + Assert.assertFalse(predicates.regexMatch("ark").test("marko")); + Assert.assertTrue(predicates.regexMatch("marko").test("marko")); + Assert.assertFalse(predicates.regexMatch("marko").negate().test("marko")); + Assert.assertTrue(predicates.regexMatch("marko").negate().test("peter")); + } + + @Test + public void testNodeRelationshipAndStringPredicates() { + CypherGremlinPredicates predicates = new CypherGremlinPredicates(); + ReferenceVertex source = new ReferenceVertex("source", "person"); + ReferenceVertex target = new ReferenceVertex("target", "person"); + ReferenceEdge edge = new ReferenceEdge("edge", "knows", source, target); + Assert.assertTrue(predicates.isNode().test(source)); + Assert.assertFalse(predicates.isNode().test(edge)); + Assert.assertTrue(predicates.isRelationship().test(edge)); + Assert.assertFalse(predicates.isRelationship().test(source)); + Assert.assertTrue(predicates.isString().test("marko")); + Assert.assertFalse(predicates.isString().test(42)); + Assert.assertFalse(predicates.isString().test(source)); + } + + @Test + public void testPublicFactoriesThroughPluginImports() throws ScriptException { + GremlinGroovyScriptEngine engine = pluginEngine(); + Bindings bindings = engine.createBindings(); + ReferenceVertex source = new ReferenceVertex("source", "person"); + ReferenceVertex target = new ReferenceVertex("target", "person"); + bindings.put("vertex", source); + bindings.put("edge", new ReferenceEdge("edge", "knows", source, target)); + Assert.assertEquals(true, engine.eval("cypherRegex('mar.*').test('marko')", bindings)); + Assert.assertEquals(false, engine.eval("cypherRegex('ark').test('marko')", bindings)); + Assert.assertEquals(true, engine.eval("cypherIsNode().test(vertex)", bindings)); + Assert.assertEquals(false, engine.eval("cypherIsNode().test(edge)", bindings)); + Assert.assertEquals(true, engine.eval("cypherIsRelationship().test(edge)", bindings)); + Assert.assertEquals(false, engine.eval("cypherIsRelationship().test(vertex)", bindings)); + Assert.assertEquals(true, engine.eval("cypherIsString().test('marko')", bindings)); + Assert.assertEquals(false, engine.eval("cypherIsString().test(42)", bindings)); + } + + @Test + public void testExplainGremlinReplaysUsingPluginImports() throws ScriptException { + CypherAst ast = CypherAst.parse("EXPLAIN UNWIND ['marko', 'peter'] AS name " + + "WITH name WHERE name =~ 'mar.*' RETURN name"); + Assert.assertTrue(ast.getOptions().contains(StatementOption.EXPLAIN)); + Translator translator = Translator.builder() + .gremlinGroovy() + .build("gremlin+cfog_server_extensions+" + + "inline_parameters"); + String gremlin = ast.buildTranslation(translator); + Assert.assertTrue(gremlin.contains("cypherRegex(")); + GremlinGroovyScriptEngine engine = pluginEngine(); + Bindings bindings = engine.createBindings(); + bindings.put("g", EmptyGraph.instance().traversal()); + GraphTraversal traversal = (GraphTraversal) engine.eval(gremlin, bindings); + List result = traversal.toList(); + Assert.assertEquals(Collections.singletonList(Collections.singletonMap("name", "marko")), + result); + } + + private static GremlinGroovyScriptEngine pluginEngine() { + Customizer[] customizers = CypherPlugin.instance().getCustomizers("gremlin-groovy").get(); + return new GremlinGroovyScriptEngine(customizers); + } + +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBCountersTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBCountersTest.java index cd42f2bccf..d7e5fc38a3 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBCountersTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBCountersTest.java @@ -21,8 +21,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions.Session; import org.apache.hugegraph.backend.store.rocksdb.RocksDBTables; import org.apache.hugegraph.testutil.Assert; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBPerfTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBPerfTest.java index 4256f290e3..dec44b917e 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBPerfTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBPerfTest.java @@ -24,7 +24,7 @@ import java.util.Map; import java.util.Random; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions.Session; import org.junit.Test; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBSessionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBSessionTest.java index 16b87d9a1c..700e466453 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBSessionTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBSessionTest.java @@ -25,7 +25,7 @@ import java.util.Map; import java.util.Random; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions.Session; import org.apache.hugegraph.testutil.Assert; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBSessionsTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBSessionsTest.java index 5dbc96c5c6..314406838f 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBSessionsTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBSessionsTest.java @@ -19,24 +19,479 @@ import java.io.File; import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.io.FileUtils; import org.apache.hugegraph.backend.store.rocksdb.RocksDBMetrics; import org.apache.hugegraph.backend.store.rocksdb.RocksDBOptions; import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions; import org.apache.hugegraph.backend.store.rocksdb.RocksDBStdSessions; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBStore; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBStoreProvider; import org.apache.hugegraph.backend.store.rocksdbsst.RocksDBSstSessions; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.unit.FakeObjects; import org.junit.Test; +import org.mockito.AdditionalAnswers; +import org.mockito.Mockito; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksIterator; import org.rocksdb.RocksDBException; +import org.rocksdb.WriteBatch; +import org.rocksdb.WriteOptions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; public class RocksDBSessionsTest extends BaseRocksDBUnitTest { + @Test + public void testResetWaitsForNativeCloseAndSkipsRetainedClosedSession() throws Exception { + CountDownLatch disposed = new CountDownLatch(1); + CountDownLatch finishClose = new CountDownLatch(1); + CountDownLatch resetStarted = new CountDownLatch(1); + CountDownLatch resetFinished = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + AtomicReference closing = new AtomicReference<>(); + Thread closer = new Thread(() -> { + try { + RocksDBSessions.Session session = this.rocks.session(); + closing.set(session); + WriteBatch previous = Whitebox.getInternalState(session, "batch"); + previous.close(); + WriteBatch guarded = new WriteBatch() { + @Override + public void close() { + super.close(); + disposed.countDown(); + try { + Assert.assertTrue(finishClose.await(30, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } + } + + @Override + public void clear() { + // Trap the bad interleaving before calling a freed JNI address. + Assert.assertTrue("Reset touched a disposed native batch", this.isOwningHandle()); + super.clear(); + } + }; + Whitebox.setInternalState(session, "batch", guarded); + this.rocks.close(); + } catch (Throwable e) { + failure.compareAndSet(null, e); + disposed.countDown(); + } + }, "session-close"); + Thread resetter = new Thread(() -> { + resetStarted.countDown(); + try { + this.rocks.forceResetSessions(); + } catch (Throwable e) { + failure.compareAndSet(null, e); + } finally { + resetFinished.countDown(); + } + }, "session-reset"); + closer.start(); + try { + Assert.assertTrue(disposed.await(30, TimeUnit.SECONDS)); + Assert.assertNull(failure.get()); + // Close is still running and the pool still exposes this session. + Assert.assertTrue(closing.get().opened()); + resetter.start(); + Assert.assertTrue(resetStarted.await(30, TimeUnit.SECONDS)); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30); + while (resetter.getState() != Thread.State.BLOCKED && resetFinished.getCount() != 0 && + System.nanoTime() < deadline) { + Thread.sleep(1L); + } + Assert.assertNull(failure.get()); + Assert.assertEquals(Thread.State.BLOCKED, resetter.getState()); + } finally { + finishClose.countDown(); + closer.join(TimeUnit.SECONDS.toMillis(30)); + resetter.join(TimeUnit.SECONDS.toMillis(30)); + } + Assert.assertFalse(closer.isAlive()); + Assert.assertFalse(resetter.isAlive()); + Assert.assertNull(failure.get()); + Assert.assertTrue(closing.get().closed()); + // A reference retained before pool removal must remain safe to reset. + closing.get().reset(); + this.put("after-close", "usable"); + Assert.assertEquals("usable", this.get("after-close")); + } + + @Test + public void testResetDiscardsPendingWritesWithoutLeakingNativeBatch() throws Exception { + RocksDBSessions.Session session = this.rocks.session(); + WriteBatch previous = Whitebox.getInternalState(session, "batch"); + for (int i = 0; i < 32; i++) { + session.put(TABLE, getBytes("pending"), getBytes("value")); + this.rocks.forceResetSessions(); + WriteBatch current = Whitebox.getInternalState(session, "batch"); + Assert.assertFalse(session.hasChanges()); + Assert.assertTrue(current.isOwningHandle()); + Assert.assertTrue(current == previous || !previous.isOwningHandle()); + previous = current; + } + session.put(TABLE, getBytes("retained"), getBytes("value")); + session.commit(); + Assert.assertNull(this.get("pending")); + Assert.assertEquals("value", this.get("retained")); + } + + @Test + public void testFinalDetachDisposesNativeOwners() throws Exception { + RocksDBSessions.Session session = this.rocks.session(); + WriteBatch batch = Whitebox.getInternalState(session, "batch"); + WriteOptions options = Whitebox.getInternalState(session, "writeOptions"); + // Keep the DB alive while this worker releases its final request lease. + CountDownLatch attached = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + Thread keeper = new Thread(() -> { + try { + this.rocks.session(); + attached.countDown(); + release.await(); + } catch (Throwable e) { + failure.set(e); + } finally { + attached.countDown(); + try { + this.rocks.close(); + } catch (Throwable e) { + failure.compareAndSet(null, e); + } + } + }); + keeper.start(); + try { + Assert.assertTrue(attached.await(30, TimeUnit.SECONDS)); + Assert.assertNull(failure.get()); + Assert.assertSame(session, this.rocks.useSession()); + Assert.assertFalse(this.rocks.close()); + Assert.assertTrue(batch.isOwningHandle()); + Assert.assertTrue(options.isOwningHandle()); + session.put(TABLE, getBytes("lease"), getBytes("retained")); + session.commit(); + Assert.assertFalse(this.rocks.close()); + Assert.assertFalse(batch.isOwningHandle()); + Assert.assertFalse(options.isOwningHandle()); + // Native close is idempotent even after the pool removed this session. + session.close(); + Assert.assertFalse(batch.isOwningHandle()); + Assert.assertFalse(options.isOwningHandle()); + for (int i = 0; i < 32; i++) { + RocksDBSessions.Session request = this.rocks.session(); + Assert.assertNotSame(session, request); + WriteBatch requestBatch = Whitebox.getInternalState(request, "batch"); + WriteOptions requestOptions = Whitebox.getInternalState(request, "writeOptions"); + Assert.assertTrue(requestBatch.isOwningHandle()); + Assert.assertTrue(requestOptions.isOwningHandle()); + Assert.assertArrayEquals(getBytes("retained"), request.get(TABLE, getBytes("lease"))); + request.put(TABLE, getBytes("request"), getBytes("value")); + request.commit(); + Assert.assertFalse(this.rocks.close()); + Assert.assertFalse(requestBatch.isOwningHandle()); + Assert.assertFalse(requestOptions.isOwningHandle()); + } + } finally { + // Leave the fixture's main-worker session open for ordinary teardown. + this.rocks.session(); + release.countDown(); + keeper.join(TimeUnit.SECONDS.toMillis(30)); + } + Assert.assertFalse(keeper.isAlive()); + Assert.assertNull(failure.get()); + Assert.assertEquals("retained", this.get("lease")); + } + + @Test + public void testAdapterToplingTruncateWithMultipleKeys() throws Exception { + this.assertAdapterTruncate(true); + } + + @Test + public void testAdapterStandardTruncateWithMultipleKeys() throws Exception { + this.assertAdapterTruncate(false); + } + + @Test + public void testAdapterToplingTruncateDiscardsPendingWritesInEmptyTable() throws Exception { + this.assertAdapterTruncateDiscardsPendingWrites(true, false); + } + + @Test + public void testAdapterToplingTruncateDiscardsPendingWritesOutsideRange() throws Exception { + this.assertAdapterTruncateDiscardsPendingWrites(true, true); + } + + @Test + public void testAdapterStandardTruncateDiscardsPendingWritesInEmptyTable() throws Exception { + this.assertAdapterTruncateDiscardsPendingWrites(false, false); + } + + @Test + public void testAdapterStandardTruncateDiscardsPendingWritesOutsideRange() throws Exception { + this.assertAdapterTruncateDiscardsPendingWrites(false, true); + } + + @Test + public void testAdapterToplingTruncatePreservesBackendVersion() throws Exception { + this.assertAdapterTruncatePreservesBackendVersion(true); + } + + @Test + public void testAdapterStandardTruncatePreservesBackendVersion() throws Exception { + this.assertAdapterTruncatePreservesBackendVersion(false); + } + + @Test + public void testToplingTruncateRoutesDynamicOlapTablesToTheirDatabase() throws Exception { + String dynamic = "graph+ap_123"; + String olapPath = DB_PATH + "/independent-olap"; + RocksDBStdSessions olap = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "olap", olapPath, olapPath); + try { + olap.createTable(dynamic); + olap.session().put(dynamic, getBytes("key"), getBytes("olap value")); + olap.session().commit(); + this.put("main", "main value"); + RocksDBStore store = new RocksDBStore.RocksDBGraphStore(null, "db", "graph") { + @Override + protected List tableNames() { + return ImmutableList.of(TABLE, dynamic); + } + + @Override + protected List olapTables() { + return ImmutableList.of(dynamic); + } + + @Override + protected Map tableDBMapping() { + return java.util.Collections.singletonMap(HugeType.OLAP.string(), olap); + } + }; + Whitebox.setInternalState(store, "sessions", this.rocks); + Map databases = Whitebox.getInternalState(store, "dbs"); + databases.put(DB_PATH, this.rocks); + databases.put(olapPath, olap); + Whitebox.setInternalState(store, "toplingProvider", true); + // Real standard JNI exercises the Topling Java routing branch. + Assert.assertFalse(this.rocks.existsTable(dynamic)); + store.truncate(); + Assert.assertNull(this.get("main")); + Assert.assertTrue(olap.existsTable(dynamic)); + Assert.assertNull(olap.session().get(dynamic, getBytes("key"))); + olap.session().put(dynamic, getBytes("next"), getBytes("after truncate")); + olap.session().commit(); + Assert.assertArrayEquals(getBytes("after truncate"), olap.session().get(dynamic, getBytes("next"))); + } finally { + olap.close(); + } + } + + private void assertAdapterTruncatePreservesBackendVersion(boolean topling) throws Exception { + RocksDBStoreProvider provider = new RocksDBStoreProvider(); + RocksDBStore store = new RocksDBStore.RocksDBSystemStore(provider, "db", "store") { + @Override + protected List tableNames() { + return ImmutableList.builder().addAll(super.tableNames()).add(TABLE).build(); + } + }; + Whitebox.setInternalState(store, "sessions", this.rocks); + Map databases = Whitebox.getInternalState(store, "dbs"); + databases.put(DB_PATH, this.rocks); + Whitebox.setInternalState(store, "toplingProvider", topling); + store.init(); + this.put("old", "before truncate"); + Assert.assertEquals(provider.driverVersion(), store.storedVersion()); + + store.truncate(); + + Assert.assertNull(this.get("old")); + Assert.assertEquals(provider.driverVersion(), store.storedVersion()); + this.put("new", "after truncate"); + this.rocks.close(); + this.rocks = new RocksDBStdSessions(FakeObjects.newConfig(), "db", "store", + DB_PATH, DB_PATH, ImmutableList.of(TABLE)); + this.rocks.session(); + Whitebox.setInternalState(store, "sessions", this.rocks); + databases.clear(); + databases.put(DB_PATH, this.rocks); + // Reopening must read the persisted version without init repairing it. + Assert.assertEquals(provider.driverVersion(), store.storedVersion()); + Assert.assertEquals("after truncate", this.get("new")); + } + + private void assertAdapterTruncateDiscardsPendingWrites(boolean topling, boolean committedRange) throws Exception { + if (committedRange) { + this.put("m", "committed first"); + this.put("n", "committed last"); + } + RocksDBSessions.Session session = this.rocks.session(); + // Both inserts fall outside the committed range; an empty CF has no range at all. + session.put(TABLE, getBytes("a"), getBytes("pending first")); + session.put(TABLE, getBytes("z"), getBytes("pending last")); + Assert.assertTrue(session.hasChanges()); + + RocksDBStore store = this.adapterStore(topling, ImmutableList.of(TABLE)); + store.truncate(); + + Assert.assertFalse(session.hasChanges()); + Assert.assertNull(session.keyRange(TABLE)); + Assert.assertNull(session.get(TABLE, getBytes("a"))); + Assert.assertNull(session.get(TABLE, getBytes("z"))); + // The retained session must not replay pending writes after truncate either. + session.commit(); + Assert.assertNull(session.keyRange(TABLE)); + this.put("new", "after truncate"); + Assert.assertEquals("after truncate", this.get("new")); + } + + private void assertAdapterTruncate(boolean topling) throws Exception { + List tables = ImmutableList.of(TABLE, "single", "empty", "multiple"); + this.rocks.createTable("single", "empty", "multiple"); + // Deliberately unordered, including empty and unsigned byte boundaries. + byte[][] keys = {new byte[]{(byte) 0xff}, new byte[]{0}, new byte[]{}, + new byte[]{(byte) 0x80}, new byte[]{0, (byte) 0xff}, new byte[]{0x7f}}; + for (String table : ImmutableList.of(TABLE, "multiple")) { + for (byte[] key : keys) { + this.rocks.session().put(table, key, new byte[]{42}); + } + } + this.rocks.session().put("single", new byte[]{(byte) 0xff}, new byte[]{43}); + this.commit(); + AtomicReference owner = Whitebox.getInternalState(this.rocks, "rocksdb"); + Object opened = owner.get(); + Map handles = Whitebox.getInternalState(opened, "cfHandles"); + Map before = new HashMap<>(handles); + RocksDBStore store = this.adapterStore(topling, tables); + store.truncate(); + for (String table : tables) { + Assert.assertTrue(this.rocks.existsTable(table)); + Assert.assertNull(this.rocks.session().keyRange(table)); + for (byte[] key : keys) { + Assert.assertNull(this.rocks.session().get(table, key)); + } + if (topling) { + Assert.assertSame(before.get(table), handles.get(table)); + } else { + Assert.assertNotSame(before.get(table), handles.get(table)); + } + } + for (String table : tables) { + this.rocks.session().put(table, new byte[]{(byte) 0xff}, new byte[]{44}); + } + this.commit(); + for (String table : tables) { + Assert.assertArrayEquals(new byte[]{44}, this.rocks.session().get(table, new byte[]{(byte) 0xff})); + } + // A second truncate covers the single-key path for every formerly empty CF. + store.truncate(); + for (String table : tables) { + Assert.assertNull(this.rocks.session().keyRange(table)); + } + } + + @Test + public void testTruncatePropagatesFirstKeyReadFailure() throws Exception { + this.assertTruncateKeyReadFailure(false); + } + + @Test + public void testTruncatePropagatesLastKeyReadFailure() throws Exception { + this.assertTruncateKeyReadFailure(true); + } + + private void assertTruncateKeyReadFailure(boolean lastKey) throws Exception { + String brokenTable = "broken"; + this.rocks.createTable(brokenTable); + this.put("healthy", "retained"); + this.rocks.session().put(brokenTable, getBytes("a"), getBytes("first")); + this.rocks.session().put(brokenTable, getBytes("z"), getBytes("last")); + this.commit(); + RocksDBStore store = this.adapterStore(true, ImmutableList.of(TABLE, brokenTable)); + AtomicReference owner = Whitebox.getInternalState(this.rocks, "rocksdb"); + Object opened = owner.get(); + RocksDB realDB = Whitebox.getInternalState(opened, "rocksdb"); + Map handles = Whitebox.getInternalState(opened, "cfHandles"); + ColumnFamilyHandle brokenHandle = Whitebox.getInternalState(handles.get(brokenTable), "handle"); + RocksIterator brokenIterator = Mockito.mock(RocksIterator.class); + Mockito.when(brokenIterator.isValid()).thenReturn(lastKey, false); + Mockito.when(brokenIterator.key()).thenReturn(getBytes("a")); + RocksDBException readFailure = new RocksDBException("injected iterator read failure"); + Mockito.doThrow(readFailure).when(brokenIterator).status(); + // Keep a real DB and healthy iterator; replace only the failing native + // boundary. The production truncate -> clearTables -> keyRange chain runs. + RocksDB faultDB = Mockito.mock(RocksDB.class, AdditionalAnswers.delegatesTo(realDB)); + Mockito.doReturn(brokenIterator).when(faultDB).newIterator(brokenHandle); + Whitebox.setInternalState(opened, "rocksdb", faultDB); + try { + Throwable failure = Assert.assertThrows(BackendException.class, store::truncate); + Assert.assertSame(readFailure, failure.getCause()); + Mockito.verify(brokenIterator).status(); + Mockito.verify(brokenIterator).close(); + } finally { + Whitebox.setInternalState(opened, "rocksdb", realDB); + } + Assert.assertEquals("retained", this.get("healthy")); + Assert.assertArrayEquals(getBytes("first"), this.rocks.session().get(brokenTable, getBytes("a"))); + Assert.assertArrayEquals(getBytes("last"), this.rocks.session().get(brokenTable, getBytes("z"))); + Assert.assertFalse(this.rocks.session().hasChanges()); + Assert.assertSame(brokenHandle, Whitebox.getInternalState(handles.get(brokenTable), "handle")); + } + + private RocksDBStore adapterStore(boolean topling, List tables) { + RocksDBStore store = new RocksDBStore.RocksDBGraphStore(new RocksDBStoreProvider(), "db", "store") { + @Override + protected List tableNames() { + return tables; + } + }; + Whitebox.setInternalState(store, "sessions", this.rocks); + Map databases = Whitebox.getInternalState(store, "dbs"); + databases.put(DB_PATH, this.rocks); + // Select the actual adapter branch, independently of the loaded JNI. + // This fixture also runs unchanged with the externally loaded TP JNI on Linux. + Whitebox.setInternalState(store, "toplingProvider", topling); + return store; + } + + @Test + public void testDatabaseOpenedDoesNotCreateSession() throws RocksDBException { + HugeConfig config = FakeObjects.newConfig(); + String path = DB_PATH + "/opened"; + RocksDBSessions sessions = + new RocksDBStdSessions(config, "db", "store", path, path); + AtomicInteger sessionCount = + Whitebox.getInternalState(sessions, "sessionCount"); + + Assert.assertEquals(0, sessionCount.get()); + Assert.assertTrue(sessions.databaseOpened()); + Assert.assertEquals(0, sessionCount.get()); + + sessions.forceCloseRocksDB(); + Assert.assertFalse(sessions.databaseOpened()); + } + @Test public void testTable() throws RocksDBException { final String TABLE2 = "test-table2"; @@ -135,6 +590,159 @@ public void testSnapshot() throws RocksDBException, IOException { } } + @Test + public void testSnapshotWithSeparateWalDirectory() throws Exception { + String dataPath = DB_PATH + "/separate-data"; + String walPath = DB_PATH + "/separate-wal"; + String snapshotPath = SNAPSHOT_PATH + "/separate-rocks"; + FileUtils.deleteDirectory(FileUtils.getFile(dataPath)); + FileUtils.deleteDirectory(FileUtils.getFile(walPath)); + FileUtils.deleteDirectory(FileUtils.getFile(snapshotPath)); + HugeConfig config = FakeObjects.newConfig(); + RocksDBSessions sessions = new RocksDBStdSessions(config, "db", "store", + dataPath, walPath); + try { + sessions.createTable(TABLE); + sessions.session().put(TABLE, getBytes("person:1gname"), + getBytes("James")); + sessions.session().put(TABLE, getBytes("person:2gname"), + getBytes("Lisa")); + sessions.session().commit(); + + sessions.createSnapshot(snapshotPath); + + sessions.session().put(TABLE, getBytes("person:1gname"), + getBytes("James2")); + sessions.session().put(TABLE, getBytes("person:3gname"), + getBytes("After")); + sessions.session().commit(); + Assert.assertEquals("James2", getString(sessions.session().get( + TABLE, getBytes("person:1gname")))); + + sessions.resumeSnapshot(snapshotPath); + + Assert.assertEquals("James", getString(sessions.session().get( + TABLE, getBytes("person:1gname")))); + Assert.assertEquals("Lisa", getString(sessions.session().get( + TABLE, getBytes("person:2gname")))); + Assert.assertNull(sessions.session().get(TABLE, + getBytes("person:3gname"))); + Assert.assertEquals(0, readWalLogs(dataPath).size()); + assertNoResumeResidue(dataPath); + } finally { + sessions.close(); + FileUtils.deleteDirectory(FileUtils.getFile(dataPath)); + FileUtils.deleteDirectory(FileUtils.getFile(walPath)); + File snapshotFile = FileUtils.getFile(SNAPSHOT_PATH); + if (snapshotFile.exists()) { + FileUtils.forceDelete(snapshotFile); + } + } + } + + + @Test + public void testResumeWhenDataDirectoryIsInsideWal() throws Exception { + String walPath = nestedRoot("parent-wal"); + String dataPath = walPath + "/nested-data"; + resumeNestedAndExpectSnapshot(dataPath, walPath); + } + + @Test + public void testResumeWhenWalDirectoryIsInsideData() throws Exception { + String dataPath = nestedRoot("parent-data"); + String walPath = dataPath + "/nested-wal"; + resumeNestedAndExpectSnapshot(dataPath, walPath); + } + + + private static String nestedRoot(String name) { + return System.getProperty("java.io.tmpdir") + "/nested-wal-closure/" + name; + } + + private void resumeNestedAndExpectSnapshot(String dataPath, String walPath) + throws Exception { + String snapshotPath = SNAPSHOT_PATH + "/nested-rocks"; + FileUtils.deleteDirectory(FileUtils.getFile(dataPath)); + FileUtils.deleteDirectory(FileUtils.getFile(walPath)); + FileUtils.forceMkdir(FileUtils.getFile(walPath).getParentFile()); + FileUtils.forceMkdir(FileUtils.getFile(dataPath).getParentFile()); + HugeConfig config = FakeObjects.newConfig(); + RocksDBSessions sessions = new RocksDBStdSessions(config, "db", "store", + dataPath, walPath); + try { + sessions.createTable(TABLE); + sessions.session().put(TABLE, getBytes("person:1gname"), + getBytes("James")); + sessions.session().commit(); + sessions.createSnapshot(snapshotPath); + sessions.session().put(TABLE, getBytes("person:1gname"), + getBytes("James2")); + sessions.session().commit(); + sessions.resumeSnapshot(snapshotPath); + Assert.assertEquals("James", getString(sessions.session().get( + TABLE, getBytes("person:1gname")))); + } finally { + sessions.close(); + FileUtils.deleteDirectory(FileUtils.getFile(dataPath)); + FileUtils.deleteDirectory(FileUtils.getFile(walPath)); + File snapshotFile = FileUtils.getFile(SNAPSHOT_PATH); + if (snapshotFile.exists()) { + FileUtils.forceDelete(snapshotFile); + } + } + } + + private static Map readWalLogs(String directory) throws IOException { + Map logs = new HashMap<>(); + File dir = FileUtils.getFile(directory); + File[] children = dir.listFiles(); + if (children == null) { + return logs; + } + for (File child : children) { + String name = child.getName(); + int dot = name.lastIndexOf('.'); + if (!child.isFile() || dot <= 0 || !".log".equals(name.substring(dot))) { + continue; + } + boolean digits = true; + for (int i = 0; i < dot; i++) { + if (!Character.isDigit(name.charAt(i))) { + digits = false; + break; + } + } + if (digits) { + logs.put(name, FileUtils.readFileToByteArray(child)); + } + } + return logs; + } + + @Test + public void testResumeResidueAssertionDetectsPendingMarker() throws IOException { + String data = DB_PATH + "/residue-data"; + File marker = FileUtils.getFile(data + ".resume-pending"); + try { + FileUtils.writeByteArrayToFile(marker, new byte[]{1}); + Assert.assertThrows(AssertionError.class, () -> assertNoResumeResidue(data)); + } finally { + FileUtils.forceDelete(marker); + } + } + + private static void assertNoResumeResidue(String dataPath) { + File data = FileUtils.getFile(dataPath).getAbsoluteFile(); + File[] children = data.getParentFile().listFiles(); + Assert.assertNotNull(children); + for (File child : children) { + String name = child.getName(); + Assert.assertFalse(name.startsWith(data.getName() + ".resume-staging-")); + Assert.assertFalse(name.equals(data.getName() + ".resume-pending")); + } + } + @Test public void testCopySessions() throws RocksDBException { Assert.assertFalse(this.rocks.closed()); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBStdSessionsLifecycleTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBStdSessionsLifecycleTest.java new file mode 100644 index 0000000000..7f9e8a49bd --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBStdSessionsLifecycleTest.java @@ -0,0 +1,352 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.unit.rocksdb; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBOptions; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions.Session; +import org.apache.hugegraph.backend.store.rocksdb.RocksDBStdSessions; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.OptionSpace; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.unit.BaseUnitTest; +import org.apache.hugegraph.unit.FakeObjects; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.rocksdb.RocksDBException; +import org.rocksdb.Status; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** + * Lifecycle and negative-path tests for {@link RocksDBStdSessions}. Every test + * uses its own temporary directory so it never shares the fixed path used by + * {@link BaseRocksDBUnitTest}. + */ +public class RocksDBStdSessionsLifecycleTest extends BaseUnitTest { + + private static final String TABLE = "lifecycle-table"; + + @Rule + public TemporaryFolder temporary = new TemporaryFolder(); + + private final List opened = new ArrayList<>(); + + @BeforeClass + public static void registerRocksDBOptions() { + // Typed values in HugeConfig require the RocksDB options to be known + if (!OptionSpace.containKey(RocksDBOptions.NUM_LEVELS.name())) { + OptionSpace.register("rocksdb", RocksDBOptions.class.getName()); + } + } + + @After + public void closeOpenedSessions() { + for (RocksDBSessions sessions : this.opened) { + try { + // Closes this thread's session and, when it is the last one, the DB + sessions.close(); + } finally { + if (sessions.databaseOpened()) { + sessions.forceCloseRocksDB(); + } + } + } + this.opened.clear(); + } + + @Test + public void testOperationsOnUnopenedTableAreRejected() throws Exception { + RocksDBSessions sessions = this.open(this.newPath("unopened")); + Session session = sessions.session(); + byte[] key = getBytes("k"); + + Assert.assertThrows(BackendException.class, () -> { + session.put("missing", key, getBytes("v")); + }, e -> { + Assert.assertContains("Table 'missing' is not opened", e.getMessage()); + }); + Assert.assertFalse(session.hasChanges()); + + Assert.assertThrows(BackendException.class, () -> { + session.get("missing", key); + }, e -> { + Assert.assertContains("Table 'missing' is not opened", e.getMessage()); + }); + Assert.assertThrows(BackendException.class, () -> { + session.scan("missing"); + }, e -> { + Assert.assertContains("Table 'missing' is not opened", e.getMessage()); + }); + Assert.assertThrows(BackendException.class, () -> { + session.delete("missing", key); + }, e -> { + Assert.assertContains("Table 'missing' is not opened", e.getMessage()); + }); + Assert.assertThrows(BackendException.class, () -> { + session.keyRange("missing"); + }, e -> { + Assert.assertContains("Table 'missing' is not opened", e.getMessage()); + }); + + // The opened table is still fully usable after the rejected calls + session.put(TABLE, key, getBytes("v")); + Assert.assertEquals(1, (int) session.commit()); + Assert.assertEquals("v", getString(session.get(TABLE, key))); + } + + @Test + public void testCreateTableTwiceKeepsDataAndDropMissingTableIsNoop() throws Exception { + RocksDBSessions sessions = this.open(this.newPath("idempotent")); + Session session = sessions.session(); + session.put(TABLE, getBytes("k"), getBytes("v")); + session.commit(); + + // Re-creating an existing table must not truncate or replace it + sessions.createTable(TABLE); + Assert.assertEquals("v", getString(session.get(TABLE, getBytes("k")))); + + // Dropping a table that never existed leaves the opened tables intact + sessions.dropTable("never-created"); + Assert.assertTrue(sessions.existsTable(TABLE)); + Assert.assertFalse(sessions.existsTable("never-created")); + + sessions.dropTable(TABLE); + Assert.assertFalse(sessions.existsTable(TABLE)); + // Dropping again after the table is gone is still a no-op + sessions.dropTable(TABLE); + Assert.assertFalse(sessions.openedTables().contains(TABLE)); + } + + @Test + public void testRollbackDiscardsPendingWrites() throws Exception { + RocksDBSessions sessions = this.open(this.newPath("rollback")); + Session session = sessions.session(); + + Assert.assertEquals(0, (int) session.commit()); + session.put(TABLE, getBytes("k2"), getBytes("v2")); + Assert.assertEquals(1, (int) session.commit()); + + session.put(TABLE, getBytes("k1"), getBytes("v1")); + session.delete(TABLE, getBytes("k2")); + Assert.assertTrue(session.hasChanges()); + + session.rollback(); + Assert.assertFalse(session.hasChanges()); + Assert.assertEquals(0, (int) session.commit()); + // Neither the pending put nor the pending delete reached the DB + Assert.assertNull(session.get(TABLE, getBytes("k1"))); + Assert.assertEquals("v2", getString(session.get(TABLE, getBytes("k2")))); + } + + @Test + public void testCommittedDataSurvivesReopenButPendingWritesDoNot() throws Exception { + String path = this.newPath("reopen"); + RocksDBSessions sessions = this.open(path); + Session session = sessions.session(); + session.put(TABLE, getBytes("committed"), getBytes("v1")); + session.commit(); + session.put(TABLE, getBytes("pending"), getBytes("v2")); + Assert.assertTrue(session.hasChanges()); + + Assert.assertTrue(sessions.close()); + Assert.assertFalse(sessions.databaseOpened()); + + // Reopen without createTable(): existing column families are reloaded + RocksDBSessions reopened = this.openWithoutTable(path); + Assert.assertTrue(reopened.existsTable(TABLE)); + Session session2 = reopened.session(); + Assert.assertFalse(session2.hasChanges()); + Assert.assertEquals("v1", getString(session2.get(TABLE, getBytes("committed")))); + Assert.assertNull(session2.get(TABLE, getBytes("pending"))); + } + + @Test + public void testLiveDatabaseCannotBeOpenedTwiceUntilClosed() throws Exception { + String path = this.newPath("exclusive"); + RocksDBSessions sessions = this.open(path); + sessions.session().put(TABLE, getBytes("k"), getBytes("v")); + sessions.session().commit(); + + Assert.assertThrows(RocksDBException.class, () -> { + this.openWithoutTable(path); + }, e -> { + Assert.assertEquals(Status.Code.IOError, ((RocksDBException) e).getStatus().getCode()); + }); + // The failed open must not disturb the live instance + Assert.assertTrue(sessions.databaseOpened()); + Assert.assertEquals("v", getString(sessions.session().get(TABLE, getBytes("k")))); + + Assert.assertTrue(sessions.close()); + RocksDBSessions reopened = this.openWithoutTable(path); + Assert.assertEquals("v", getString(reopened.session().get(TABLE, getBytes("k")))); + } + + @Test + public void testClosedDatabaseRejectsTableAndSessionAccess() throws Exception { + String path = this.newPath("closed"); + RocksDBSessions sessions = this.open(path); + sessions.forceCloseRocksDB(); + Assert.assertFalse(sessions.databaseOpened()); + + Assert.assertThrows(IllegalStateException.class, () -> { + sessions.createTable("another-table"); + }, e -> { + Assert.assertContains("RocksDB has been closed", e.getMessage()); + }); + Assert.assertThrows(IllegalStateException.class, () -> { + sessions.dropTable(TABLE); + }, e -> { + Assert.assertContains("RocksDB has been closed", e.getMessage()); + }); + Assert.assertThrows(IllegalStateException.class, sessions::compactRange, e -> { + Assert.assertContains("RocksDB has been closed", e.getMessage()); + }); + Assert.assertThrows(IllegalStateException.class, sessions::session, e -> { + Assert.assertContains("session pool is closed", e.getMessage()); + }); + + // Force close must release the database lock for a later owner + RocksDBSessions reopened = this.openWithoutTable(path); + Assert.assertTrue(reopened.databaseOpened()); + Assert.assertEquals(ImmutableSet.of("default", TABLE), reopened.openedTables()); + } + + @Test + public void testScanIteratorCloseIsIdempotentAndStopsIteration() throws Exception { + RocksDBSessions sessions = this.open(this.newPath("scan-close")); + Session session = sessions.session(); + session.put(TABLE, getBytes("a1"), getBytes("1")); + session.put(TABLE, getBytes("a2"), getBytes("2")); + session.put(TABLE, getBytes("b1"), getBytes("3")); + session.commit(); + + BackendColumnIterator iter = session.scan(TABLE, getBytes("a")); + Assert.assertTrue(iter.hasNext()); + Assert.assertEquals("a1", getString(iter.next().name)); + Assert.assertEquals("a1", getString(iter.position())); + + // Closing early releases the native iterator; further reads end cleanly + iter.close(); + iter.close(); + Assert.assertFalse(iter.hasNext()); + Assert.assertThrows(NoSuchElementException.class, iter::next); + + // A fully consumed iterator clears its paging position + BackendColumnIterator all = session.scan(TABLE, getBytes("a")); + Assert.assertEquals("a1", getString(all.next().name)); + Assert.assertEquals("a2", getString(all.next().name)); + Assert.assertFalse(all.hasNext()); + Assert.assertNull(all.position()); + all.close(); + } + + @Test + public void testMismatchedLevelCompressionsRejectedAndLockReleased() throws Exception { + String path = this.newPath("bad-options"); + PropertiesConfiguration conf = new PropertiesConfiguration(); + conf.setProperty(RocksDBOptions.NUM_LEVELS.name(), "3"); + conf.setProperty(RocksDBOptions.LEVELS_COMPRESSIONS.name(), "[none, snappy]"); + HugeConfig badConfig = new HugeConfig(conf); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + new RocksDBStdSessions(badConfig, "db", "store", path, path); + }, e -> { + Assert.assertContains(RocksDBOptions.LEVELS_COMPRESSIONS.name(), e.getMessage()); + Assert.assertContains("but got 2 != 3", e.getMessage()); + }); + + // The failed open must not leave the database lock behind + RocksDBSessions sessions = this.open(path); + Assert.assertTrue(sessions.databaseOpened()); + } + + @Test + public void testBuildSnapshotPathRequiresExistingSnapshot() throws Exception { + File parent = this.temporary.newFolder("graph"); + File dataDir = new File(parent, "rocksdb-data/g"); + Assert.assertTrue(dataDir.getParentFile().mkdirs()); + String dataPath = dataDir.getAbsolutePath(); + RocksDBSessions sessions = this.open(dataPath); + + File expected = new File(parent, "snapshot_rocksdb-data/g"); + Assert.assertThrows(IllegalArgumentException.class, () -> { + sessions.buildSnapshotPath("snapshot"); + }, e -> { + Assert.assertContains("doesn't exist", e.getMessage()); + Assert.assertContains(expected.getPath(), e.getMessage()); + }); + + Assert.assertTrue(expected.mkdirs()); + Assert.assertEquals(expected.getPath(), sessions.buildSnapshotPath("snapshot")); + } + + @Test + public void testNonAsciiDataSurvivesReopen() throws Exception { + String path = this.newPath("utf8"); + RocksDBSessions sessions = this.open(path); + sessions.session().put(TABLE, getBytes("图-key"), getBytes("值-\uD83D\uDE00")); + sessions.session().commit(); + Assert.assertTrue(sessions.close()); + + RocksDBSessions reopened = this.openWithoutTable(path); + Assert.assertEquals("值-\uD83D\uDE00", + getString(reopened.session().get(TABLE, getBytes("图-key")))); + Assert.assertArrayEquals("图-key".getBytes(StandardCharsets.UTF_8), getBytes("图-key")); + } + + private String newPath(String name) throws Exception { + return new File(this.temporary.newFolder(name), "db").getAbsolutePath(); + } + + private RocksDBSessions open(String path) throws RocksDBException { + RocksDBSessions sessions = this.openWithoutTable(path); + sessions.createTable(TABLE); + return sessions; + } + + private RocksDBSessions openWithoutTable(String path) throws RocksDBException { + // Passing no CF names still reopens every column family found on disk + RocksDBSessions sessions = new RocksDBStdSessions(FakeObjects.newConfig(), "db", + "store", path, path, + ImmutableList.of()); + this.opened.add(sessions); + return sessions; + } + + private static byte[] getBytes(String str) { + return str.getBytes(StandardCharsets.UTF_8); + } + + private static String getString(byte[] bytes) { + return bytes == null ? null : new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBTableQueryByIdsTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBTableQueryByIdsTest.java index 26598ba497..a50beb24b1 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBTableQueryByIdsTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/rocksdb/RocksDBTableQueryByIdsTest.java @@ -27,10 +27,10 @@ import java.util.Set; import org.apache.commons.lang3.tuple.Pair; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.query.IdQuery; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; import org.apache.hugegraph.backend.store.rocksdb.RocksDBSessions; import org.apache.hugegraph.backend.store.rocksdb.RocksDBTables; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinaryBackendEntryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinaryBackendEntryTest.java index 7c8a507021..bcaab2878f 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinaryBackendEntryTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinaryBackendEntryTest.java @@ -18,7 +18,7 @@ package org.apache.hugegraph.unit.serializer; import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.unit.BaseUnitTest; @@ -38,6 +38,7 @@ public void testColumns() { entry.columns(ImmutableList.of(col)); Assert.assertEquals(1, entry.columnsSize()); Assert.assertEquals(ImmutableList.of(col), entry.columns()); + Assert.assertSame(col, entry.columns().iterator().next()); entry.columns(ImmutableList.of(col, col)); Assert.assertEquals(3, entry.columnsSize()); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinaryScatterSerializerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinaryScatterSerializerTest.java index a0df546327..224867c71a 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinaryScatterSerializerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinaryScatterSerializerTest.java @@ -24,7 +24,6 @@ import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.testutil.Assert; -import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.unit.BaseUnitTest; import org.apache.hugegraph.unit.FakeObjects; import org.junit.Test; @@ -49,7 +48,7 @@ public void testVertex() { assertCollectionEquals(edge.targetVertex().getProperties(), vertex2.getProperties()); - Whitebox.setInternalState(vertex2, "removed", true); + vertex2.element().removed(true); Assert.assertTrue(vertex2.removed()); BackendEntry entry3 = ser.writeVertex(vertex2); Assert.assertEquals(0, entry3.columnsSize()); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinarySerializerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinarySerializerTest.java index 02d64bcc78..853c981aae 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinarySerializerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinarySerializerTest.java @@ -17,29 +17,172 @@ package org.apache.hugegraph.unit.serializer; +import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; import java.util.Date; +import java.util.Properties; import java.util.Set; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.backend.serializer.AbstractSerializer; +import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; import org.apache.hugegraph.backend.serializer.BinarySerializer; +import org.apache.hugegraph.backend.serializer.TextSerializer; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.config.HugeConfig; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.Userdata; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdPrefixQuery; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.Userdata; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; +import org.apache.hugegraph.structure.Index.IdWithExpiredTime; +import org.apache.hugegraph.structure.Index; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.hugegraph.type.define.IdStrategy; +import org.apache.hugegraph.type.define.IndexType; import org.apache.hugegraph.unit.BaseUnitTest; import org.apache.hugegraph.unit.FakeObjects; +import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.DateUtil; import org.junit.Test; +import org.mockito.Mockito; public class BinarySerializerTest extends BaseUnitTest { + @Test + public void testEdgeQueryLabelCandidateContract() { + for (AbstractSerializer serializer : Arrays.asList( + new BinarySerializer(FakeObjects.newConfig()), + new TextSerializer(FakeObjects.newConfig()))) { + ConditionQuery eq = new ConditionQuery(HugeType.EDGE); + eq.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + eq.eq(HugeKeys.DIRECTION, Directions.OUT); + eq.eq(HugeKeys.LABEL, IdGenerator.of(2)); + ConditionQuery in = new ConditionQuery(HugeType.EDGE); + in.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + in.eq(HugeKeys.DIRECTION, Directions.OUT); + in.query(Condition.in(HugeKeys.LABEL, Arrays.asList(IdGenerator.of(2)))); + Assert.assertEquals(((IdPrefixQuery) serializer.writeQuery(eq)).prefix(), + ((IdPrefixQuery) serializer.writeQuery(in)).prefix()); + + ConditionQuery multiple = new ConditionQuery(HugeType.EDGE); + multiple.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + multiple.eq(HugeKeys.DIRECTION, Directions.OUT); + multiple.query(Condition.in(HugeKeys.LABEL, + Arrays.asList(IdGenerator.of(2), IdGenerator.of(3)))); + Assert.assertThrows(IllegalStateException.class, () -> serializer.writeQuery(multiple)); + } + } + + @Test + public void testLegacyStoredLabelExpiryReadEliminateAndAppendKeys() throws IOException { + Properties core = labelFixtures("core-rows.properties"); + Properties store = labelFixtures("store-index-rows.properties"); + long expiry = Long.parseLong(core.getProperty("expiry")); + FakeObjects objects = new FakeObjects("legacy-fixture"); + Mockito.when(objects.graph().now()).thenReturn(expiry + 1L); + BinarySerializer serializer = new BinarySerializer(); + for (String name : new String[]{"system", "system.edge"}) { + boolean edge = name.endsWith("edge"); + HugeType base = edge ? HugeType.EDGE : HugeType.VERTEX; + IndexLabel label = IndexLabel.label(base); + HugeType indexType = edge ? HugeType.EDGE_LABEL_INDEX : HugeType.VERTEX_LABEL_INDEX; + Id element = edge ? new EdgeId(IdGenerator.of(1L), Directions.OUT, + IdGenerator.of(5L), IdGenerator.of(5L), "2026!x", + IdGenerator.of(2L)) : IdGenerator.of(1L); + byte[] key = Bytes.fromHex(store.getProperty(name + ".name")); + byte[] value = Bytes.fromHex(store.getProperty(name + ".value")); + byte[] rowId = Bytes.fromHex(core.getProperty("index." + name + ".id")); + BinaryBackendEntry stored = new BinaryBackendEntry(indexType, rowId); + stored.column(key, value); + ConditionQuery query = new ConditionQuery(indexType); + query.eq(HugeKeys.FIELD_VALUES, edge ? 5L : 4L); + Index read = serializer.readIndex(objects.graph(), query, stored); + Assert.assertEquals(element, read.elementId()); + Assert.assertEquals(expiry, read.expiredTime()); + Assert.assertFalse(read.hasTtl()); + Set expired = read.expiredElementIds(); + Assert.assertEquals(1, expired.size()); + Assert.assertTrue(read.elementIds().isEmpty()); + Index removal = read.clone(); + removal.resetElementIds(); + for (IdWithExpiredTime record : expired) { + removal.elementIds(record.id(), record.expiredTime()); + } + BackendEntry removed = serializer.writeIndex(removal); + Assert.assertEquals(1, removed.columnsSize()); + BackendColumn removedColumn = removed.columns().iterator().next(); + Assert.assertArrayEquals(key, removedColumn.name); + Assert.assertNotNull(removedColumn.value); + Assert.assertArrayEquals(BytesBuffer.BYTES_EMPTY, removedColumn.value); + Assert.assertTrue(read.elementIds().isEmpty()); + + Index append = new Index(objects.graph(), label); + append.fieldValues(edge ? 5L : 4L); + append.elementIds(element, expiry); + BackendEntry appended = serializer.writeIndex(append); + BackendColumn appendedColumn = appended.columns().iterator().next(); + Assert.assertArrayEquals(key, appendedColumn.name); + Assert.assertArrayEquals(Bytes.fromHex(core.getProperty( + "index." + name + ".column.0.name")), appendedColumn.name); + Assert.assertNotNull(appendedColumn.value); + Assert.assertArrayEquals(Bytes.fromHex(core.getProperty( + "index." + name + ".column.0.value")), appendedColumn.value); + // Core append and TTL elimination address the original Store cell, + // so they cannot introduce a second key carrying an expiry suffix. + Assert.assertArrayEquals(removedColumn.name, appendedColumn.name); + Index appendedRead = serializer.readIndex(objects.graph(), query, appended); + Assert.assertEquals(1, appendedRead.elementIds().size()); + Assert.assertEquals(element, appendedRead.elementId()); + Assert.assertEquals(0L, appendedRead.expiredTime()); + } + } + + @Test + public void testOrdinaryIndexWriterProducesNonNullKvValue() { + FakeObjects objects = new FakeObjects(); + Id fieldId = IdGenerator.of(1L); + Id baseId = IdGenerator.of(2L); + objects.newPropertyKey(fieldId, "age", DataType.INT, Cardinality.SINGLE); + objects.newVertexLabel(baseId, "person", IdStrategy.CUSTOMIZE_NUMBER, fieldId); + IndexLabel label = objects.newIndexLabel(IdGenerator.of(3L), "by-age", HugeType.VERTEX_LABEL, + baseId, IndexType.SECONDARY, fieldId); + Index index = new Index(objects.graph(), label); + index.fieldValues("42"); + index.elementIds(IdGenerator.of(4L)); + + BackendEntry entry = new BinarySerializer().writeIndex(index); + Assert.assertEquals(1, entry.columnsSize()); + BackendColumn column = entry.columns().iterator().next(); + Assert.assertNotNull(column.name); + Assert.assertNotNull(column.value); + Assert.assertArrayEquals(BytesBuffer.BYTES_EMPTY, column.value); + } + + private static Properties labelFixtures(String name) throws IOException { + Properties fixtures = new Properties(); + try (InputStream input = BinarySerializerTest.class.getResourceAsStream( + "/compatibility/2f827d6e8/" + name)) { + Assert.assertNotNull(input); + fixtures.load(input); + } + return fixtures; + } + @Test public void testVertex() { HugeConfig config = FakeObjects.newConfig(); @@ -58,7 +201,7 @@ public void testVertex() { assertCollectionEquals(edge.targetVertex().getProperties(), vertex2.getProperties()); - Whitebox.setInternalState(vertex2, "removed", true); + vertex2.element().removed(true); Assert.assertTrue(vertex2.removed()); BackendEntry entry3 = ser.writeVertex(vertex2); Assert.assertEquals(0, entry3.columnsSize()); @@ -107,7 +250,7 @@ public void testVertexForPartition() { assertCollectionEquals(edge.targetVertex().getProperties(), vertex2.getProperties()); - Whitebox.setInternalState(vertex2, "removed", true); + vertex2.element().removed(true); Assert.assertTrue(vertex2.removed()); BackendEntry entry3 = ser.writeVertex(vertex2); Assert.assertEquals(0, entry3.columnsSize()); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BytesBufferTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BytesBufferTest.java index 8d82a7c6c8..af438b578f 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BytesBufferTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BytesBufferTest.java @@ -30,13 +30,13 @@ import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.id.IdGenerator.UuidId; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.IdGenerator.UuidId; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; -import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; @@ -921,77 +921,77 @@ public void testProperty() { PropertyKey pkey = genPkey(DataType.BOOLEAN); Object value = true; byte[] bytes = genBytes("01"); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); value = false; bytes = genBytes("00"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.BYTE); value = (byte) 127; bytes = genBytes("7f"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.INT); value = 127; bytes = genBytes("7f"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.INT); value = 128; bytes = genBytes("8100"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.FLOAT); value = 1.0f; bytes = genBytes("3f800000"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.FLOAT); value = 3.14f; bytes = genBytes("4048f5c3"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.FLOAT); value = -1.0f; bytes = genBytes("bf800000"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.FLOAT); value = Float.MAX_VALUE; bytes = genBytes("7f7fffff"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.LONG); value = 127L; bytes = genBytes("7f"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.DOUBLE); value = 3.14d; bytes = genBytes("40091eb851eb851f"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.DATE); Calendar c = Calendar.getInstance(TimeZone.getTimeZone("Beijing")); @@ -999,53 +999,53 @@ public void testProperty() { value = c.getTime(); bytes = genBytes("adc9a098e22a"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.TEXT); value = "abc"; bytes = genBytes("03616263"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.BLOB); value = genBytes("001199aabbcc"); bytes = genBytes("06001199aabbcc"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); Assert.assertEquals(Blob.wrap((byte[]) value), - BytesBuffer.wrap(bytes).readProperty(pkey)); + BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.UUID); value = UUID.fromString("3cfcafc8-7906-4ab7-a207-4ded056f58de"); bytes = genBytes("3cfcafc879064ab7a2074ded056f58de"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.OBJECT); value = new Point(3, 8); bytes = genBytes("1301006a6176612e6177742e506f696ef4010610"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.OBJECT); value = UUID.fromString("3cfcafc8-7906-4ab7-a207-4ded056f58de"); bytes = genBytes("2101006a6176612e7574696c2e555549c401" + "3cfcafc879064ab7a2074ded056f58de"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genPkey(DataType.OBJECT); value = new int[]{1, 3, 8}; bytes = genBytes("0901005bc90104020610"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); Assert.assertArrayEquals((int[]) value, (int[]) - BytesBuffer.wrap(bytes).readProperty(pkey)); + BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); } @Test @@ -1054,50 +1054,50 @@ public void testPropertyWithList() { PropertyKey pkey = genListPkey(DataType.BOOLEAN); Object value = ImmutableList.of(true, false); byte[] bytes = genBytes("020100"); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genListPkey(DataType.BYTE); value = ImmutableList.of(); bytes = genBytes("00"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genListPkey(DataType.BYTE); value = ImmutableList.of((byte) 127, (byte) 128); bytes = genBytes("027f8fffffff00"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genListPkey(DataType.INT); value = ImmutableList.of(127, 128); bytes = genBytes("027f8100"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genListPkey(DataType.FLOAT); value = ImmutableList.of(1.0f, 3.14f); bytes = genBytes("023f8000004048f5c3"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genListPkey(DataType.LONG); value = ImmutableList.of(127L, 128L); bytes = genBytes("027f8100"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genListPkey(DataType.DOUBLE); value = ImmutableList.of(1.0d, 3.14d); bytes = genBytes("023ff000000000000040091eb851eb851f"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genListPkey(DataType.DATE); Calendar c = Calendar.getInstance(TimeZone.getTimeZone("Beijing")); @@ -1105,22 +1105,22 @@ public void testPropertyWithList() { value = ImmutableList.of(c.getTime(), c.getTime()); bytes = genBytes("02adc9a098e22aadc9a098e22a"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genListPkey(DataType.TEXT); value = ImmutableList.of("abc", "123"); bytes = genBytes("020361626303313233"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genListPkey(DataType.BLOB); value = ImmutableList.of(genBytes("001199aabbcc"), genBytes("5566")); bytes = genBytes("0206001199aabbcc025566"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - List list = (List) BytesBuffer.wrap(bytes).readProperty(pkey); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + List list = (List) BytesBuffer.wrap(bytes).readSchemaProperty(pkey); Assert.assertEquals(Blob.wrap(genBytes("001199aabbcc")), list.get(0)); Assert.assertEquals(Blob.wrap(genBytes("5566")), list.get(1)); @@ -1130,23 +1130,23 @@ public void testPropertyWithList() { bytes = genBytes("023cfcafc879064ab7a2074ded056f58de" + "3cfcafc879064ab7a2074ded056f58de"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genListPkey(DataType.OBJECT); value = ImmutableList.of(new Point(3, 8), new Point(3, 9)); bytes = genBytes("021301006a6176612e6177742e506f696ef4010610" + "1301006a6176612e6177742e506f696ef4010612"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genListPkey(DataType.OBJECT); value = ImmutableList.of(new int[]{1, 3}, new int[]{2, 5}); bytes = genBytes("020801005bc9010302060801005bc90103040a"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - list = (List) BytesBuffer.wrap(bytes).readProperty(pkey); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + list = (List) BytesBuffer.wrap(bytes).readSchemaProperty(pkey); Assert.assertArrayEquals(new int[]{1, 3}, (int[]) list.get(0)); Assert.assertArrayEquals(new int[]{2, 5}, (int[]) list.get(1)); } @@ -1157,50 +1157,50 @@ public void testPropertyWithSet() { PropertyKey pkey = genSetPkey(DataType.BOOLEAN); Object value = ImmutableSet.of(true, false); byte[] bytes = genBytes("020100"); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genSetPkey(DataType.BYTE); value = ImmutableSet.of(); bytes = genBytes("00"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genSetPkey(DataType.BYTE); value = ImmutableSet.of((byte) 127, (byte) 128); bytes = genBytes("027f8fffffff00"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genSetPkey(DataType.INT); value = ImmutableSet.of(127, 128); bytes = genBytes("027f8100"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genSetPkey(DataType.FLOAT); value = ImmutableSet.of(1.0f, 3.14f); bytes = genBytes("023f8000004048f5c3"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genSetPkey(DataType.LONG); value = ImmutableSet.of(127L, 128L); bytes = genBytes("027f8100"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genSetPkey(DataType.DOUBLE); value = ImmutableSet.of(1.0d, 3.14d); bytes = genBytes("023ff000000000000040091eb851eb851f"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genSetPkey(DataType.DATE); Calendar c = Calendar.getInstance(TimeZone.getTimeZone("Beijing")); @@ -1208,22 +1208,22 @@ public void testPropertyWithSet() { value = ImmutableSet.of(c.getTime(), c.getTime()); bytes = genBytes("01adc9a098e22a"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genSetPkey(DataType.TEXT); value = ImmutableSet.of("abc", "123"); bytes = genBytes("020361626303313233"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genSetPkey(DataType.BLOB); value = ImmutableSet.of(genBytes("001199aabbcc"), genBytes("5566")); bytes = genBytes("0206001199aabbcc025566"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Set set = (Set) BytesBuffer.wrap(bytes).readProperty(pkey); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Set set = (Set) BytesBuffer.wrap(bytes).readSchemaProperty(pkey); Iterator iterator = set.iterator(); Assert.assertEquals(Blob.wrap(genBytes("001199aabbcc")), iterator.next()); @@ -1234,23 +1234,23 @@ public void testPropertyWithSet() { value = ImmutableSet.of(uuid, uuid); bytes = genBytes("013cfcafc879064ab7a2074ded056f58de"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genSetPkey(DataType.OBJECT); value = ImmutableSet.of(new Point(3, 8), new Point(3, 9)); bytes = genBytes("021301006a6176612e6177742e506f696ef4010610" + "1301006a6176612e6177742e506f696ef4010612"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - Assert.assertEquals(value, BytesBuffer.wrap(bytes).readProperty(pkey)); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + Assert.assertEquals(value, BytesBuffer.wrap(bytes).readSchemaProperty(pkey)); pkey = genSetPkey(DataType.OBJECT); value = ImmutableSet.of(new int[]{1, 3}, new int[]{2, 5}); bytes = genBytes("020801005bc9010302060801005bc90103040a"); buf.forReadWritten(); - Assert.assertArrayEquals(bytes, buf.writeProperty(pkey, value).bytes()); - set = (Set) BytesBuffer.wrap(bytes).readProperty(pkey); + Assert.assertArrayEquals(bytes, buf.writeSchemaProperty(pkey, value).bytes()); + set = (Set) BytesBuffer.wrap(bytes).readSchemaProperty(pkey); iterator = set.iterator(); Assert.assertArrayEquals(new int[]{1, 3}, (int[]) iterator.next()); Assert.assertArrayEquals(new int[]{2, 5}, (int[]) iterator.next()); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.java new file mode 100644 index 0000000000..8f0af5417a --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.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.hugegraph.unit.serializer; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; + +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.IdGenerator.LongId; +import org.apache.hugegraph.id.IdGenerator.StringId; +import org.apache.hugegraph.id.IdGenerator.UuidId; +import org.apache.hugegraph.io.HugeGraphIoRegistry; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.unit.BaseUnitTest; +import org.apache.hugegraph.unit.FakeObjects; +import org.apache.hugegraph.util.JsonUtil; +import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONMapper; +import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONReader; +import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONVersion; +import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONWriter; +import org.apache.tinkerpop.gremlin.structure.io.graphson.TypeInfo; +import org.junit.Test; + +public class HugeGraphSONModuleTest extends BaseUnitTest { + + @Test + public void testSerializeFileWithGraphSONTypeInfo() throws IOException { + String json = writeTyped(new File("test.text")); + Map typedFile = JsonUtil.fromJson(json, Map.class); + Object value = typedFile.get("@value"); + + Assert.assertEquals("hugegraph:File", typedFile.get("@type")); + Assert.assertInstanceOf(Map.class, value); + Assert.assertEquals("test.text", ((Map) value).get("file")); + Assert.assertContains("hugegraph:File", json); + Assert.assertContains("\"file\"", json); + Assert.assertContains("test.text", json); + + File file = readTyped(json, File.class); + Assert.assertEquals("test.text", file.getName()); + } + + @Test + public void testRoundTripIdWithGraphSONTypeInfo() throws IOException { + StringId expectedStringId = (StringId) IdGenerator.of("marko"); + LongId expectedLongId = (LongId) IdGenerator.of(123L); + UuidId expectedUuidId = (UuidId) IdGenerator.of( + UUID.fromString("3cfcafc8-7906-4ab7-a207-4ded056f58de")); + EdgeId expectedEdgeId = EdgeId.parse("S1>2>3>4>L6"); + + String stringId = writeTyped(expectedStringId); + String longId = writeTyped(expectedLongId); + String uuidId = writeTyped(expectedUuidId); + String edgeId = writeTyped(expectedEdgeId); + + Assert.assertContains("hugegraph:StringId", stringId); + Assert.assertContains("marko", stringId); + Assert.assertContains("hugegraph:LongId", longId); + Assert.assertContains("123", longId); + Assert.assertContains("hugegraph:UuidId", uuidId); + Assert.assertContains("3cfcafc8-7906-4ab7-a207-4ded056f58de", + uuidId); + Assert.assertContains("hugegraph:EdgeId", edgeId); + Assert.assertContains("S1>2>3>4>L6", edgeId); + + Assert.assertEquals(expectedStringId, + readTyped(stringId, StringId.class)); + Assert.assertEquals(expectedLongId, + readTyped(longId, LongId.class)); + Assert.assertEquals(expectedUuidId, + readTyped(uuidId, UuidId.class)); + Assert.assertEquals(expectedEdgeId, + readTyped(edgeId, EdgeId.class)); + } + + @Test + public void testSerializeSchemaWithUntypedGraphSONModule() throws IOException { + FakeObjects objects = new FakeObjects(); + PropertyKey propertyKey = objects.newPropertyKey(IdGenerator.of(1L), + "name"); + + String json = writeUntyped(propertyKey); + + Assert.assertContains("\"name\"", json); + } + + private static String writeTyped(Object object) throws IOException { + GraphSONMapper mapper = mapper(TypeInfo.PARTIAL_TYPES); + GraphSONWriter writer = GraphSONWriter.build().mapper(mapper).create(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + writer.writeObject(output, object); + + return output.toString(StandardCharsets.UTF_8.name()); + } + + private static T readTyped(String json, Class clazz) + throws IOException { + GraphSONMapper mapper = mapper(TypeInfo.PARTIAL_TYPES); + GraphSONReader reader = GraphSONReader.build().mapper(mapper).create(); + ByteArrayInputStream input = new ByteArrayInputStream( + json.getBytes(StandardCharsets.UTF_8)); + + return reader.readObject(input, clazz); + } + + private static String writeUntyped(Object object) throws IOException { + GraphSONMapper mapper = mapper(TypeInfo.NO_TYPES); + GraphSONWriter writer = GraphSONWriter.build().mapper(mapper).create(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + writer.writeObject(output, object); + + return output.toString(StandardCharsets.UTF_8.name()); + } + + private static GraphSONMapper mapper(TypeInfo typeInfo) { + GraphSONMapper mapper = GraphSONMapper.build() + .version(GraphSONVersion.V3_0) + .typeInfo(typeInfo) + .addRegistry(HugeGraphIoRegistry.instance()) + .create(); + + return mapper; + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/SerializerFactoryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/SerializerFactoryTest.java index 67afa69842..2a241e30e2 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/SerializerFactoryTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/SerializerFactoryTest.java @@ -17,7 +17,15 @@ package org.apache.hugegraph.unit.serializer; -import org.apache.hugegraph.backend.BackendException; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.serializer.AbstractSerializer; import org.apache.hugegraph.backend.serializer.BinaryScatterSerializer; import org.apache.hugegraph.backend.serializer.BinarySerializer; @@ -58,9 +66,14 @@ public void testRegister() { Assert.assertEquals(FakeSerializer.class, SerializerFactory.serializer(config, "fake").getClass()); + // Identical registration is idempotent + SerializerFactory.register("fake", FakeSerializer.class.getName()); + Assert.assertEquals(FakeSerializer.class, + SerializerFactory.serializer(config, "fake").getClass()); + Assert.assertThrows(BackendException.class, () -> { - // exist - SerializerFactory.register("fake", FakeSerializer.class.getName()); + // conflict + SerializerFactory.register("fake", TextSerializer.class.getName()); }, e -> { Assert.assertContains("Exists serializer:", e.getMessage()); }); @@ -81,6 +94,63 @@ public void testRegister() { }); } + @Test + public void testRegisterConcurrently() throws Exception { + String name = "concurrent-serializer-" + System.nanoTime(); + CountDownLatch ready = new CountDownLatch(2); + CountDownLatch start = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + List>> registrations = List.of( + executor.submit(() -> { + ready.countDown(); + start.await(); + SerializerFactory.register( + name, FakeSerializer.class.getName()); + return FakeSerializer.class; + }), + executor.submit(() -> { + ready.countDown(); + start.await(); + SerializerFactory.register( + name, TextSerializer.class.getName()); + return TextSerializer.class; + })); + + Assert.assertTrue(ready.await(5L, TimeUnit.SECONDS)); + start.countDown(); + + int successes = 0; + int conflicts = 0; + Class registered = null; + for (Future> registration : registrations) { + try { + registered = registration.get(5L, TimeUnit.SECONDS); + successes++; + } catch (ExecutionException e) { + Assert.assertInstanceOf(BackendException.class, + e.getCause()); + Assert.assertContains("Exists serializer:", + e.getCause().getMessage()); + conflicts++; + } + } + + Assert.assertEquals(1, successes); + Assert.assertEquals(1, conflicts); + Assert.assertNotNull(registered); + + SerializerFactory.register(name, registered.getName()); + HugeConfig config = FakeObjects.newConfig(); + Assert.assertEquals( + registered, + SerializerFactory.serializer(config, name).getClass()); + } finally { + start.countDown(); + executor.shutdownNow(); + } + } + public static class FakeSerializer extends BinarySerializer { public FakeSerializer(HugeConfig config) { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/StoreSerializerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/StoreSerializerTest.java index 321f018de2..9d4497aa83 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/StoreSerializerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/StoreSerializerTest.java @@ -20,7 +20,7 @@ import java.util.Iterator; import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; -import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.backend.store.BackendAction; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendMutation; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TableBackendEntryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TableBackendEntryTest.java index 2771764d48..2c27643d26 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TableBackendEntryTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TableBackendEntryTest.java @@ -18,10 +18,10 @@ package org.apache.hugegraph.unit.serializer; import org.apache.commons.lang3.NotImplementedException; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.serializer.TableBackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TextBackendEntryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TextBackendEntryTest.java index 4700ca4afa..e016588bc5 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TextBackendEntryTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TextBackendEntryTest.java @@ -17,9 +17,9 @@ package org.apache.hugegraph.unit.serializer; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.serializer.TextBackendEntry; -import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.HugeKeys; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TextSerializerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TextSerializerTest.java index 4d55a9b046..8e5af99921 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TextSerializerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/TextSerializerTest.java @@ -21,12 +21,12 @@ import java.util.Date; import java.util.Set; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.serializer.TextSerializer; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.config.HugeConfig; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.Userdata; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.Userdata; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/store/RamIntObjectMapTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/store/RamIntObjectMapTest.java index 400ede6dc9..b16bcfbf97 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/store/RamIntObjectMapTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/store/RamIntObjectMapTest.java @@ -17,7 +17,13 @@ package org.apache.hugegraph.unit.store; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import org.apache.hugegraph.backend.store.ram.IntObjectMap; import org.junit.Assert; @@ -26,47 +32,46 @@ public class RamIntObjectMapTest { @Test - public void testConcurrency() { + public void testConcurrency() throws Exception { int size = 32; IntObjectMap map = new IntObjectMap<>(size); - - final int numThreads = 10; - final CountDownLatch startSignal = new CountDownLatch(1); - final CountDownLatch doneSignal = new CountDownLatch(numThreads); - - for (int i = 0; i < numThreads; i++) { - new Thread(() -> { - try { - startSignal.await(); - } catch (InterruptedException e) { - e.printStackTrace(); - Assert.fail(e.getMessage()); - } - - for (int j = 0; j < size; j++) { - map.set(j, j); - } - - doneSignal.countDown(); - }).start(); - } - - startSignal.countDown(); - + int numThreads = 10; + CountDownLatch startSignal = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(numThreads); + List> workers = new ArrayList<>(); try { - doneSignal.await(); - } catch (InterruptedException e) { - e.printStackTrace(); - Assert.fail(e.getMessage()); - } - - for (int i = 0; i < numThreads; i++) { - new Thread(() -> { - for (int j = 0; j < size; j++) { - Integer value = map.get(j); - Assert.assertNotNull(value); - } - }).start(); + for (int i = 0; i < numThreads; i++) { + workers.add(executor.submit(() -> { + startSignal.await(); + for (int j = 0; j < size; j++) { + map.set(j, j); + } + return null; + })); + } + startSignal.countDown(); + for (Future worker : workers) { + worker.get(10, TimeUnit.SECONDS); + } + workers.clear(); + for (int i = 0; i < numThreads; i++) { + workers.add(executor.submit(() -> { + for (int j = 0; j < size; j++) { + Assert.assertEquals(Integer.valueOf(j), map.get(j)); + } + })); + } + for (Future worker : workers) { + worker.get(10, TimeUnit.SECONDS); + } + Assert.assertEquals(size, map.size()); + for (int j = 0; j < size; j++) { + Assert.assertEquals(Integer.valueOf(j), map.get(j)); + } + } finally { + startSignal.countDown(); + executor.shutdownNow(); + Assert.assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); } } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/traversal/ConcurrentVerticesConsumerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/traversal/ConcurrentVerticesConsumerTest.java new file mode 100644 index 0000000000..c496cf938b --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/traversal/ConcurrentVerticesConsumerTest.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.hugegraph.unit.traversal; + +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.config.CoreOptions; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.traversal.algorithm.HugeTraverser; +import org.apache.hugegraph.traversal.algorithm.KoutTraverser; +import org.apache.hugegraph.traversal.algorithm.OltpTraverser.ConcurrentVerticesConsumer; +import org.apache.hugegraph.type.define.CollectionType; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.util.Consumers; +import org.apache.hugegraph.util.collection.IdSet; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class ConcurrentVerticesConsumerTest { + + @Test + public void testSerializeResultInsertion() throws Exception { + assertSerialized(HugeTraverser.NO_LIMIT); + } + + @Test + public void testCheckLimitTogetherWithInsertion() throws Exception { + assertSerialized(1L); + } + + @Test + public void testConcurrentStringIds() throws Exception { + assertNativeResults(false, HugeTraverser.NO_LIMIT); + } + + @Test + public void testConcurrentNumericIds() throws Exception { + assertNativeResults(true, HugeTraverser.NO_LIMIT); + } + + @Test + public void testConcurrentDuplicatesAndLimit() throws Exception { + assertNativeResults(false, 32L); + assertNativeResults(true, 32L); + } + + @Test + public void testSourceExcludedAndDuplicateDoNotCount() { + Id source = IdGenerator.of("source"); + Id excluded = IdGenerator.of("excluded"); + Id first = IdGenerator.of("first"); + Id second = IdGenerator.of("second"); + Set neighbors = new IdSet(CollectionType.EC); + ConcurrentVerticesConsumer consumer = new ConcurrentVerticesConsumer( + source, Collections.singleton(excluded), 2L, neighbors); + consumer.accept(edge(source)); + consumer.accept(edge(excluded)); + consumer.accept(edge(first)); + consumer.accept(edge(first)); + consumer.accept(edge(second)); + Assert.assertEquals(new HashSet<>(Arrays.asList(first, second)), neighbors); + Assert.assertThrows(Consumers.StopExecution.class, + () -> consumer.accept(edge(IdGenerator.of("third")))); + } + + @Test + public void testKoutLayersNearestLimitAndCapacity() { + Id marko = IdGenerator.of("marko"); + Id peter = IdGenerator.of("peter"); + Id ripple = IdGenerator.of("ripple"); + Id josh = IdGenerator.of("josh"); + Map> edges = Map.of( + marko, Arrays.asList(peter, ripple), + peter, Arrays.asList(marko, josh, ripple), + ripple, Arrays.asList(marko, peter)); + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.option(CoreOptions.OLTP_COLLECTION_TYPE)).thenReturn(CollectionType.EC); + Mockito.when(graph.option(CoreOptions.OLTP_CONCURRENT_THREADS)).thenReturn(0); + KoutTraverser traverser = new KoutTraverser(graph) { + @Override + protected void traverseIdsByBfs(Iterator sources, Directions dir, + Id label, long degree, long capacity, + Consumer consumer) { + ExecutorService executor = Executors.newFixedThreadPool(8); + try { + List> futures = new ArrayList<>(); + while (sources.hasNext()) { + Id source = sources.next(); + futures.add(executor.submit(() -> { + for (Id target : edges.getOrDefault(source, Collections.emptyList())) { + try { + consumer.accept(edge(target)); + } catch (Consumers.StopExecution ignored) { + break; + } + } + })); + } + for (Future future : futures) { + future.get(10L, TimeUnit.SECONDS); + } + } catch (Exception e) { + throw new AssertionError(e); + } finally { + executor.shutdownNow(); + try { + Assert.assertTrue(executor.awaitTermination(5L, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } + } + } + }; + Assert.assertEquals(Collections.singleton(josh), traverser.kout( + marko, Directions.BOTH, null, 2, true, 100L, -1L, -1L)); + Assert.assertEquals(new HashSet<>(Arrays.asList(peter, ripple, josh)), traverser.kout( + marko, Directions.BOTH, null, 2, false, 100L, -1L, -1L)); + Assert.assertEquals(1, traverser.kout( + marko, Directions.BOTH, null, 2, false, 100L, -1L, 1L).size()); + Assert.assertThrows(HugeException.class, () -> traverser.kout( + marko, Directions.BOTH, null, 2, false, 100L, 2L, 1L)); + } + + private static void assertSerialized(long limit) throws Exception { + OverlapDetectingSet neighbors = new OverlapDetectingSet(); + ConcurrentVerticesConsumer consumer = new ConcurrentVerticesConsumer( + IdGenerator.of("source"), null, limit, neighbors); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future first = executor.submit(() -> accept(consumer, IdGenerator.of("first"))); + Assert.assertTrue(neighbors.firstAdd.await(5L, TimeUnit.SECONDS)); + Future second = executor.submit(() -> accept(consumer, IdGenerator.of("second"))); + first.get(10L, TimeUnit.SECONDS); + second.get(10L, TimeUnit.SECONDS); + Assert.assertEquals("Concurrent writes to the result set", 1, neighbors.maxActive.get()); + Assert.assertEquals(limit == 1L ? 1 : 2, neighbors.size()); + } finally { + executor.shutdownNow(); + Assert.assertTrue(executor.awaitTermination(5L, TimeUnit.SECONDS)); + } + } + + private static void assertNativeResults(boolean numeric, long limit) throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(8); + try { + for (int round = 0; round < 64; round++) { + Set neighbors = new IdSet(CollectionType.EC); + ConcurrentVerticesConsumer consumer = new ConcurrentVerticesConsumer( + IdGenerator.of("source"), null, limit, neighbors); + CountDownLatch start = new CountDownLatch(1); + List> futures = new ArrayList<>(); + Set expected = new HashSet<>(); + for (int key = 0; key < 64; key++) { + expected.add(id(numeric, key)); + } + for (int worker = 0; worker < 8; worker++) { + futures.add(executor.submit(() -> { + await(start); + for (int key = 0; key < 64; key++) { + accept(consumer, id(numeric, key)); + } + })); + } + start.countDown(); + for (Future future : futures) { + future.get(10L, TimeUnit.SECONDS); + } + if (limit == HugeTraverser.NO_LIMIT) { + Assert.assertEquals(expected, neighbors); + } else { + Assert.assertEquals(limit, neighbors.size()); + Assert.assertTrue(expected.containsAll(neighbors)); + } + } + } finally { + executor.shutdownNow(); + Assert.assertTrue(executor.awaitTermination(5L, TimeUnit.SECONDS)); + } + } + + private static Id id(boolean numeric, int key) { + return numeric ? IdGenerator.of((long) key) : IdGenerator.of("vertex-" + key); + } + + private static void accept(ConcurrentVerticesConsumer consumer, Id target) { + try { + consumer.accept(edge(target)); + } catch (Consumers.StopExecution ignored) { + // Reaching the finite result limit is normal traversal termination. + } + } + + private static EdgeId edge(Id target) { + Id label = IdGenerator.of(1L); + return new EdgeId(IdGenerator.of("owner"), Directions.OUT, + label, label, "", target); + } + + private static void await(CountDownLatch latch) { + try { + Assert.assertTrue(latch.await(5L, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } + } + + private static class OverlapDetectingSet extends AbstractSet { + + private final Set values = Collections.synchronizedSet(new HashSet<>()); + private final CountDownLatch firstAdd = new CountDownLatch(1); + private final CountDownLatch overlappingAdd = new CountDownLatch(1); + private final AtomicInteger active = new AtomicInteger(); + private final AtomicInteger maxActive = new AtomicInteger(); + + @Override + public boolean add(Id id) { + int active = this.active.incrementAndGet(); + this.maxActive.accumulateAndGet(active, Math::max); + try { + if (this.firstAdd.getCount() != 0L) { + this.firstAdd.countDown(); + // Give the other worker a bounded opportunity to enter add(). + this.overlappingAdd.await(2L, TimeUnit.SECONDS); + } else { + this.overlappingAdd.countDown(); + } + return this.values.add(id); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } finally { + this.active.decrementAndGet(); + } + } + + @Override + public Iterator iterator() { + return this.values.iterator(); + } + + @Override + public int size() { + return this.values.size(); + } + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/traversal/ShortestPathTraverserTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/traversal/ShortestPathTraverserTest.java index e3630a1109..c527212c53 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/traversal/ShortestPathTraverserTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/traversal/ShortestPathTraverserTest.java @@ -27,9 +27,9 @@ import java.util.Map; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.testutil.Assert; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/ConsumersTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/ConsumersTest.java index 1bb152ec75..ea2cb0e0bc 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/ConsumersTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/ConsumersTest.java @@ -17,8 +17,13 @@ package org.apache.hugegraph.unit.util; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import org.apache.hugegraph.testutil.Assert; @@ -50,48 +55,49 @@ public void testStartProvideAwaitNormal() throws Throwable { } } - /** - * Regression test for deadlock: - * - * ContextCallable fails before entering runAndDone(). - * await() must still return because latch is decremented in safeRun(). - */ @Test(timeout = 1000) - public void testAwaitDoesNotHangWhenContextCallableFails() throws Throwable { + public void testAwaitPreservesFatalConsumerError() throws Throwable { ExecutorService executor = Executors.newFixedThreadPool(1); + CountDownLatch release = new CountDownLatch(1); + AssertionError failure = new AssertionError("fatal consumer failure"); try { - // Use AssertionError to bypass the inner catch(Exception) loop in runAndDone() - // This simulates a scenario where an exception escapes the task logic - // (similar to how a ContextCallable failure would behave from safeRun's perspective) Consumers consumers = new Consumers<>(executor, v -> { - throw new AssertionError("Simulated fatal error (OOM/StackOverflow/etc)"); + try { + if (!release.await(5L, TimeUnit.SECONDS)) { + throw new AssertionError("Consumer was not released"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } + throw failure; }); consumers.start("test-fatal-error"); consumers.provide(1); - // Verification: - // Without the fix, the latch would never be decremented (because runAndDone crashes), causing await() to hang. - // With the fix (safeRun wrapper), the finally block ensures latch.countDown() is called. - consumers.await(); - - // Note: consumer.exception will be null because safeRun only catches Exception, not Error. - // This is acceptable behavior for fatal errors, as long as it doesn't deadlock. + release.countDown(); + Assert.assertSame(failure, Assert.assertThrows(AssertionError.class, consumers::await)); } finally { + release.countDown(); executor.shutdownNow(); + Assert.assertTrue(executor.awaitTermination(5L, TimeUnit.SECONDS)); } } @Test(timeout = 1000) public void testAwaitThrowsWhenConsumerThrows() throws Throwable { ExecutorService executor = Executors.newFixedThreadPool(2); + CountDownLatch release = new CountDownLatch(1); try { final String msg = "Injected exception for test"; Consumers consumers = new Consumers<>(executor, v -> { + awaitGate(release); throw new RuntimeException(msg); }); consumers.start("test"); consumers.provide(1); + release.countDown(); try { consumers.await(); @@ -105,7 +111,271 @@ public void testAwaitThrowsWhenConsumerThrows() throws Throwable { root.getMessage().contains(msg)); } } finally { + release.countDown(); executor.shutdownNow(); } } + + @Test + public void testStopWithFullQueueAllowsAwait() throws Exception { + this.checkStopWithFullQueue(false, false); + } + + @Test + public void testStopWithFullQueueReleasesProducer() throws Exception { + this.checkStopWithFullQueue(true, false); + } + + @Test + public void testStopCleanupFailureWithFullQueueAllowsAwait() throws Exception { + this.checkStopWithFullQueue(false, true); + } + + @Test + public void testStopCleanupFailureWithFullQueueReleasesProducer() throws Exception { + this.checkStopWithFullQueue(true, true); + } + + @Test + public void testBusinessFailureWithFullQueueReleasesProducer() throws Exception { + this.checkTerminalWithFullQueue(true, false, new IllegalStateException("business failure")); + } + + @Test + public void testFatalErrorWithFullQueueReleasesProducer() throws Exception { + this.checkTerminalWithFullQueue(true, false, new AssertionError("fatal failure")); + } + + private void checkStopWithFullQueue(boolean blockedProducer, boolean cleanupFailure) throws Exception { + this.checkTerminalWithFullQueue(blockedProducer, cleanupFailure, null); + } + + private void checkTerminalWithFullQueue(boolean blockedProducer, boolean cleanupFailure, + Throwable terminalFailure) throws Exception { + ExecutorService worker = Executors.newFixedThreadPool(1); + ExecutorService caller = Executors.newSingleThreadExecutor(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicInteger processed = new AtomicInteger(); + AtomicInteger done = new AtomicInteger(); + CountDownLatch finished = new CountDownLatch(1); + RuntimeException closeFailure = new IllegalStateException("cleanup failure"); + try { + Consumers consumers = new Consumers<>(worker, value -> { + processed.incrementAndGet(); + entered.countDown(); + try { + if (!release.await(5L, TimeUnit.SECONDS)) { + throw new AssertionError("Consumer was not released"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } + if (terminalFailure instanceof Error) { + throw (Error) terminalFailure; + } else if (terminalFailure != null) { + throw (RuntimeException) terminalFailure; + } + Consumers.StopExecution stop = new Consumers.StopExecution("capacity"); + if (cleanupFailure) { + stop.addSuppressed(closeFailure); + } + throw stop; + }, () -> { + done.incrementAndGet(); + finished.countDown(); + }, 1); + consumers.start("full-queue-stop"); + try { + consumers.provide(1); + Assert.assertTrue(entered.await(5L, TimeUnit.SECONDS)); + consumers.provide(2); + consumers.provide(3); + } catch (Throwable failure) { + throw new AssertionError(failure); + } + CountDownLatch calling = new CountDownLatch(1); + Future result = caller.submit(() -> { + calling.countDown(); + try { + if (blockedProducer) { + consumers.provide(4); + } + consumers.await(); + return null; + } catch (Throwable failure) { + return failure; + } + }); + Assert.assertTrue(calling.await(5L, TimeUnit.SECONDS)); + Assert.assertThrows(TimeoutException.class, () -> result.get(100L, TimeUnit.MILLISECONDS)); + release.countDown(); + Throwable returned = result.get(3L, TimeUnit.SECONDS); + if (blockedProducer && !cleanupFailure && terminalFailure == null) { + Assert.assertTrue(returned instanceof Consumers.StopExecution); + } else { + Assert.assertSame(cleanupFailure ? closeFailure : terminalFailure, returned); + } + Assert.assertTrue(finished.await(5L, TimeUnit.SECONDS)); + if (blockedProducer && (cleanupFailure || terminalFailure != null)) { + // A failure reported by provide() must not leave await() blocked. + Future joined = caller.submit(() -> { + try { + consumers.await(); + return null; + } catch (Throwable failure) { + return failure; + } + }); + Assert.assertSame(cleanupFailure ? closeFailure : terminalFailure, + joined.get(3L, TimeUnit.SECONDS)); + } + Assert.assertEquals(1, processed.get()); + Assert.assertEquals(1, done.get()); + } finally { + release.countDown(); + caller.shutdownNow(); + worker.shutdownNow(); + for (int i = 0; i < 50 && !caller.awaitTermination(100L, TimeUnit.MILLISECONDS); i++) { + caller.shutdownNow(); + } + Assert.assertTrue(caller.isTerminated()); + Assert.assertTrue(worker.awaitTermination(5L, TimeUnit.SECONDS)); + } + } + @Test + public void testProducerFailureRemainsPrimaryAfterCleanupFailure() throws Exception { + this.checkProducerFailureAfterCleanup(1); + } + + @Test + public void testConcurrentCleanupFailuresRemainSuppressed() throws Exception { + this.checkProducerFailureAfterCleanup(2); + } + + private void checkProducerFailureAfterCleanup(int workers) throws Exception { + ExecutorService worker = Executors.newFixedThreadPool(workers); + ExecutorService caller = Executors.newSingleThreadExecutor(); + CountDownLatch entered = new CountDownLatch(workers); + CountDownLatch releaseFailure = new CountDownLatch(1); + CountDownLatch observed = new CountDownLatch(1); + AtomicInteger done = new AtomicInteger(); + RuntimeException business = new IllegalStateException("business failure"); + RuntimeException[] cleanup = new RuntimeException[workers]; + for (int i = 0; i < workers; i++) { + cleanup[i] = new IllegalStateException("cleanup failure " + i); + } + try { + Consumers consumers = new Consumers<>(worker, value -> { + entered.countDown(); + awaitGate(releaseFailure); + throw business; + }, () -> { + int index = done.getAndIncrement(); + // Cleanup fails strictly after the blocked producer observes business. + awaitGate(observed); + throw cleanup[index]; + }, 1); + consumers.start("primary-cleanup-race"); + try { + for (int i = 0; i < workers; i++) { + consumers.provide(i); + } + Assert.assertTrue(entered.await(5L, TimeUnit.SECONDS)); + for (int i = 0; i < workers + 1; i++) { + consumers.provide(workers + i); + } + } catch (Throwable failure) { + throw new AssertionError(failure); + } + CountDownLatch calling = new CountDownLatch(1); + Future provided = caller.submit(() -> { + calling.countDown(); + try { + consumers.provide(2 * workers + 1); + return null; + } catch (Throwable failure) { + return failure; + } + }); + Assert.assertTrue(calling.await(5L, TimeUnit.SECONDS)); + Assert.assertThrows(TimeoutException.class, () -> provided.get(100L, TimeUnit.MILLISECONDS)); + releaseFailure.countDown(); + Assert.assertSame(business, provided.get(3L, TimeUnit.SECONDS)); + observed.countDown(); + Future awaited = caller.submit(() -> { + try { + consumers.await(); + return null; + } catch (Throwable failure) { + return failure; + } + }); + Assert.assertSame(business, awaited.get(3L, TimeUnit.SECONDS)); + Assert.assertEquals(workers, business.getSuppressed().length); + for (RuntimeException failure : cleanup) { + Assert.assertTrue(Arrays.asList(business.getSuppressed()).contains(failure)); + } + Assert.assertEquals(workers, done.get()); + } finally { + releaseFailure.countDown(); + observed.countDown(); + caller.shutdownNow(); + worker.shutdownNow(); + Assert.assertTrue(caller.awaitTermination(5L, TimeUnit.SECONDS)); + Assert.assertTrue(worker.awaitTermination(5L, TimeUnit.SECONDS)); + } + } + + @Test + public void testCleanStopEndsLazyProducer() throws Exception { + ExecutorService worker = Executors.newFixedThreadPool(1); + CountDownLatch stopped = new CountDownLatch(1); + AtomicInteger reads = new AtomicInteger(); + AtomicInteger processed = new AtomicInteger(); + try { + Consumers consumers = new Consumers<>(worker, value -> { + processed.incrementAndGet(); + throw new Consumers.StopExecution("capacity"); + }, stopped::countDown, 1); + consumers.start("lazy-producer-stop"); + try { + for (int i = 0; i < 100; i++) { + if (i == 1) { + Assert.assertTrue(stopped.await(5L, TimeUnit.SECONDS)); + } + reads.incrementAndGet(); + consumers.provide(i); + } + } catch (Consumers.StopExecution ignored) { + // Same successful producer termination contract as Oltp traversal. + } catch (Throwable failure) { + throw new AssertionError(failure); + } finally { + try { + consumers.await(); + } catch (Throwable failure) { + throw new AssertionError(failure); + } + } + Assert.assertEquals(2, reads.get()); + Assert.assertEquals(1, processed.get()); + } finally { + worker.shutdownNow(); + Assert.assertTrue(worker.awaitTermination(5L, TimeUnit.SECONDS)); + } + } + + private static void awaitGate(CountDownLatch gate) { + try { + if (!gate.await(5L, TimeUnit.SECONDS)) { + throw new AssertionError("Gate was not released"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } + } + } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java index c76d536ff0..990841d928 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java @@ -22,16 +22,17 @@ import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.UUID; import org.apache.commons.lang3.tuple.Pair; -import org.apache.hugegraph.backend.id.EdgeId; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.schema.EdgeLabel; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.PropertyKey; -import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeEdgeProperty; import org.apache.hugegraph.structure.HugeProperty; @@ -39,7 +40,6 @@ import org.apache.hugegraph.structure.HugeVertexProperty; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Utils; -import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; @@ -51,6 +51,9 @@ import org.apache.hugegraph.unit.FakeObjects; import org.apache.hugegraph.util.JsonUtil; import org.apache.hugegraph.util.collection.CollectionFactory; +import org.apache.tinkerpop.gremlin.process.traversal.Path; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.MutablePath; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree; import org.apache.tinkerpop.shaded.jackson.core.type.TypeReference; import org.eclipse.collections.api.map.primitive.MutableIntObjectMap; import org.junit.Test; @@ -247,7 +250,7 @@ public void testSerializeVertexWithNumberId() { age.id(), new HugeVertexProperty<>(vertex, age, 29), city.id(), new HugeVertexProperty<>(vertex, city, "Beijing") ); - Whitebox.setInternalState(vertex, "properties", properties); + properties.values().forEach(vertex::setProperty); String json = JsonUtil.toJson(vertex); Assert.assertEquals("{\"id\":123456,\"label\":\"person\"," + @@ -287,8 +290,7 @@ public void testSerializeEdge() { Id id = EdgeId.parse("L123456>1>1>>L987654"); HugeEdge edge = new HugeEdge(fakeObject.graph(), id, el); - Whitebox.setInternalState(edge, "sourceVertex", source); - Whitebox.setInternalState(edge, "targetVertex", target); + edge.vertices(true, source, target); Date dateValue = Utils.date("2019-03-12"); MutableIntObjectMap> properties = @@ -296,7 +298,7 @@ public void testSerializeEdge() { date.id(), new HugeEdgeProperty<>(edge, date, dateValue), weight.id(), new HugeEdgeProperty<>(edge, weight, 0.8) ); - Whitebox.setInternalState(edge, "properties", properties); + properties.values().forEach(edge::setProperty); String json = JsonUtil.toJson(edge); Assert.assertEquals("{\"id\":\"L123456>1>1>>L987654\"," + @@ -308,6 +310,31 @@ public void testSerializeEdge() { "\"weight\":0.8}}", json); } + @Test + public void testSerializePath() { + Path path = MutablePath.make() + .extend("marko", Set.of("a")) + .extend(29, Set.of("b")); + + String json = JsonUtil.toJson(path); + + Assert.assertEquals("{\"labels\":[[\"a\"],[\"b\"]]," + + "\"objects\":[\"marko\",29]}", json); + } + + @Test + public void testSerializeTree() { + Tree child = new Tree<>(); + child.put("lop", new Tree<>()); + Tree tree = new Tree<>(); + tree.put("marko", child); + + String json = JsonUtil.toJson(tree); + + Assert.assertEquals("[{\"key\":\"marko\",\"value\":[{" + + "\"key\":\"lop\",\"value\":[]}]}]", json); + } + @Test public void testDeserializeList() { String json = "[\"1\", \"2\", \"3\"]"; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/MetricsUtilTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/MetricsUtilTest.java new file mode 100644 index 0000000000..508466644b --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/MetricsUtilTest.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.hugegraph.unit.util; + +import static org.apache.hugegraph.metrics.MetricsUtil.sanitizePrometheusMetricName; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import org.apache.hugegraph.metrics.MetricsUtil; +import org.apache.hugegraph.testutil.Assert; +import org.junit.Test; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.MetricRegistry; + +public class MetricsUtilTest { + + private static final Pattern VALID_NAME = + Pattern.compile("[a-zA-Z_:][a-zA-Z0-9_:]*"); + + @Test + public void testSanitizeReplacesInvalidChars() { + Assert.assertEquals("a_b_c_d_e_f", + sanitizePrometheusMetricName("a.b-c/d$e~f")); + Assert.assertEquals("org_apache_Cache_schema_DEFAULT_hugegraph_hits", + sanitizePrometheusMetricName( + "org.apache.Cache.schema~DEFAULT-hugegraph.hits")); + Assert.assertEquals("_graphs_1_x", + sanitizePrometheusMetricName("/graphs/1~x")); + } + + @Test + public void testSanitizeKeepsValidName() { + Assert.assertEquals("valid_name:total", + sanitizePrometheusMetricName("valid_name:total")); + Assert.assertEquals("Mixed_Case_09", + sanitizePrometheusMetricName("Mixed_Case_09")); + } + + @Test + public void testSanitizePrefixesLeadingDigit() { + Assert.assertEquals("_9cache_hits", + sanitizePrometheusMetricName("9cache.hits")); + } + + @Test + public void testSanitizeEmptyName() { + Assert.assertEquals("_", sanitizePrometheusMetricName("")); + } + + @Test + public void testWritePrometheusFormatWithTildeInGaugeName() { + MetricRegistry registry = new MetricRegistry(); + String key = "org.apache.hugegraph.Cache.schema~DEFAULT-hugegraph.hits"; + registry.register(key, (Gauge) () -> 42L); + + StringBuilder output = new StringBuilder(); + MetricsUtil.writePrometheusFormat(output, registry); + String text = output.toString(); + + String name = "org_apache_hugegraph_Cache_schema_DEFAULT_hugegraph_hits"; + Assert.assertFalse(text, text.contains("~")); + Assert.assertEquals("# HELP " + name + "\n" + + "# TYPE " + name + " gauge\n" + + name + " 42\n", text); + } + + @Test + public void testWritePrometheusFormatProducesValidNames() { + MetricRegistry registry = new MetricRegistry(); + registry.register("cache~a.size", (Gauge) () -> 1); + registry.histogram("api/graphs/1~x"); + registry.meter("9meter-rate"); + registry.timer("timer$latency~p"); + + StringBuilder output = new StringBuilder(); + MetricsUtil.writePrometheusFormat(output, registry); + + for (String line : output.toString().split("\n")) { + String name; + if (line.startsWith("# HELP ") || line.startsWith("# TYPE ")) { + name = line.substring(7).split(" ")[0]; + } else { + name = line.split("[{ ]")[0]; + } + Assert.assertTrue(line, VALID_NAME.matcher(name).matches()); + } + } + + @Test + public void testWriteStatisticsPrometheusFormatQuotesLabelValues() { + Map items = new LinkedHashMap<>(); + items.put("MAX_RESPONSE_TIME", 12L); + items.put("MEAN_RESPONSE_TIME", 3.5); + Map> metricMap = new LinkedHashMap<>(); + metricMap.put("graphs/hugegraph/graph/vertices/GET", items); + + StringBuilder output = new StringBuilder(); + MetricsUtil.writeStatisticsPrometheusFormat(output, metricMap); + + String name = "graphs_hugegraph_graph_vertices_GET"; + Assert.assertEquals("# HELP " + name + "\n" + + "# TYPE " + name + " gauge\n" + + name + "{name=\"MAX_RESPONSE_TIME\",} 12\n" + + name + "{name=\"MEAN_RESPONSE_TIME\",} 3.5\n", + output.toString()); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/RateLimiterTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/RateLimiterTest.java index 111b639421..4ace252175 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/RateLimiterTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/RateLimiterTest.java @@ -17,6 +17,9 @@ package org.apache.hugegraph.unit.util; +import java.lang.reflect.Constructor; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.atomic.LongAdder; import org.apache.hugegraph.testutil.Assert; @@ -74,6 +77,55 @@ public RateLimiter newRateLimiter(int rate) { return new FixedTimerWindowRateLimiter(rate); } + @Test + public void testInitialTimerWindow() throws Exception { + try (RecordingTimer timer = new RecordingTimer()) { + Constructor constructor = + FixedTimerWindowRateLimiter.class.getDeclaredConstructor(int.class, Timer.class); + constructor.setAccessible(true); + RateLimiter limiter = constructor.newInstance(400, timer); + + // The initial reset must wait for a complete window, even if the timer thread starts late. + Assert.assertEquals(RateLimiter.RESET_PERIOD, timer.delay); + Assert.assertEquals(RateLimiter.RESET_PERIOD, timer.period); + for (int i = 0; i < 400; i++) { + Assert.assertTrue(limiter.tryAcquire()); + } + LongAdder count = Whitebox.getInternalState(limiter, "count"); + Assert.assertEquals(400, count.intValue()); + Assert.assertFalse(limiter.tryAcquire()); + + timer.task.run(); + Assert.assertEquals(0, count.intValue()); + Assert.assertTrue(limiter.tryAcquire()); + timer.task.run(); + Assert.assertEquals(1, count.intValue()); + } + } + + private static class RecordingTimer extends Timer implements AutoCloseable { + + private TimerTask task; + private long delay; + private long period; + + private RecordingTimer() { + super("RateLimiterTest", true); + } + + @Override + public void schedule(TimerTask task, long delay, long period) { + this.task = task; + this.delay = delay; + this.period = period; + } + + @Override + public void close() { + this.cancel(); + } + } + @Test public void testTryAcquireAfterPeriod() throws Exception { int rateLimit = 400; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/StringEncodingTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/StringEncodingTest.java index 39d18802b9..42de26447e 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/StringEncodingTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/StringEncodingTest.java @@ -202,20 +202,5 @@ public void testCheckPasswordSupportsOldAndNewCost() { hashedPassword.matches("^\\$2[aby]\\$10\\$.*") ); - // Compare computational cost between work factor 4 and 10 - long start4 = System.nanoTime(); - StringEncoding.checkPassword(testPassword, oldPassword); - long elapsed4 = System.nanoTime() - start4; - - long start10 = System.nanoTime(); - StringEncoding.checkPassword(testPassword, hashedPassword); - long elapsed10 = System.nanoTime() - start10; - - // BCrypt cost difference: (10-4) = 6 => theoretical ~2^6 = 64x - Assert.assertTrue( - "Work factor 10 should be significantly slower than work factor 4 " + - "(expected exponential cost increase)", - elapsed10 >= elapsed4 * 32 - ); } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/CollectionFactoryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/CollectionFactoryTest.java index 9e4697d81a..917a3d6d44 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/CollectionFactoryTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/CollectionFactoryTest.java @@ -25,7 +25,7 @@ import java.util.Map; import java.util.Set; -import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.type.define.CollectionType; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/IdSetTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/IdSetTest.java index 2a9082c831..02ceaaea66 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/IdSetTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/IdSetTest.java @@ -23,8 +23,8 @@ import java.util.UUID; import org.apache.commons.lang.RandomStringUtils; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.type.define.CollectionType; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/IntMapTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/IntMapTest.java index c22a76a8ed..baa272b352 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/IntMapTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/IntMapTest.java @@ -17,15 +17,17 @@ package org.apache.hugegraph.unit.util.collection; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Random; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; import org.apache.hugegraph.testutil.Assert; @@ -433,51 +435,141 @@ public void testIntMapByDynamicHashSingleThread() { } @Test - public void testIntMapByDynamicHashMultiThread() throws InterruptedException { - IntMap map = new IntMapByDynamicHash(); + public void testIntMapByDynamicHashSparseKeysAcrossGrowth() { + IntMap map = new IntMapByDynamicHash(4); + int count = 32; + int stride = 1024; + for (int i = 0; i < count; i++) { + Assert.assertTrue(map.put(i * stride, i + 1)); + Assert.assertEquals(i + 1, map.size()); + for (int k = 0; k <= i; k++) { + Assert.assertTrue(map.containsKey(k * stride)); + Assert.assertEquals(k + 1, map.get(k * stride)); + } + } + for (int i = 0; i < count; i++) { + Assert.assertTrue(map.put(i * stride, -i - 1)); + Assert.assertEquals(-i - 1, map.get(i * stride)); + Assert.assertEquals(count, map.size()); + } + for (int i = 0; i < count; i++) { + Assert.assertTrue(map.remove(i * stride)); + Assert.assertFalse(map.containsKey(i * stride)); + Assert.assertFalse(map.remove(i * stride)); + Assert.assertEquals(count - i - 1, map.size()); + for (int k = i + 1; k < count; k++) { + Assert.assertEquals(-k - 1, map.get(k * stride)); + } + } + } - //int cpus = IntSet.CPUS; + @Test + public void testIntMapByDynamicHashMultiThread() throws Exception { + IntMap map = new IntMapByDynamicHash(); int cpus = 16; - ThreadPoolExecutor executor = - new ThreadPoolExecutor(cpus, cpus, 1, TimeUnit.MINUTES, - new LinkedBlockingDeque<>()) { - @Override - protected void afterExecute(Runnable r, Throwable t) { - super.afterExecute(r, t); - if (t != null) { - Assert.fail(t.getMessage()); - } - } - }; - - AtomicInteger size = new AtomicInteger(); int mapSize = 100; - CountDownLatch latch = new CountDownLatch(cpus); - for (int i = 1; i <= cpus; i++) { - int index = i; - executor.submit(() -> { - try { + ExecutorService executor = Executors.newFixedThreadPool(cpus); + List> workers = new ArrayList<>(); + try { + for (int i = 0; i < cpus; i++) { + int index = i; + workers.add(executor.submit(() -> { for (int j = 0; j < mapSize; j++) { - int key = j + (index - 1) * mapSize; - map.put(key, j); - size.getAndIncrement(); - //Assert.assertTrue(map.containsKey(key)); + int key = j + index * mapSize; + Assert.assertTrue(map.put(key, j)); Assert.assertEquals(j, map.get(key)); - //System.out.println(key + " " + j); } - } catch (Exception e) { - e.printStackTrace(); - Assert.fail(e.getMessage()); - } finally { - latch.countDown(); - } - }); + })); + } + for (Future worker : workers) { + worker.get(10, TimeUnit.SECONDS); + } + Assert.assertEquals(cpus * mapSize, map.size()); + for (int i = 0; i < cpus * mapSize; i++) { + Assert.assertTrue(map.containsKey(i)); + Assert.assertEquals(i % mapSize, map.get(i)); + } + } finally { + executor.shutdownNow(); + Assert.assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); } + } - latch.await(); - System.out.println(); - - Assert.assertEquals(size.get(), map.size()); + @Test + public void testIntMapByDynamicHashConcurrentGrowth() throws Throwable { + int threads = 16; + int keysPerThread = 100; + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(55); + ExecutorService executor = Executors.newFixedThreadPool(threads); + Throwable primaryFailure = null; + try { + // Each fresh map crosses multiple table generations; this is bounded work, + // not a retry of a failed test or concurrent access to another worker's keys. + for (int capacity : new int[]{4, 16, 64}) { + for (int scenario = 0; scenario < 300; scenario++) { + IntMap map = new IntMapByDynamicHash(capacity); + CountDownLatch started = new CountDownLatch(threads); + CountDownLatch start = new CountDownLatch(1); + List> workers = new ArrayList<>(); + try { + for (int worker = 0; worker < threads; worker++) { + int index = worker; + workers.add(executor.submit(() -> { + started.countDown(); + Assert.assertTrue(start.await(5, TimeUnit.SECONDS)); + for (int item = 0; item < keysPerThread; item++) { + int key = index * keysPerThread + item; + Assert.assertTrue(map.put(key, item)); + Assert.assertEquals(item, map.get(key)); + Assert.assertTrue(map.containsKey(key)); + if (item % 2 != 0) { + Assert.assertTrue(map.remove(key)); + Assert.assertFalse(map.containsKey(key)); + } + } + return null; + })); + } + long remaining = deadline - System.nanoTime(); + Assert.assertTrue("Concurrent growth budget exhausted", remaining > 0L); + Assert.assertTrue(started.await(Math.min(remaining, TimeUnit.SECONDS.toNanos(5)), + TimeUnit.NANOSECONDS)); + start.countDown(); + for (Future worker : workers) { + remaining = deadline - System.nanoTime(); + Assert.assertTrue("Concurrent growth budget exhausted", remaining > 0L); + worker.get(remaining, TimeUnit.NANOSECONDS); + } + Assert.assertEquals(threads * keysPerThread / 2, map.size()); + for (int key = 0; key < threads * keysPerThread; key++) { + if (key % 2 == 0) { + Assert.assertTrue(map.containsKey(key)); + Assert.assertEquals(key % keysPerThread, map.get(key)); + } else { + Assert.assertFalse(map.containsKey(key)); + } + } + } finally { + start.countDown(); + } + } + } + } catch (Throwable failure) { + primaryFailure = failure; + throw failure; + } finally { + try { + executor.shutdownNow(); + Assert.assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } catch (Throwable failure) { + if (primaryFailure == null) { + throw failure; + } + if (primaryFailure != failure) { + primaryFailure.addSuppressed(failure); + } + } + } } @Test diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/ObjectIntMappingTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/ObjectIntMappingTest.java index 4d74c5bd85..f712e9421b 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/ObjectIntMappingTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/collection/ObjectIntMappingTest.java @@ -23,8 +23,8 @@ import java.util.UUID; import org.apache.commons.lang.RandomStringUtils; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.util.collection.ObjectIntMapping; import org.apache.hugegraph.util.collection.ObjectIntMappingFactory; diff --git a/hugegraph-server/hugegraph-test/src/main/resources/META-INF/services/io.cucumber.core.backend.ObjectFactory b/hugegraph-server/hugegraph-test/src/main/resources/META-INF/services/io.cucumber.core.backend.ObjectFactory new file mode 100644 index 0000000000..8361d8526d --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/resources/META-INF/services/io.cucumber.core.backend.ObjectFactory @@ -0,0 +1 @@ +org.apache.hugegraph.tinkerpop.HugeGraphFeatureTest$HugeGraphGuiceFactory diff --git a/hugegraph-server/hugegraph-test/src/main/resources/compatibility/2f827d6e8/README.md b/hugegraph-server/hugegraph-test/src/main/resources/compatibility/2f827d6e8/README.md new file mode 100644 index 0000000000..5f5868f933 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/resources/compatibility/2f827d6e8/README.md @@ -0,0 +1,22 @@ + + +# Historical fixtures for Server label-index tests + +`BinarySerializerTest.testLegacyStoredLabelExpiryReadEliminateAndAppendKeys` uses `core-rows.properties` and `store-index-rows.properties` to recover Store expiry values and retain original label-index keys through elimination and append. + +These byte-identical copies allow tests to load fixtures from this module's own resource classpath. Keep them synchronized with the shared resources. The [canonical fixture guide](../../../../../../../hugegraph-struct/src/test/resources/compatibility/2f827d6e8/README.md) contains the pinned source revision, historical producers, regeneration instructions, compatibility limits and SHA-256 checksums. Regenerate against those historical writers only, never the current writer. diff --git a/hugegraph-server/hugegraph-test/src/main/resources/compatibility/2f827d6e8/core-rows.properties b/hugegraph-server/hugegraph-test/src/main/resources/compatibility/2f827d6e8/core-rows.properties new file mode 100644 index 0000000000..fd489e58c1 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/resources/compatibility/2f827d6e8/core-rows.properties @@ -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. + # +now=1609459200000 +expiry=1609459260000 +vertex.type=VERTEX +vertex.id=0801 +vertex.ttl=60000 +vertex.columns=1 +vertex.column.0.name=0801 +vertex.column.0.value=08040301070203078fffffff7f0703028fffffff7f07aeebd9fdb460 +edge.out.type=EDGE_OUT +edge.out.id=08018208050805323032362178000802 +edge.out.ttl=60000 +edge.out.columns=1 +edge.out.column.0.name=08018208050805323032362178000802 +edge.out.column.0.value=0301070203078fffffff7f0703028fffffff7f07aeebd9fdb460 +edge.in.type=EDGE_IN +edge.in.id=08028c08050805323032362178000801 +edge.in.ttl=60000 +edge.in.columns=1 +edge.in.column.0.name=08028c08050805323032362178000801 +edge.in.column.0.value=0301070203078fffffff7f0703028fffffff7f07aeebd9fdb460 +olap.type=OLAP +olap.id=0801 +olap.ttl=0 +olap.columns=1 +olap.column.0.name=0801 +olap.column.0.value=092a +index.secondary.type=SECONDARY_INDEX +index.secondary.id=53493a363a616c69636500 +index.secondary.ttl=60000 +index.secondary.columns=1 +index.secondary.column.0.name=53493a363a616c696365000801aeebd9fdb460 +index.secondary.column.0.value= +index.int.type=RANGE_INT_INDEX +index.int.id=a00000000780000007 +index.int.ttl=60000 +index.int.columns=1 +index.int.column.0.name=a000000007800000070801aeebd9fdb460 +index.int.column.0.value= +index.long.type=RANGE_LONG_INDEX +index.long.id=a2000000088000000000000007 +index.long.ttl=60000 +index.long.columns=1 +index.long.column.0.name=a20000000880000000000000070801aeebd9fdb460 +index.long.column.0.value= +vertex.untimed.type=VERTEX +vertex.untimed.id=0803 +vertex.untimed.ttl=0 +vertex.untimed.columns=1 +vertex.untimed.column.0.name=0803 +vertex.untimed.column.0.value=080a0301070203078fffffff7f0703028fffffff7f07 +edge.untimed.type=EDGE_OUT +edge.untimed.id=080182080b080b000802 +edge.untimed.ttl=0 +edge.untimed.columns=1 +edge.untimed.column.0.name=080182080b080b000802 +edge.untimed.column.0.value=0301070203078fffffff7f0703028fffffff7f07 +index.secondary.untimed.type=SECONDARY_INDEX +index.secondary.untimed.id=53493a433a616c69636500 +index.secondary.untimed.ttl=0 +index.secondary.untimed.columns=1 +index.secondary.untimed.column.0.name=53493a433a616c696365000801 +index.secondary.untimed.column.0.value= +index.int.untimed.type=RANGE_INT_INDEX +index.int.untimed.id=a00000000d80000007 +index.int.untimed.ttl=0 +index.int.untimed.columns=1 +index.int.untimed.column.0.name=a00000000d800000070801 +index.int.untimed.column.0.value= +index.long.untimed.type=RANGE_LONG_INDEX +index.long.untimed.id=a20000000e8000000000000007 +index.long.untimed.ttl=0 +index.long.untimed.columns=1 +index.long.untimed.column.0.name=a20000000e80000000000000070801 +index.long.untimed.column.0.value= +index.hashed.type=SECONDARY_INDEX +index.hashed.id=53493a463a323234303534613200 +index.hashed.ttl=60000 +index.hashed.columns=1 +index.hashed.column.0.name=53493a463a3232343035346132000801aeebd9fdb460 +index.hashed.column.0.value=6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d +index.system.type=VERTEX_LABEL_INDEX +index.system.id=56493a2d313a3400 +index.system.ttl=0 +index.system.columns=1 +index.system.column.0.name=56493a2d313a34000801 +index.system.column.0.value= +index.system.edge.type=EDGE_LABEL_INDEX +index.system.edge.id=45493a2d323a3500 +index.system.edge.ttl=0 +index.system.edge.columns=1 +index.system.edge.column.0.name=45493a2d323a35007e08018208050805323032362178000802 +index.system.edge.column.0.value= diff --git a/hugegraph-server/hugegraph-test/src/main/resources/compatibility/2f827d6e8/store-index-rows.properties b/hugegraph-server/hugegraph-test/src/main/resources/compatibility/2f827d6e8/store-index-rows.properties new file mode 100644 index 0000000000..7f5a599ed4 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/resources/compatibility/2f827d6e8/store-index-rows.properties @@ -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. + # +secondary.name=53493a363a616c696365000801 +secondary.value=00414141426472732f576d413d +secondary.expiry=1609459260000 +int.name=a000000007800000070801 +int.value=00414141426472732f576d413d +int.expiry=1609459260000 +long.name=a20000000880000000000000070801 +long.value=00414141426472732f576d413d +long.expiry=1609459260000 +system.name=56493a2d313a34000801 +system.value=00414141426472732f576d413d +system.expiry=1609459260000 +system.edge.name=45493a2d323a35007e08018208050805323032362178000802 +system.edge.value=00414141426472732f576d413d +system.edge.expiry=1609459260000 diff --git a/hugegraph-server/hugegraph-test/src/main/resources/fast-methods.filter b/hugegraph-server/hugegraph-test/src/main/resources/fast-methods.filter index 2d162bd705..02970dc2ee 100644 --- a/hugegraph-server/hugegraph-test/src/main/resources/fast-methods.filter +++ b/hugegraph-server/hugegraph-test/src/main/resources/fast-methods.filter @@ -17,40 +17,38 @@ # #################### structure suite #################### - -## automatic ID for edges +## Filter reason categories: +## HG-LEGACY: Existing HugeGraph feature boundary before this upgrade +## TP37-SEMANTICS: New or renamed TinkerPop 3.7 suite expectation +## TEST-INFRA: Test harness/storage lifecycle issue, not a feature regression +## PERF: Fast-suite-only runtime guard +## FOLLOW-UP: Keep tracked for later behavior or infrastructure cleanup + +## HG-LEGACY: automatic ID for edges org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdgeTest.shouldNotEvaluateToEqualDifferentId: Not support automatic/custom id for edge org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldNotEvaluateToEqualDifferentId: Not support automatic/custom id for edge org.apache.tinkerpop.gremlin.structure.EdgeTest.BasicEdgeTest.shouldValidateIdEquality: Not support automatic/custom id for edge org.apache.tinkerpop.gremlin.structure.EdgeTest.BasicEdgeTest.shouldValidateEquality: Not support automatic/custom id for edge -## property ID +## HG-LEGACY: property ID org.apache.tinkerpop.gremlin.structure.util.detached.DetachedPropertyTest.shouldNotBeEqualPropertiesAsThereIsDifferentKey: Not support property id -## id should be String type +## HG-LEGACY: id should be String type org.apache.tinkerpop.gremlin.structure.GraphTest.shouldAddVertexWithUserSuppliedStringId: expect vertex.id is String type -## same id for element +## HG-LEGACY: same id for element org.apache.tinkerpop.gremlin.structure.GraphTest.shouldHaveExceptionConsistencyWhenAssigningSameIdOnVertex: Assigning the same ID to an Element is accepted(override), but tinkerpop expect throw an exception -## uniform list -org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertexOnAdd[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list -org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnEdgeOnAdd[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list -org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertex[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list -org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnEdge[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list -org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertexOnAdd[supportsUniformListValues([try1, try2])]: Uniform list can't be accessed by vertex.property() which is supposed right by tinkerpop -org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertex[supportsUniformListValues([try1, try2])]: Uniform list can't be accessed by vertex.property() which is supposed right by tinkerpop - -# expect multi properties after setting single property multi times +## HG-LEGACY: expect multi properties after setting single property multi times org.apache.tinkerpop.gremlin.structure.GraphTest.shouldOverwriteEarlierKeyValuesWithLaterKeyValuesOnAddVertexIfMultiProperty: Expect multi properties after setting single property multi times org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyRemoval.shouldRemoveMultiProperties: Expect multi properties after setting single property multi times org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyRemoval.shouldRemoveMultiPropertiesWhenVerticesAreRemoved: Expect multi properties after setting single property multi times org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyAddition.shouldAllowIdenticalValuedMultiProperties: Expect multi properties after setting single property multi times -# not support nested property +## HG-LEGACY: not support nested property org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyRemoval.shouldAllowIteratingAndRemovingVertexPropertyProperties: Not support nested property -## failed tests with wrong edge number, reason: replicated edges treated as one +## HG-LEGACY: failed tests with wrong edge number, reason: replicated edges treated as one org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.ProcessorTest.shouldProcessEdges: Replicated edges treated as one org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateSameGraph[test(NormalDistribution{stdDeviation=2.0, mean=0.0},NormalDistribution{stdDeviation=2.0, mean=0.0})]: Replicated edges treated as one org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateDifferentGraph[test(NormalDistribution{stdDeviation=2.0, mean=0.0},NormalDistribution{stdDeviation=2.0, mean=0.0})]: Replicated edges treated as one @@ -65,27 +63,56 @@ org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.Diffe org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateSameGraph[test(PowerLawDistribution{gamma=2.3, multiplier=0.0},PowerLawDistribution{gamma=2.8, multiplier=0.0})]: Replicated edges treated as one org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateDifferentGraph[test(PowerLawDistribution{gamma=2.3, multiplier=0.0},PowerLawDistribution{gamma=2.8, multiplier=0.0})]: Replicated edges treated as one -## user supplied numeric id +## HG-LEGACY: user supplied numeric id org.apache.tinkerpop.gremlin.structure.FeatureSupportTest.VertexFunctionalityTest.shouldSupportUserSuppliedIdsOfTypeNumericLong: Not open the feature 'support user supplied id of numeric long' although numeric long id is allowed org.apache.tinkerpop.gremlin.structure.FeatureSupportTest.VertexFunctionalityTest.shouldSupportUserSuppliedIdsOfTypeNumericInt: Not open the feature 'support user supplied id of numeric long' although numeric long id is allowed -## user supplied uuid id +## HG-LEGACY/FOLLOW-UP: user supplied uuid id feature flag remains closed org.apache.tinkerpop.gremlin.structure.FeatureSupportTest.VertexFunctionalityTest.shouldSupportUserSuppliedIdsOfTypeUuid: Not open the feature 'support user supplied id of uuid' although uuid id is allowed -## edge id format validate firstly and throw NotFoundException if invalid +## HG-LEGACY/FOLLOW-UP: edge id format validation throws NotFoundException first org.apache.tinkerpop.gremlin.structure.GraphTest.shouldHaveExceptionConsistencyWhenFindEdgeByIdThatIsNonExistentViaIterator: Invalid format of edge id will introduce throwing NotFoundException before try to query in backend store -## not bugs +## HG-LEGACY/TEST-INFRA/FOLLOW-UP: existing non-bug behavior and long-running transaction case org.apache.tinkerpop.gremlin.structure.GraphTest.shouldRemoveVertices: Random UUID as edge label org.apache.tinkerpop.gremlin.structure.GraphTest.shouldRemoveEdges: Random UUID as edge label org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldExecuteWithCompetingThreads: Hang the tests because property 'test' have both String and long value -## vertex properties doesn't have nested structures with HugeVertexSerializer +## TEST-INFRA/FOLLOW-UP: gryo-v3 migration tests reopen source graph path before RocksDB releases lock +org.apache.tinkerpop.gremlin.structure.io.IoGraphTest.shouldMigrateModernGraph[gryo-v3]: RocksDB lock not released between source and target graph in gryo-v3 migration test +org.apache.tinkerpop.gremlin.structure.io.IoGraphTest.shouldMigrateClassicGraph[gryo-v3]: RocksDB lock not released between source and target graph in gryo-v3 migration test + +## TP37-SEMANTICS/FOLLOW-UP: TinkerPop 3.7 transaction lifecycle checks spawned thread transactions +org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldExecuteCompetingThreadsOnMultipleDbInstances: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close +org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldAllowReferenceOfEdgeIdOutsideOfOriginalThreadManual: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close +org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldSupportTransactionIsolationCommitCheck: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close +org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldAllowReferenceOfVertexIdOutsideOfOriginalThreadManual: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close + +## HG-LEGACY/TP37-SEMANTICS/FOLLOW-UP: TransactionMultiThreadedTest assumes spawned transaction isolation and cleanup +org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldCommit: Memory backend exposes uncommitted vertices across ThreadLocal transactions and spawned tx stays open +org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldDeleteVertexOnCommit: Spawned read transaction is not auto-closed after thread exit +org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldDeleteRelatedEdgesOnVertexDelete: Spawned verification transaction is not auto-closed after thread exit +org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldRollbackAddedVertex: Memory backend exposes uncommitted vertices and spawned tx stays open +org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldCommitEdge: Spawned read transactions are not auto-closed after thread exit +org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldRollbackAddedEdge: Spawned read transactions are not auto-closed after thread exit + +## HG-MODEL/FOLLOW-UP: scenarios supply custom vertex ids while the standard test graph uses AUTOMATIC id strategy +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person' +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person' +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person' +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_injectXlabel_knows_out_marko_in_vadasX_mergeE: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person' +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeE_with_outV_inV_options: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person' + +## HG-MODEL/FOLLOW-UP: HugeGraph multi-edge labels require a non-null sort key on every parallel edge +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X_exists: Scenario requires two same-label parallel edges while one edge has no sort-key value +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_V_hasXperson_name_marko_X_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Scenario requires two same-label parallel edges while one edge has no sort-key value + +## HG-LEGACY/FOLLOW-UP: vertex properties don't have nested structures with HugeVertexSerializer org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializeVertex: Vertex properties doesn't have nested structures with HugeVertexSerializer org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializePath: Vertex properties doesn't have nested structures with HugeVertexSerializer org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializeTree: Vertex properties doesn't have nested structures with HugeVertexSerializer -## update property but no commit, lead there are changes in indexTx, can't do index query +## HG-LEGACY: update property but no commit leaves changes in indexTx org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdgeTest.shouldConstructDetachedEdge: Can't do index query when there are changes in transaction org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdgeTest.shouldConstructDetachedEdgeAsReference: Can't do index query when there are changes in transaction org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldConstructReferenceEdge: Can't do index query when there are changes in transaction @@ -95,9 +122,11 @@ org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldCo # unsupported automatic edge id, therefore number of edge is wrong org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdge: Unsupported automatic edge id, therefore number of edge is wrong org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdgeByPath: Unsupported automatic edge id, therefore number of edge is wrong +org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddVertexWithPropertyThenPropertyAdded: Unsupported vertex property changed event tracking for addV().property() # shouldWriteToMultiplePartitions org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldWriteToMultiplePartitions: It's not allowed to query by index when there are uncommitted records org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldWriteVerticesToMultiplePartitions: It's not allowed to query by index when there are uncommitted records +org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldPartitionWithAbstractLambdaChildTraversal: Unsupported partition test schema label # assert error, long time, reason: replicated edges treated as one org.apache.tinkerpop.gremlin.process.traversal.step.map.CountTest.Traversals.g_V_both_both_count: Replicated edges treated as one @@ -118,7 +147,7 @@ org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals.g_addVXanimalX_propertyXname_mateoX_propertyXname_gateoX_propertyXname_cateoX_propertyXage_5X: Expect multi properties after setting single property multi times org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals.g_V_addVXanimalX_propertyXname_valuesXnameXX_propertyXname_an_animalX_propertyXvaluesXnameX_labelX: Expect multi properties after setting single property multi times -# pass, long time +## PERF: pass but too slow for fast TinkerPop profile org.apache.tinkerpop.gremlin.process.traversal.step.map.CountTest.Traversals.g_V_repeatXoutX_timesX5X_asXaX_outXwrittenByX_asXbX_selectXa_bX_count: long time org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchTest.CountMatchTraversals.g_V_matchXa_followedBy_count_isXgtX10XX_b__a_0followedBy_count_isXgtX10XX_bX_count: long time org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchTest.CountMatchTraversals.g_V_hasLabelXsongsX_matchXa_name_b__a_performances_cX_selectXb_cX_count: long time @@ -154,22 +183,17 @@ org.apache.tinkerpop.gremlin.process.traversal.step.ComplexTest.Traversals.class org.apache.tinkerpop.gremlin.process.traversal.step.ComplexTest.Traversals.playlistPaths: long time org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.TreeTest.Traversals.g_V_out_out_treeXaX_capXaX: long time -# Unsupported query +## HG-LEGACY/TP37-SEMANTICS/FOLLOW-UP: unsupported has predicates and empty-id semantics org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasIdXwithoutXemptyXX_count: Unsupported query 'NOT IN []' org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasIdXemptyX_count: Unsupported query 'hasId(EmptyList)' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_containingXarkXX: Unsupported predicate 'containing(ark)' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_endingWithXasXX: Unsupported predicate 'endingWith(as)' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_startingWithXmarXX: Unsupported predicate 'startingWith(mar)' org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXage_withoutX27X_count: Unsupported relation 'NEQ' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXperson_name_containingXoX_andXltXmXXX: Unsupported predicate 'containing(o)' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_startingWithXmarXX: Unsupported predicate 'notStartingWith(mar)' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_containingXarkXX: Unsupported predicate 'notContaining(ark)' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_endingWithXasXX: Unsupported predicate 'notEndingWith(as)' org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXage_withoutX27_29X_count: Unsupported relation 'NEQ' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_gtXmX_andXcontainingXoXXX: Unsupported predicate 'containing(o)' org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXp_neqXvXX: Don't accept query based on properties [p] that are not indexed in any label, may not match not-equal condition +org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategyProcessTest.shouldSetIdOnAddEWithNamePropertyKeySpecifiedAndNameSuppliedAsProperty: Unsupported ElementIdStrategy edge id/property index query +org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategyProcessTest.shouldGenerateDefaultIdOnGraphAddVWithGeneratedCustomId: Unsupported ElementIdStrategy generated custom id schema + # Unsupport edge label 'created': 'software' -> 'person', has existed an edgelabel (created: person -> software) in this case org.apache.tinkerpop.gremlin.process.traversal.step.map.AddEdgeTest.Traversals.g_V_hasXname_markoX_asXaX_outEXcreatedX_asXbX_inV_addEXselectXbX_labelX_toXaX: Unsupport edge from inV to outV diff --git a/hugegraph-server/hugegraph-test/src/main/resources/methods.filter b/hugegraph-server/hugegraph-test/src/main/resources/methods.filter index d4b7c3e787..33c4425e86 100644 --- a/hugegraph-server/hugegraph-test/src/main/resources/methods.filter +++ b/hugegraph-server/hugegraph-test/src/main/resources/methods.filter @@ -16,40 +16,37 @@ # limitations under the License. # #################### structure suite #################### +## Filter reason categories: +## HG-LEGACY: Existing HugeGraph feature boundary before this upgrade +## TP37-SEMANTICS: New or renamed TinkerPop 3.7 suite expectation +## TEST-INFRA: Test harness/storage lifecycle issue, not a feature regression +## FOLLOW-UP: Keep tracked for later behavior or infrastructure cleanup -## automatic ID for edges +## HG-LEGACY: automatic ID for edges org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdgeTest.shouldNotEvaluateToEqualDifferentId: Not support automatic/custom id for edge org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldNotEvaluateToEqualDifferentId: Not support automatic/custom id for edge org.apache.tinkerpop.gremlin.structure.EdgeTest.BasicEdgeTest.shouldValidateIdEquality: Not support automatic/custom id for edge org.apache.tinkerpop.gremlin.structure.EdgeTest.BasicEdgeTest.shouldValidateEquality: Not support automatic/custom id for edge -## property ID +## HG-LEGACY: property ID org.apache.tinkerpop.gremlin.structure.util.detached.DetachedPropertyTest.shouldNotBeEqualPropertiesAsThereIsDifferentKey: Not support property id -## id should be String type +## HG-LEGACY: id should be String type org.apache.tinkerpop.gremlin.structure.GraphTest.shouldAddVertexWithUserSuppliedStringId: expect vertex.id is String type -## same id for element +## HG-LEGACY: same id for element org.apache.tinkerpop.gremlin.structure.GraphTest.shouldHaveExceptionConsistencyWhenAssigningSameIdOnVertex: Assigning the same ID to an Element is accepted(override), but tinkerpop expect throw an exception -## uniform list -org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertexOnAdd[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list -org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnEdgeOnAdd[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list -org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertex[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list -org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnEdge[supportsUniformListValues([100, 200, 300])]: Not support uniform of Integer because define uniform as String list -org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertexOnAdd[supportsUniformListValues([try1, try2])]: Uniform list can't be accessed by vertex.property() which is supposed right by tinkerpop -org.apache.tinkerpop.gremlin.structure.PropertyTest.PropertyFeatureSupportTest.shouldSetValueOnVertex[supportsUniformListValues([try1, try2])]: Uniform list can't be accessed by vertex.property() which is supposed right by tinkerpop - -# expect multi properties after setting single property multi times +## HG-LEGACY: expect multi properties after setting single property multi times org.apache.tinkerpop.gremlin.structure.GraphTest.shouldOverwriteEarlierKeyValuesWithLaterKeyValuesOnAddVertexIfMultiProperty: Expect multi properties after setting single property multi times org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyRemoval.shouldRemoveMultiProperties: Expect multi properties after setting single property multi times org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyRemoval.shouldRemoveMultiPropertiesWhenVerticesAreRemoved: Expect multi properties after setting single property multi times org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyAddition.shouldAllowIdenticalValuedMultiProperties: Expect multi properties after setting single property multi times -# not support nested property +## HG-LEGACY: not support nested property org.apache.tinkerpop.gremlin.structure.VertexPropertyTest.VertexPropertyRemoval.shouldAllowIteratingAndRemovingVertexPropertyProperties: Not support nested property -## failed tests with wrong edge number, reason: replicated edges treated as one +## HG-LEGACY: failed tests with wrong edge number, reason: replicated edges treated as one org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.ProcessorTest.shouldProcessEdges: Replicated edges treated as one org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateSameGraph[test(NormalDistribution{stdDeviation=2.0, mean=0.0},NormalDistribution{stdDeviation=2.0, mean=0.0})]: Replicated edges treated as one org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateDifferentGraph[test(NormalDistribution{stdDeviation=2.0, mean=0.0},NormalDistribution{stdDeviation=2.0, mean=0.0})]: Replicated edges treated as one @@ -64,27 +61,56 @@ org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.Diffe org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateSameGraph[test(PowerLawDistribution{gamma=2.3, multiplier=0.0},PowerLawDistribution{gamma=2.8, multiplier=0.0})]: Replicated edges treated as one org.apache.tinkerpop.gremlin.algorithm.generator.DistributionGeneratorTest.DifferentDistributionsTest.shouldGenerateDifferentGraph[test(PowerLawDistribution{gamma=2.3, multiplier=0.0},PowerLawDistribution{gamma=2.8, multiplier=0.0})]: Replicated edges treated as one -## user supplied numeric id +## HG-LEGACY: user supplied numeric id org.apache.tinkerpop.gremlin.structure.FeatureSupportTest.VertexFunctionalityTest.shouldSupportUserSuppliedIdsOfTypeNumericLong: Not open the feature 'support user supplied id of numeric long' although numeric long id is allowed org.apache.tinkerpop.gremlin.structure.FeatureSupportTest.VertexFunctionalityTest.shouldSupportUserSuppliedIdsOfTypeNumericInt: Not open the feature 'support user supplied id of numeric long' although numeric long id is allowed -## user supplied uuid id +## HG-LEGACY/FOLLOW-UP: user supplied uuid id feature flag remains closed org.apache.tinkerpop.gremlin.structure.FeatureSupportTest.VertexFunctionalityTest.shouldSupportUserSuppliedIdsOfTypeUuid: Not open the feature 'support user supplied id of uuid' although uuid id is allowed -## edge id format validate firstly and throw NotFoundException if invalid +## HG-LEGACY/FOLLOW-UP: edge id format validation throws NotFoundException first org.apache.tinkerpop.gremlin.structure.GraphTest.shouldHaveExceptionConsistencyWhenFindEdgeByIdThatIsNonExistentViaIterator: Invalid format of edge id will introduce throwing NotFoundException before try to query in backend store -## not bugs +## HG-LEGACY/TEST-INFRA/FOLLOW-UP: existing non-bug behavior and long-running transaction case org.apache.tinkerpop.gremlin.structure.GraphTest.shouldRemoveVertices: Random UUID as edge label org.apache.tinkerpop.gremlin.structure.GraphTest.shouldRemoveEdges: Random UUID as edge label org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldExecuteWithCompetingThreads: Hang the tests because property 'test' have both String and long value -## vertex properties doesn't have nested structures with HugeVertexSerializer +## TEST-INFRA/FOLLOW-UP: gryo-v3 migration tests reopen source graph path before RocksDB releases lock +org.apache.tinkerpop.gremlin.structure.io.IoGraphTest.shouldMigrateModernGraph[gryo-v3]: RocksDB lock not released between source and target graph in gryo-v3 migration test +org.apache.tinkerpop.gremlin.structure.io.IoGraphTest.shouldMigrateClassicGraph[gryo-v3]: RocksDB lock not released between source and target graph in gryo-v3 migration test + +## TP37-SEMANTICS/FOLLOW-UP: TinkerPop 3.7 transaction lifecycle checks spawned thread transactions +org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldExecuteCompetingThreadsOnMultipleDbInstances: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close +org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldAllowReferenceOfEdgeIdOutsideOfOriginalThreadManual: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close +org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldSupportTransactionIsolationCommitCheck: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close +org.apache.tinkerpop.gremlin.structure.TransactionTest.shouldAllowReferenceOfVertexIdOutsideOfOriginalThreadManual: HugeGraph uses ThreadLocal transactions, spawned thread tx not auto-closed on graph close + +## HG-LEGACY/TP37-SEMANTICS/FOLLOW-UP: TransactionMultiThreadedTest assumes spawned transaction isolation and cleanup +org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldCommit: Memory backend exposes uncommitted vertices across ThreadLocal transactions and spawned tx stays open +org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldDeleteVertexOnCommit: Spawned read transaction is not auto-closed after thread exit +org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldDeleteRelatedEdgesOnVertexDelete: Spawned verification transaction is not auto-closed after thread exit +org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldRollbackAddedVertex: Memory backend exposes uncommitted vertices and spawned tx stays open +org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldCommitEdge: Spawned read transactions are not auto-closed after thread exit +org.apache.tinkerpop.gremlin.structure.TransactionMultiThreadedTest.shouldRollbackAddedEdge: Spawned read transactions are not auto-closed after thread exit + +## HG-MODEL/FOLLOW-UP: scenarios supply custom vertex ids while the standard test graph uses AUTOMATIC id strategy +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person' +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person' +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadasX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person' +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_injectXlabel_knows_out_marko_in_vadasX_mergeE: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person' +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeE_with_outV_inV_options: Can't customize vertex id when id strategy is 'AUTOMATIC' for vertex label 'person' + +## HG-MODEL/FOLLOW-UP: HugeGraph multi-edge labels require a non-null sort key on every parallel edge +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_mergeEXlabel_knows_out_marko_in_vadas_weight_05X_exists: Scenario requires two same-label parallel edges while one edge has no sort-key value +org.apache.tinkerpop.gremlin.process.traversal.step.map.MergeEdgeTest.Traversals.g_V_hasXperson_name_marko_X_mergeEXlabel_knowsX_optionXonCreate_created_YX_optionXonMatch_created_NX_exists_updated: Scenario requires two same-label parallel edges while one edge has no sort-key value + +## HG-LEGACY/FOLLOW-UP: vertex properties don't have nested structures with HugeVertexSerializer org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializeVertex: Vertex properties doesn't have nested structures with HugeVertexSerializer org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializePath: Vertex properties doesn't have nested structures with HugeVertexSerializer org.apache.tinkerpop.gremlin.structure.SerializationTest.GraphSONTest.shouldSerializeTree: Vertex properties doesn't have nested structures with HugeVertexSerializer -## update property but no commit, lead there are changes in indexTx, can't do index query +## HG-LEGACY: update property but no commit leaves changes in indexTx org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdgeTest.shouldConstructDetachedEdge: Can't do index query when there are changes in transaction org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdgeTest.shouldConstructDetachedEdgeAsReference: Can't do index query when there are changes in transaction org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldConstructReferenceEdge: Can't do index query when there are changes in transaction @@ -94,9 +120,11 @@ org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdgeTest.shouldCo # unsupported automatic edge id, therefore number of edge is wrong org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdge: Unsupported automatic edge id, therefore number of edge is wrong org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddEdgeByPath: Unsupported automatic edge id, therefore number of edge is wrong +org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategyProcessTest.shouldTriggerAddVertexWithPropertyThenPropertyAdded: Unsupported vertex property changed event tracking for addV().property() # shouldWriteToMultiplePartitions org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldWriteToMultiplePartitions: It's not allowed to query by index when there are uncommitted records org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldWriteVerticesToMultiplePartitions: It's not allowed to query by index when there are uncommitted records +org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategyProcessTest.shouldPartitionWithAbstractLambdaChildTraversal: Unsupported partition test schema label # assert error, long time, reason: replicated edges treated as one org.apache.tinkerpop.gremlin.process.traversal.step.map.CountTest.Traversals.g_V_both_both_count: Replicated edges treated as one @@ -117,22 +145,17 @@ org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals.g_addVXanimalX_propertyXname_mateoX_propertyXname_gateoX_propertyXname_cateoX_propertyXage_5X: Expect multi properties after setting single property multi times org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexTest.Traversals.g_V_addVXanimalX_propertyXname_valuesXnameXX_propertyXname_an_animalX_propertyXvaluesXnameX_labelX: Expect multi properties after setting single property multi times -# unsupported predicate +## HG-LEGACY/TP37-SEMANTICS/FOLLOW-UP: unsupported has predicates and empty-id semantics org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasIdXwithoutXemptyXX_count: Unsupported query 'NOT IN []' org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasIdXemptyX_count: Unsupported query 'hasId(EmptyList)' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_containingXarkXX: Unsupported predicate 'containing(ark)' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_endingWithXasXX: Unsupported predicate 'endingWith(as)' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_startingWithXmarXX: Unsupported predicate 'startingWith(mar)' org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXage_withoutX27X_count: Unsupported relation 'NEQ' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXperson_name_containingXoX_andXltXmXXX: Unsupported predicate 'containing(o)' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_startingWithXmarXX: Unsupported predicate 'notStartingWith(mar)' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_containingXarkXX: Unsupported predicate 'notContaining(ark)' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_not_endingWithXasXX: Unsupported predicate 'notEndingWith(as)' org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXage_withoutX27_29X_count: Unsupported relation 'NEQ' -org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXname_gtXmX_andXcontainingXoXXX: Unsupported predicate 'containing(o)' org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasTest.Traversals.g_V_hasXp_neqXvXX: Don't accept query based on properties [p] that are not indexed in any label, may not match not-equal condition +org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategyProcessTest.shouldSetIdOnAddEWithNamePropertyKeySpecifiedAndNameSuppliedAsProperty: Unsupported ElementIdStrategy edge id/property index query +org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategyProcessTest.shouldGenerateDefaultIdOnGraphAddVWithGeneratedCustomId: Unsupported ElementIdStrategy generated custom id schema + # Unsupport edge label 'created': 'software' -> 'person', has existed an edgelabel (created: person -> software) in this case org.apache.tinkerpop.gremlin.process.traversal.step.map.AddEdgeTest.Traversals.g_V_hasXname_markoX_asXaX_outEXcreatedX_asXbX_inV_addEXselectXbX_labelX_toXaX: Unsupport edge from inV to outV diff --git a/hugegraph-server/hugegraph-test/src/test/rocksdb-compatibility/RocksDBCompatibilityTest.java b/hugegraph-server/hugegraph-test/src/test/rocksdb-compatibility/RocksDBCompatibilityTest.java new file mode 100644 index 0000000000..7d52e2ba3e --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/test/rocksdb-compatibility/RocksDBCompatibilityTest.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. + */ + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import org.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.ColumnFamilyOptions; +import org.rocksdb.DBOptions; +import org.rocksdb.FlushOptions; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksIterator; +import org.rocksdb.WriteOptions; + +/** RocksDB SST/WAL compatibility test. Each phase must run in a separate JVM. */ +public final class RocksDBCompatibilityTest { + + private static int assertions; + + public static void main(String[] args) throws Exception { + if (args.length != 4) { + throw new IllegalArgumentException("Usage: "); + } + String phase = args[0]; + if (!Arrays.asList("seed", "upgrade", "verify").contains(phase)) { + throw new IllegalArgumentException("Unknown phase: " + phase); + } + Path jar = Paths.get(RocksDB.class.getProtectionDomain().getCodeSource() + .getLocation().toURI()).toRealPath(); + require(jar.equals(Paths.get(args[3]).toRealPath()), "CodeSource matches explicit JAR"); + System.out.println("CodeSource=" + jar + " sha256=" + sha256(jar)); + try { + Class.forName("org.rocksdb.SidePluginRepo", false, RocksDB.class.getClassLoader()); + throw new IllegalStateException("This fixture requires standard RocksDB, not a TP JAR"); + } catch (ClassNotFoundException expected) { + assertions++; + } + RocksDB.loadLibrary(); + String version = RocksDB.rocksdbVersion().toString(); + System.out.println("nativeVersion=" + version); + require(version.equals(args[2]), "native version matches requested version"); + Path path = Paths.get(args[1]); + boolean seed = "seed".equals(phase); + require(seed ? !Files.exists(path) : Files.isDirectory(path), "database path precondition"); + List handles = new ArrayList<>(); + try (DBOptions options = new DBOptions().setCreateIfMissing(seed) + .setCreateMissingColumnFamilies(seed); + ColumnFamilyOptions cfOptions = new ColumnFamilyOptions().setDisableAutoCompactions(true); + WriteOptions write = new WriteOptions().setSync(true); + FlushOptions flush = new FlushOptions().setWaitForFlush(true); + RocksDB db = RocksDB.open(options, path.toString(), Arrays.asList( + new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, cfOptions), + new ColumnFamilyDescriptor(bytes("compatibility-fixture"), cfOptions)), handles)) { + try { + for (ColumnFamilyHandle handle : handles) { + if (seed) { + put(db, handle, write, "sst-keep", "old"); + put(db, handle, write, "sst-update", "old"); + put(db, handle, write, "sst-delete", "old"); + db.flush(flush, handle); + put(db, handle, write, "wal-keep", "old"); + put(db, handle, write, "wal-delete", "old"); + require(db.getLongProperty(handle, "rocksdb.num-entries-active-mem-table") == 2, + "two unflushed entries in active memtable"); + } else if ("upgrade".equals(phase)) { + check(db, handle, false); + put(db, handle, write, "sst-update", "new"); + db.delete(handle, write, bytes("sst-delete")); + db.delete(handle, write, bytes("wal-delete")); + put(db, handle, write, "new-key", "new"); + check(db, handle, true); + db.flush(flush, handle); + } else { + check(db, handle, true); + } + } + if (seed) { + db.flushWal(true); + require(hasFile(path, ".sst"), "flushed SST exists"); + require(hasFile(path, ".log"), "nonempty WAL exists"); + // Deliberately skip close: preserve WAL-only writes for recovery by the new version. + System.out.printf("PASS phase=seed columnFamilies=2 records=10 assertions=%d " + + "exit=halt(0) shutdownFlush=false%n", assertions); + System.out.flush(); + Runtime.getRuntime().halt(0); + } + } finally { + for (ColumnFamilyHandle handle : handles) { + handle.close(); + } + } + } + System.out.printf("PASS phase=%s columnFamilies=2 records=8 assertions=%d%n", phase, assertions); + } + + private static void check(RocksDB db, ColumnFamilyHandle cf, boolean upgraded) throws Exception { + expect(db, cf, "sst-keep", "old"); + expect(db, cf, "sst-update", upgraded ? "new" : "old"); + expect(db, cf, "sst-delete", upgraded ? null : "old"); + expect(db, cf, "wal-keep", "old"); + expect(db, cf, "wal-delete", upgraded ? null : "old"); + expect(db, cf, "new-key", upgraded ? "new" : null); + int count = 0; + try (RocksIterator iterator = db.newIterator(cf)) { + for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) { + count++; + } + iterator.status(); + } + require(count == (upgraded ? 4 : 5), "exact iterator record count"); + } + + private static void expect(RocksDB db, ColumnFamilyHandle cf, String key, String value) throws Exception { + require(Arrays.equals(value == null ? null : bytes(value), db.get(cf, bytes(key))), "value for " + key); + } + + private static void put(RocksDB db, ColumnFamilyHandle cf, WriteOptions write, + String key, String value) throws Exception { + db.put(cf, write, bytes(key), bytes(value)); + } + + private static boolean hasFile(Path directory, String suffix) throws Exception { + try (Stream files = Files.list(directory)) { + return files.anyMatch(file -> file.toString().endsWith(suffix) && file.toFile().length() > 0); + } + } + + private static String sha256(Path path) throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (InputStream input = Files.newInputStream(path)) { + byte[] buffer = new byte[8192]; + int length; + while ((length = input.read(buffer)) != -1) { + digest.update(buffer, 0, length); + } + } + StringBuilder hex = new StringBuilder(); + for (byte value : digest.digest()) { + hex.append(String.format("%02x", value & 0xff)); + } + return hex.toString(); + } + + private static void require(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + assertions++; + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } + + private RocksDBCompatibilityTest() { + } +} diff --git a/hugegraph-server/hugegraph-test/src/test/rocksdb-compatibility/run.sh b/hugegraph-server/hugegraph-test/src/test/rocksdb-compatibility/run.sh new file mode 100755 index 0000000000..c60cc01647 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/test/rocksdb-compatibility/run.sh @@ -0,0 +1,112 @@ +#!/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. +# + +set -euo pipefail +if [[ $# -ne 3 ]]; then + echo "Usage: $0 " >&2 + exit 2 +fi +script_dir=$(cd "$(dirname "$0")" && pwd) +before=$(cd "$1" && pwd) +after=$(cd "$2" && pwd) +# A new directory contains only synthetic test data; never reuse a previous run. +mkdir "$3" +output=$(cd "$3" && pwd) +mkdir "$output/models" "$output/jars" "$output/cases" +exec > >(tee "$output/run.log") 2>&1 +trap 'result=$?; echo "RESULT exit=$result output=$output"' EXIT +run() { + local result + printf 'COMMAND '; printf '%q ' "$@"; printf '\n' + if "$@"; then result=0; else result=$?; fi + echo "COMMAND_EXIT=$result" + return "$result" +} +for variable in JAVA_TOOL_OPTIONS JDK_JAVA_OPTIONS _JAVA_OPTIONS LD_PRELOAD DYLD_INSERT_LIBRARIES; do + if [[ -n "${!variable:-}" ]]; then + echo "Unset $variable before running the standard RocksDB compatibility test" >&2 + exit 2 + fi +done +# Maven resolves properties and inheritance. Only these three owner modules matter. +for side in before after; do + source_dir=${!side} + for entry in server:hugegraph-server/hugegraph-rocksdb pd:hugegraph-pd/hg-pd-core \ + store:hugegraph-store/hg-store-rocksdb; do + component=${entry%%:*} + module=${entry#*:} + (cd "$source_dir" && run mvn -B -ntp -N -f "$module/pom.xml" \ + org.apache.maven.plugins:maven-help-plugin:3.2.0:effective-pom \ + "-Doutput=$output/models/$component-$side.xml") + done +done +python3 - "$output" <<'PYTHON' +from pathlib import Path +import re +import sys +import xml.etree.ElementTree as ET + +output = Path(sys.argv[1]) +ns = {'m': 'http://maven.apache.org/POM/4.0.0'} +pairs = {} +for component in ('server', 'pd', 'store'): + versions = [] + for side in ('before', 'after'): + model = ET.parse(output / 'models' / f'{component}-{side}.xml').getroot() + matches = [dep.findtext('m:version', namespaces=ns) + for dep in model.findall('m:dependencies/m:dependency', ns) + if dep.findtext('m:groupId', namespaces=ns) == 'org.rocksdb' + and dep.findtext('m:artifactId', namespaces=ns) == 'rocksdbjni'] + if len(matches) != 1 or not re.fullmatch(r'[0-9][0-9A-Za-z._-]*', matches[0] or ''): + raise SystemExit(f'Cannot resolve one concrete RocksDB version: {component} {side}: {matches}') + versions.append(matches[0]) + print(f'VERSION {component}: {versions[0]} -> {versions[1]}') + if versions[0] != versions[1]: + pairs.setdefault(tuple(versions), []).append(component) +with (output / 'pairs.tsv').open('w') as stream: + for (old, new), components in pairs.items(): + stream.write(f'{old}\t{new}\t{",".join(components)}\n') +PYTHON +if [[ ! -s "$output/pairs.tsv" ]]; then + echo "SKIP: actual RocksDB versions are unchanged in all three components" + exit 0 +fi +# Fetch only versions in this change; inputs are not part of uploaded evidence. +cut -f1,2 "$output/pairs.tsv" | tr '\t' '\n' | sort -u > "$output/versions.txt" +while read -r version; do + (cd "$after" && run mvn -B -ntp -N dependency:copy \ + "-Dartifact=org.rocksdb:rocksdbjni:$version" "-DoutputDirectory=$output/jars") +done < "$output/versions.txt" +pairs=0 +while IFS=$'\t' read -r old new components; do + case_dir="$output/cases/$old-to-$new" + mkdir -p "$case_dir/classes" "$case_dir/empty-native-path" + old_jar="$output/jars/rocksdbjni-$old.jar" + new_jar="$output/jars/rocksdbjni-$new.jar" + echo "PAIR $old -> $new components=$components" + run javac -cp "$old_jar" -d "$case_dir/classes" "$script_dir/RocksDBCompatibilityTest.java" + run java -Djava.library.path="$case_dir/empty-native-path" -cp "$case_dir/classes:$old_jar" \ + RocksDBCompatibilityTest seed "$case_dir/original" "$old" "$old_jar" + run cp -R "$case_dir/original" "$case_dir/reopened" + run java -Djava.library.path="$case_dir/empty-native-path" -cp "$case_dir/classes:$new_jar" \ + RocksDBCompatibilityTest upgrade "$case_dir/reopened" "$new" "$new_jar" + run java -Djava.library.path="$case_dir/empty-native-path" -cp "$case_dir/classes:$new_jar" \ + RocksDBCompatibilityTest verify "$case_dir/reopened" "$new" "$new_jar" + pairs=$((pairs + 1)) +done < "$output/pairs.tsv" +echo "PASS compatibilityPairs=$pairs JVMPhases=$((pairs * 3)) scope=synthetic-SST-WAL-only" diff --git a/hugegraph-server/pom.xml b/hugegraph-server/pom.xml index a9169591a3..7fff054fe2 100644 --- a/hugegraph-server/pom.xml +++ b/hugegraph-server/pom.xml @@ -42,7 +42,7 @@ 1.2.17 2.17.1 4.13.1 - 3.5.1 + 3.8.1 2.7 25.1-jre 4.5.13 @@ -51,10 +51,8 @@ 3.21.0-GA 3.1.2 8.45 - 1.47.0 3.21.7 1.36 - 3.7.1 2.2.18 @@ -158,12 +156,12 @@ org.apache.tinkerpop - gremlin-groovy-test - 3.2.11 + gremlin-driver + ${tinkerpop.version} org.apache.tinkerpop - gremlin-driver + gremlin-util ${tinkerpop.version} @@ -239,7 +237,7 @@ com.google.protobuf protobuf-java - ${protobuf.version} + ${protobuf.runtime.version} diff --git a/hugegraph-store/Dockerfile b/hugegraph-store/Dockerfile index 988b472b09..9e6fbdf183 100644 --- a/hugegraph-store/Dockerfile +++ b/hugegraph-store/Dockerfile @@ -18,7 +18,7 @@ # Dockerfile for HugeGraph Store # 1st stage: build source code -FROM --platform=$BUILDPLATFORM maven:3.9.0-eclipse-temurin-11 AS build +FROM --platform=$BUILDPLATFORM maven:3.9.16-eclipse-temurin-17 AS build WORKDIR /pkg @@ -35,7 +35,8 @@ RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sh # 2nd stage: runtime env # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13 -FROM eclipse-temurin:11-jre-jammy +FROM eclipse-temurin:17-jdk-jammy +# Local Arthas attach requires the JDK attach and compiler modules. LABEL maintainer="HugeGraph Docker Maintainers " diff --git a/hugegraph-store/README.md b/hugegraph-store/README.md index f12df4427e..3615649ff1 100644 --- a/hugegraph-store/README.md +++ b/hugegraph-store/README.md @@ -3,6 +3,8 @@ [![License](https://img.shields.io/badge/license-Apache%202-0E78BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) [![Version](https://img.shields.io/badge/version-1.7.0-blue)](https://github.com/apache/hugegraph) + + > **Note**: From revision 1.5.0, the HugeGraph-Store code has been adapted to this location. ## Overview @@ -20,10 +22,10 @@ HugeGraph Store is a distributed storage backend for HugeGraph that provides hig ### Technology Stack -- **Storage Engine**: RocksDB 7.7.3 (optimized for graph workloads) +- **Storage Engine**: RocksDB 8.10.2 (see the [upgrade guidance](../docs/storage-lifecycle.md#runtime-compatibility-and-upgrades)) - **Consensus Protocol**: Apache JRaft (Ant Financial's Raft implementation) - **RPC Framework**: gRPC + Protocol Buffers -- **Deployment**: Java 11+, Docker/Kubernetes support +- **Deployment**: Java 17 (currently supported release), Docker/Kubernetes support ### When to Use HugeGraph Store @@ -87,8 +89,8 @@ For detailed architecture, Raft consensus mechanisms, and partition management, ### Prerequisites -- **Java**: 11 or higher -- **Maven**: 3.5 or higher +- **Java**: 17 (currently the only supported release) +- **Maven**: 3.6.3 or higher - **HugeGraph PD Cluster**: Store requires a running PD cluster for metadata coordination (see [PD README](../hugegraph-pd/README.md)) - **Disk Space**: At least 10GB per Store node for data and Raft logs - **Network**: Low-latency network (<5ms) between Store nodes for Raft consensus @@ -112,6 +114,15 @@ The assembled distribution will be available at: hugegraph-store/apache-hugegraph-store-/lib/hg-store-node-.jar ``` +The Node module keeps a regular JAR for development dependencies and attaches its +Spring Boot executable as `hg-store-node--exec.jar`. The distribution +uses the executable JAR with the filename shown above, so the start script is +unchanged. To run the Store server tests through the package phase: + +```bash +mvn clean package -pl hugegraph-store/hg-store-test -am -Pstore-server-test +``` + ### Configuration Extract the distribution package and edit `conf/application.yml`: @@ -234,10 +245,21 @@ bin/restart-hugegraph-store.sh bin/start-hugegraph-store.sh [-g GC_TYPE] [-j "JVM_OPTIONS"] [-d DAEMON] ``` -- `-g`: GC type (`g1` or `ZGC`, default: `g1`) +- `-g`: Explicit GC selection (`g1`/`G1` or `zgc`/`ZGC`). Without `-g`, JVM defaults or the collector + set in `JAVA_OPTIONS` apply. Explicit `-g g1` selects G1 even when JVM ergonomics would choose Serial GC; + unknown values are rejected. When using explicit `-g`, do not select another collector in `JAVA_OPTIONS` + or `-j`; conflicting selections are rejected by the JVM. - `-j`: Custom JVM options (e.g., `-j "-Xmx16g -Xms8g"`) - `-d`: Daemon mode (`true` = daemon, `false` = foreground; default: `true`). Set to `false` when running under Docker or a process supervisor so the container exits if Java dies. +The launcher always loads `bin/jvm-module.options`, including with custom `JAVA_OPTIONS` or `-j`. +Keep this file in the distribution: it grants the Java 17 access required by Hessian Raft serialization; +startup fails if the file is missing or unreadable. Direct Java launches must include `@bin/jvm-module.options`. + +On supported architectures, startup reuses a checksum-verified jemalloc library or tries to download it. +The optional download allows 5 seconds to connect and 30 seconds for the complete transfer. If it fails +or times out, Store continues without adding jemalloc to `LD_PRELOAD`; partial downloads are discarded. + Default JVM memory settings (defined in `start-hugegraph-store.sh`): - Max heap: 32GB - Min heap: 512MB @@ -386,8 +408,19 @@ For Docker and Kubernetes deployment details, see [Deployment Guide](docs/deploy --- +## Stopping a Store node + +See [Store-wide shutdown](../docs/storage-lifecycle.md#store-node-shutdown) for stop commands, +timeout handling, diagnostics and resource drain behavior. + ## Documentation +For offline diagnosis of a running node without restarting it, see +[Arthas diagnostics](../docs/server-runtime.md#arthas-diagnostics). + +See the [storage lifecycle guide](../docs/storage-lifecycle.md) for request ownership, +shutdown, recovery and provider configuration. + Comprehensive documentation for HugeGraph Store: | Documentation | Description | diff --git a/hugegraph-store/docs/deployment-guide.md b/hugegraph-store/docs/deployment-guide.md index ca39ae509c..3d4f798a59 100644 --- a/hugegraph-store/docs/deployment-guide.md +++ b/hugegraph-store/docs/deployment-guide.md @@ -1,5 +1,7 @@ # Deployment Guide + + This guide provides comprehensive instructions for deploying HugeGraph Store in various environments, from development to production clusters. > **PD REST credential.** Calls to a PD REST endpoint on port 8620, other than `/v1/health`, `/v1/ready`, `/actuator/**` and `/v1/prom/targets/*`, need HTTP Basic auth: one of the internal service names (`hg`, `store`, `hubble`, `vermeer`) and PD's `auth.secret-key` value as the password. A call without it gets HTTP 401 and a `{"status":-1,"error":"Unauthorized"}` body, not the payloads shown below. Export the secret before following a step that uses `${PD_SECRET}`: @@ -392,7 +394,7 @@ graph.name=hugegraph **On all nodes**: ```bash -# Check Java version (11+ required) +# Check Java version (Java 17 is the only currently supported release) java -version # Check Maven (for building from source) diff --git a/hugegraph-store/docs/development-guide.md b/hugegraph-store/docs/development-guide.md index 44136776e7..63d6be8727 100644 --- a/hugegraph-store/docs/development-guide.md +++ b/hugegraph-store/docs/development-guide.md @@ -18,8 +18,8 @@ Comprehensive guide for developing, testing, and contributing to HugeGraph Store ### Prerequisites **Required**: -- Java: 11 or higher (OpenJDK or Oracle JDK) -- Maven: 3.5 or higher +- Java: 17 (currently the only supported release; OpenJDK or Oracle JDK) +- Maven: 3.6.3 or higher - Git: Latest version - IDE: IntelliJ IDEA (recommended) or Eclipse @@ -303,12 +303,12 @@ mvn test -P store-core-test **Specific test class**: ```bash -mvn test -Dtest=HgStoreEngineTest +mvn test -pl hg-store-test -am -Dtest=HgStoreEngineTest ``` **Specific test method**: ```bash -mvn test -Dtest=HgStoreEngineTest#testPartitionCreation +mvn test -pl hg-store-test -am -Dtest=HgStoreEngineTest#testPartitionCreation ``` **From IntelliJ**: @@ -796,11 +796,17 @@ When adding third-party dependencies: 5. Update `install-dist/scripts/dependency/known-dependencies.txt` **Run Dependency Check**: + +Run from the repository root after building Server, PD and Store distributions for the current revision: + ```bash -cd install-dist/scripts/dependency -./regenerate_known_dependencies.sh +mvn install -DskipTests -Dmaven.javadoc.skip=true +bash install-dist/scripts/dependency/regenerate_known_dependencies.sh current-dependencies.txt +bash install-dist/scripts/dependency/check_dependencies.sh ``` +This build prepares artifacts without running tests. The collector combines the Maven runtime union with actual distribution libraries, including nested Spring Boot jars; missing current-revision distribution libraries fail. Review both added and removed names against `known-dependencies.txt`, including license/NOTICE changes, before updating the approved list. Comparison is strict, and source-only inspection is insufficient; verify the release platform/profile variants too. + ### Documentation **When to Update Docs**: diff --git a/hugegraph-store/docs/integration-guide.md b/hugegraph-store/docs/integration-guide.md index 13164872e0..9ead2464cb 100644 --- a/hugegraph-store/docs/integration-guide.md +++ b/hugegraph-store/docs/integration-guide.md @@ -1,5 +1,7 @@ # Integration Guide + + This guide explains how to integrate HugeGraph Store with HugeGraph Server, use the client library, and migrate from other storage backends. > **PD REST credential.** Calls to a PD REST endpoint on port 8620, other than `/v1/health`, `/v1/ready`, `/actuator/**` and `/v1/prom/targets/*`, need HTTP Basic auth: one of the internal service names (`hg`, `store`, `hubble`, `vermeer`) and PD's `auth.secret-key` value as the password. A call without it gets HTTP 401. Export the secret before following a procedure that uses `${PD_SECRET}`: diff --git a/hugegraph-store/docs/operations-guide.md b/hugegraph-store/docs/operations-guide.md index 1df19b8be6..78e1c718b5 100644 --- a/hugegraph-store/docs/operations-guide.md +++ b/hugegraph-store/docs/operations-guide.md @@ -685,6 +685,10 @@ curl http://192.168.1.10:8620/v1/partitionsAndStatus ## Rolling Upgrades + + +The 1.8.0 package paths below illustrate a future release upgrade. Run these steps only after that release is published. + ### Upgrade Strategy **Goal**: Upgrade cluster with zero downtime diff --git a/hugegraph-store/hg-store-cli/pom.xml b/hugegraph-store/hg-store-cli/pom.xml index 5c62664b97..a10f9be327 100644 --- a/hugegraph-store/hg-store-cli/pom.xml +++ b/hugegraph-store/hg-store-cli/pom.xml @@ -33,7 +33,7 @@ org.springframework.boot spring-boot-starter - 2.5.14 + ${spring-boot.version} org.springframework.boot diff --git a/hugegraph-store/hg-store-client/pom.xml b/hugegraph-store/hg-store-client/pom.xml index 736bc36790..ae5089af8e 100644 --- a/hugegraph-store/hg-store-client/pom.xml +++ b/hugegraph-store/hg-store-client/pom.xml @@ -76,7 +76,7 @@ com.google.protobuf protobuf-java-util - 3.17.2 + ${protobuf.runtime.version} @@ -88,7 +88,6 @@ com.fasterxml.jackson.core jackson-databind - 2.13.0 test diff --git a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/NodeTxExecutor.java b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/NodeTxExecutor.java index 594a2f1947..6965a9dafa 100644 --- a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/NodeTxExecutor.java +++ b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/NodeTxExecutor.java @@ -115,6 +115,7 @@ void rollbackTx() { throw t; } finally { this.isTx = false; + this.entries.clear(); this.sessions.clear(); } } diff --git a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/KvBatchScanner.java b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/KvBatchScanner.java index 5b711844ca..ab1a021ff3 100644 --- a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/KvBatchScanner.java +++ b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/KvBatchScanner.java @@ -40,6 +40,7 @@ import org.apache.hugegraph.store.grpc.common.ScanOrderType; import org.apache.hugegraph.store.grpc.stream.HgStoreStreamGrpc; import org.apache.hugegraph.store.grpc.stream.KvStream; +import org.apache.hugegraph.store.grpc.stream.ScanCancelRequest; import org.apache.hugegraph.store.grpc.stream.ScanReceiptRequest; import org.apache.hugegraph.store.grpc.stream.ScanStreamBatchReq; @@ -167,12 +168,16 @@ public void dataArrived(Supplier> supplier) throws * Data reception ended */ public void dataComplete() { - close(); + close(false); } // Flow is closed @Override public void close() { + close(true); + } + + private void close(boolean cancel) { try { if (notifier.unregisterScanner(this) < 0) { notifier.dataArrived(this, NO_DATA); // Task finished, wake up the queue @@ -183,6 +188,12 @@ public void close() { synchronized (this.sender) { try { if (running) { + running = false; + if (cancel) { + sender.onNext(ScanStreamBatchReq.newBuilder() + .setHeader(Header.newBuilder().setGraph(graphName)) + .setCancelRequest(ScanCancelRequest.getDefaultInstance()).build()); + } sender.onCompleted(); } } catch (Exception e) { @@ -344,7 +355,7 @@ public void onCompleted() { private void close() { if (scanner != null) { - scanner.close(); + scanner.dataComplete(); } } } diff --git a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/KvPageScanner.java b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/KvPageScanner.java index 4bf938fa80..3ab1d8c84f 100644 --- a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/KvPageScanner.java +++ b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/KvPageScanner.java @@ -32,6 +32,7 @@ import org.apache.hugegraph.store.HgOwnerKey; import org.apache.hugegraph.store.HgPageSize; import org.apache.hugegraph.store.HgSeekAble; +import org.apache.hugegraph.store.client.HgStoreNodeManager; import org.apache.hugegraph.store.client.HgStoreNodeSession; import org.apache.hugegraph.store.client.util.HgBufferProxy; import org.apache.hugegraph.store.client.util.HgStoreClientConfig; @@ -50,6 +51,7 @@ import com.google.protobuf.ByteString; +import io.grpc.Status; import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; @@ -67,6 +69,8 @@ class KvPageScanner implements KvCloseableIterator, HgPageSize, HgSeekAble { StreamObserver> streamFactory; private final boolean orderedScan; private final AtomicBoolean completed = new AtomicBoolean(false); + // Buffer publication and channel retirement must stay outside this lock. + private final Object requestLock = new Object(); private final SelectParam.Builder selectBuilder = SelectParam.newBuilder(); private final BlockingQueue reqQueue = new LinkedBlockingQueue<>(); private int pageSize = clientConfig.getNetKvScannerPageSize(); @@ -174,7 +178,7 @@ private void init() { private void serverScan() { if (this.completed.get()) { - this.proxy.close(); + // The response callback closes the buffer after publishing its final page or error. return; } if (this.proxy.isClosed()) { @@ -188,19 +192,38 @@ private void stopSever() { } private void send(ScanStreamReq req) { - if (!this.completed.get()) { - try { - this.observer.onNext(req); - } catch (IllegalStateException | IllegalArgumentException e) { + synchronized (this.requestLock) { + if (!this.completed.get()) { + try { + this.observer.onNext(req); + } catch (IllegalStateException | IllegalArgumentException e) { + + } catch (Exception e) { + throw e; + } + } + } + } + private void completeRequest() { + synchronized (this.requestLock) { + if (!this.completed.compareAndSet(false, true)) { + return; + } + try { + this.observer.onCompleted(); } catch (Exception e) { - throw e; + log.warn("failed to invoke requestObserver.onCompleted(), reason: {}", e.getMessage()); } } } private void clientError(String msg) { - this.observer.onError(GrpcUtil.toErr(msg)); + synchronized (this.requestLock) { + if (this.completed.compareAndSet(false, true)) { + this.observer.onError(GrpcUtil.toErr(msg)); + } + } } /*** Iterator ***/ @@ -302,8 +325,7 @@ public void onNext(KvPageRes value) { return; } if (value.getOver()) { - completed.set(true); - observer.onCompleted(); + completeRequest(); } proxy.send(value.getDataList()); if (completed.get()) { @@ -313,11 +335,13 @@ public void onNext(KvPageRes value) { @Override public void onError(Throwable t) { - completed.set(true); - try { - observer.onCompleted(); - } catch (Exception e) { - log.warn("failed to invoke requestObserver.onCompleted(), reason:", e.getMessage()); + completeRequest(); + // Retire the failed pool before publishing the original iterator error, + // so immediate retries cannot reuse it. Cancellation or protocol errors + // must not evict healthy nodes, and stale notices preserve replacements. + if (Status.fromThrowable(t).getCode() == Status.Code.UNAVAILABLE) { + new NotifyingExecutor(session.getGraphName(), + HgStoreNodeManager.getInstance(), session).handleErr(t); } proxy.setError(t); proxy.close(); @@ -326,7 +350,9 @@ public void onError(Throwable t) { @Override public void onCompleted() { - completed.set(true); + synchronized (requestLock) { + completed.set(true); + } proxy.close(); } } diff --git a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/NotifyingExecutor.java b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/NotifyingExecutor.java index 75cbd88c36..352fa02e90 100644 --- a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/NotifyingExecutor.java +++ b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/grpc/NotifyingExecutor.java @@ -114,7 +114,7 @@ Optional invoke(Supplier supplier, Function return option; } - private void handleErr(Throwable t) { + void handleErr(Throwable t) { try { notifyErrConsumer(HgNodeStatus.NOT_WORK).accept(t); } catch (Throwable tt) { diff --git a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/CommonKvStreamObserver.java b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/CommonKvStreamObserver.java index 7abcbf88d0..21da9a39fe 100644 --- a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/CommonKvStreamObserver.java +++ b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/CommonKvStreamObserver.java @@ -18,17 +18,16 @@ package org.apache.hugegraph.store.client.query; import java.util.Iterator; +import java.util.Objects; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Function; import org.apache.hugegraph.store.client.type.HgStoreClientException; import io.grpc.stub.StreamObserver; -import lombok.Data; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -47,7 +46,7 @@ public class CommonKvStreamObserver implements StreamObserver { private Consumer requestSender; /** - * Handling the case that server has no results, close channel + * Close the request stream: true means serialized half-close; false means transport cancellation. */ @Setter private Consumer transferComplete; @@ -61,19 +60,18 @@ public class CommonKvStreamObserver implements StreamObserver { */ private final Function stateWatcher; - /** - * It can be ended by the client to stop receiving redundant data. - */ - private final AtomicBoolean closed = new AtomicBoolean(false); + private final Object stateLock = new Object(); + // Only outgoing sends and normal half-close share this lock. Cancellation bypasses it. + private final Object requestLock = new Object(); + private volatile ResultState terminal; + private volatile int parsing; + private volatile boolean closed; + private volatile long current = System.nanoTime(); + private String queryId; @Setter private long timeout = 1800 * 1000; - /** - * Monitor internal state - */ - private final ResultStateWatcher watcher = new ResultStateWatcher(); - public CommonKvStreamObserver(Function> valueExtractor, Function stateWatcher) { this.queue = new LinkedBlockingQueue<>(); @@ -85,55 +83,89 @@ public CommonKvStreamObserver(Function> valueExtractor, * Send requests */ public void sendRequest() { - if (!isServerFinished() && !closed.get()) { + synchronized (this.requestLock) { + synchronized (this.stateLock) { + if (this.closed || this.terminal != null) { + return; + } + this.current = System.nanoTime(); + } this.requestSender.accept(true); - this.watcher.setState(ResultState.WAITING); } } public boolean isServerFinished() { - return this.watcher.getState() == ResultState.FINISHED - || this.watcher.getState() == ResultState.ERROR; + ResultState state = this.terminal; + return this.closed || state == ResultState.ERROR || + (state == ResultState.FINISHED && this.parsing == 0); } @Override public void onNext(R value) { - watcher.setState(ResultState.INNER_BUSY); + synchronized (this.stateLock) { + if (this.closed || this.terminal != null) { + return; + } + this.parsing++; + this.current = System.nanoTime(); + } + + Iterator iterator = null; + ResultState result = ResultState.ERROR; try { - var state = stateWatcher.apply(value); - log.debug("observer state: {}", state); - - switch (state) { - case IDLE: - case FINISHED: - if (!this.closed.get()) { - queue.offer(this.valueExtractor.apply(value)); + ResultState responseState = this.stateWatcher.apply(value); + if (responseState == ResultState.IDLE || responseState == ResultState.FINISHED) { + iterator = Objects.requireNonNull(this.valueExtractor.apply(value), "response iterator"); + result = responseState; + } else { + iterator = new ErrorMessageIterator<>(responseState.getMessage()); + result = ResultState.ERROR; + } + } catch (Throwable e) { + log.error("handling server data for query {}, got error: ", this.queryId, e); + result = ResultState.ERROR; + iterator = new ErrorMessageIterator<>(e.getMessage()); + if (e instanceof Error) { + throw (Error) e; + } + } finally { + synchronized (this.stateLock) { + // Completion can arrive while parsing. Publish before releasing the parsing count. + // An error or client close instead discards responses still being parsed. + if (!this.closed && this.terminal != ResultState.ERROR) { + if (iterator != null) { + this.queue.offer(iterator); + } + // A failed accepted response overrides completion received during parsing. + if (result == ResultState.ERROR || + (this.terminal == null && result != ResultState.IDLE)) { + this.terminal = result; } - // this.stop(); - break; - default: - queue.offer(new ErrorMessageIterator<>(state.getMessage())); - break; + } + this.parsing--; + this.current = System.nanoTime(); } - watcher.setState(state); - // sendRequest(); - } catch (Exception e) { - log.error("handling server data, got error: ", e); - queue.offer(new ErrorMessageIterator<>(e.getMessage())); } } public Iterator consume() { try { - while (!Thread.currentThread().isInterrupted() && (!this.queue.isEmpty() || - !isServerFinished())) { + while (!Thread.currentThread().isInterrupted()) { + // Read terminal state before the queue, including a concurrently published final batch. + if (isServerFinished() && this.queue.isEmpty()) { + return null; + } var iterator = this.queue.poll(200, TimeUnit.MILLISECONDS); if (iterator != null) { sendRequest(); return iterator; } - if ((System.nanoTime() - watcher.current) / 1000_000 > this.timeout) { + // Read terminal state before the queue: a final batch is published with that state. + if (isServerFinished() && this.queue.isEmpty()) { + return null; + } + if ((System.nanoTime() - this.current) / 1000_000 > this.timeout) { throw new HgStoreClientException("iterator timeout"); } @@ -146,48 +178,52 @@ public Iterator consume() { } /** - * Send onComplete, stop receiving data + * Stop feedback and invoke the request stream's completion or cancellation callback. */ public void clear() { - if (!this.closed.get()) { - this.closed.set(true); - this.transferComplete.accept(true); + boolean finished; + synchronized (this.stateLock) { + if (this.closed) { + return; + } + finished = this.terminal == ResultState.FINISHED && this.parsing == 0; + this.closed = true; + this.queue.clear(); + } + if (finished) { + synchronized (this.requestLock) { + this.transferComplete.accept(true); + } + } else { + // The transport cancellation hook is thread-safe even during a blocked send. + this.transferComplete.accept(false); } - this.queue.clear(); } @Override public void onError(Throwable t) { + synchronized (this.stateLock) { + if (this.closed || this.terminal != null) { + return; + } + this.queue.offer(new ErrorMessageIterator<>(t.getMessage())); + this.terminal = ResultState.ERROR; + this.current = System.nanoTime(); + } log.error("StreamObserver got error:", t); - this.queue.offer(new ErrorMessageIterator<>(t.getMessage())); - this.watcher.setState(ResultState.ERROR); } @Override public void onCompleted() { - if (watcher.getState() != ResultState.ERROR) { - watcher.setState(ResultState.FINISHED); + synchronized (this.stateLock) { + if (!this.closed && this.terminal == null) { + this.terminal = ResultState.FINISHED; + this.current = System.nanoTime(); + } } } public void setWatcherQueryId(String queryId) { - this.watcher.setQueryId(queryId); - } - - @Data - private static class ResultStateWatcher { - - private long current = System.nanoTime(); - private volatile ResultState state = ResultState.IDLE; - - private String queryId; - - public void setState(ResultState state) { - log.debug("query Id: {}, COST_STAT: {} -> {}, cost {} ms", this.queryId, this.state, - state, - +(System.nanoTime() - current) * 1.0 / 1000000); - this.state = state; - this.current = System.nanoTime(); - } + this.queryId = queryId; } } diff --git a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/MultiStreamIterator.java b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/MultiStreamIterator.java index 0a4373c7f3..22be95b1ce 100644 --- a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/MultiStreamIterator.java +++ b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/MultiStreamIterator.java @@ -17,7 +17,12 @@ package org.apache.hugegraph.store.client.query; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.List; import java.util.NoSuchElementException; @@ -27,40 +32,74 @@ public class MultiStreamIterator implements HgKvIterator { private HgKvIterator currentIterator = null; + private final List> iterators; private final Iterator> listIterator; + private final AtomicBoolean closed = new AtomicBoolean(); public MultiStreamIterator(List> iterators) { - this.listIterator = iterators.iterator(); + this.iterators = new ArrayList<>(iterators); + this.listIterator = this.iterators.iterator(); } @Override public byte[] key() { + checkOpen(); return currentIterator.key(); } @Override public byte[] value() { + checkOpen(); return currentIterator.value(); } @Override public void close() { - //Todo is syntax correct? - if (currentIterator != null && currentIterator.hasNext()) { - currentIterator.close(); + if (!this.closed.compareAndSet(false, true)) { + return; + } + Throwable failure = null; + Set> visited = Collections.newSetFromMap(new IdentityHashMap<>()); + for (HgKvIterator iterator : this.iterators) { + if (!visited.add(iterator)) { + continue; + } + try { + iterator.close(); + } catch (RuntimeException | Error error) { + if (failure == null) { + failure = error; + } else if (failure != error) { + failure.addSuppressed(error); + } + } + } + if (failure instanceof Error) { + throw (Error) failure; + } + if (failure != null) { + throw (RuntimeException) failure; } } @Override public byte[] position() { + checkOpen(); return currentIterator.position(); } @Override public void seek(byte[] position) { + checkOpen(); this.currentIterator.seek(position); } + private void checkOpen() { + if (this.closed.get()) { + throw new IllegalStateException("Iterator is closed"); + } + } + private void getNextIterator() { if (currentIterator != null && currentIterator.hasNext()) { return; @@ -76,13 +115,16 @@ private void getNextIterator() { @Override public boolean hasNext() { + if (this.closed.get()) { + return false; + } getNextIterator(); return currentIterator != null && currentIterator.hasNext(); } @Override public E next() { - if (currentIterator == null || !currentIterator.hasNext()) { + if (this.closed.get() || currentIterator == null || !currentIterator.hasNext()) { throw new NoSuchElementException(); } return currentIterator.next(); diff --git a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/QueryExecutor.java b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/QueryExecutor.java index b4102bb373..bacc0aa383 100644 --- a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/QueryExecutor.java +++ b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/query/QueryExecutor.java @@ -45,6 +45,7 @@ import org.apache.hugegraph.store.grpc.query.Index; import org.apache.hugegraph.store.grpc.query.QueryRequest; import org.apache.hugegraph.store.grpc.query.QueryResponse; +import org.apache.hugegraph.store.grpc.query.QueryResultFormat; import org.apache.hugegraph.store.grpc.query.ScanType; import org.apache.hugegraph.store.grpc.query.ScanTypeParam; import org.apache.hugegraph.store.query.BaseElementComparator; @@ -60,6 +61,8 @@ import com.google.protobuf.ByteString; +import io.grpc.stub.ClientCallStreamObserver; +import io.grpc.stub.ClientResponseObserver; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -191,7 +194,8 @@ private StreamKvIterator getIterator(String address, QueryRequest r public boolean hasNext() { if (element == null) { while (itr.hasNext()) { - element = fromKv(request.getTable(), itr.next(), hasAgg); + element = fromKv(request.getTable(), itr.next(), hasAgg, + !QueryResultFormat.returnsTaggedElements(request)); if (element != null) { break; } @@ -235,10 +239,35 @@ public BaseElement next() { ); - var reqStream = stub.query(observer); observer.setWatcherQueryId(request.getQueryId() + '-' + address); - observer.setRequestSender(r -> reqStream.onNext(request)); - observer.setTransferComplete(r -> reqStream.onCompleted()); + stub.query(new ClientResponseObserver() { + @Override + public void beforeStart(ClientCallStreamObserver requestStream) { + observer.setRequestSender(ignored -> requestStream.onNext(request)); + observer.setTransferComplete(finished -> { + if (finished) { + requestStream.onCompleted(); + } else { + requestStream.cancel("Query iterator closed", null); + } + }); + } + + @Override + public void onNext(QueryResponse response) { + observer.onNext(response); + } + + @Override + public void onError(Throwable error) { + observer.onError(error); + } + + @Override + public void onCompleted() { + observer.onCompleted(); + } + }); observer.setTimeout(this.timeout); var itr = new StreamKvIterator<>(b -> observer.clear(), observer::consume); @@ -447,7 +476,7 @@ private static List getOrCreate(List list) { return new ArrayList<>(); } - private BaseElement fromKv(String table, Kv kv, boolean isAgg) { + private BaseElement fromKv(String table, Kv kv, boolean isAgg, boolean schemaRow) { if (isAgg) { return KvElement.of(KvSerializer.fromBytes(kv.getKey().toByteArray()), KvSerializer.fromObjectBytes(kv.getValue().toByteArray())); @@ -457,9 +486,11 @@ private BaseElement fromKv(String table, Kv kv, boolean isAgg) { BackendColumn.of(kv.getKey().toByteArray(), kv.getValue().toByteArray()); try { if (IN_EDGE_TABLE.equals(table) || OUT_EDGE_TABLE.equals(table)) { - return serializer.parseEdge(this.supplier, backendColumn, null, true); + return schemaRow ? serializer.parseSchemaEdge(this.supplier, backendColumn, null, true) : + serializer.parseEdge(this.supplier, backendColumn, null, true); } - return serializer.parseVertex(this.supplier, backendColumn, null); + return schemaRow ? serializer.parseSchemaVertex(this.supplier, backendColumn, null) : + serializer.parseVertex(this.supplier, backendColumn, null); } catch (Exception e) { log.error("parse element error,", e); return null; @@ -539,7 +570,7 @@ public boolean hasNext() { @Override public BaseElement next() { - return fromKv(query.getTable(), data.next(), false); + return fromKv(query.getTable(), data.next(), false, true); } }; }) @@ -574,7 +605,7 @@ public boolean hasNext() { @Override public BaseElement next() { - return fromKv(query.getTable(), data.next(), true); + return fromKv(query.getTable(), data.next(), true, false); } }; }) diff --git a/hugegraph-store/hg-store-core/pom.xml b/hugegraph-store/hg-store-core/pom.xml index 19c255ae5e..6d456af251 100644 --- a/hugegraph-store/hg-store-core/pom.xml +++ b/hugegraph-store/hg-store-core/pom.xml @@ -29,6 +29,10 @@ hg-store-core + + 3.8.1 + + org.apache.hugegraph @@ -55,7 +59,7 @@ com.alipay.sofa jraft-core - ${pd-store-jraft.version} + ${jraft.version} org.rocksdb @@ -88,7 +92,7 @@ com.alibaba fastjson - 1.2.83 + 1.2.84 @@ -109,11 +113,6 @@ ${revision} compile - - org.apache.hugegraph - hugegraph-core - ${revision} - org.apache.hugegraph hg-store-common @@ -129,7 +128,7 @@ org.apache.tinkerpop gremlin-core - 3.5.1 + ${tinkerpop.version} org.yaml @@ -152,7 +151,7 @@ org.apache.tinkerpop gremlin-groovy - 3.5.1 + ${tinkerpop.version} com.github.jeremyh diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/HeartbeatService.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/HeartbeatService.java index fe48248c0c..ea6f1b5870 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/HeartbeatService.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/HeartbeatService.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.hugegraph.pd.common.PDException; import org.apache.hugegraph.pd.common.PDRuntimeException; @@ -63,11 +64,15 @@ public class HeartbeatService implements Lifecycle, Partit private final List stateListeners; private final Object partitionThreadLock = new Object(); private final Object storeThreadLock = new Object(); + private final Object stateChangeLock = new Object(); private int heartbeatFailCount = 0; private int reportErrCount = 0; // Thread sleep time private volatile int timerNextDelay = 1000; private volatile boolean terminated = false; + private final AtomicBoolean fatalExitRequested = new AtomicBoolean(); + private Thread storeHeartbeatThread; + private Thread partitionHeartbeatThread; public HeartbeatService(HgStoreEngine storeEngine) { this.storeEngine = storeEngine; @@ -90,8 +95,10 @@ public boolean init(HgStoreEngineOptions opts) { storeInfo.setDeployPath(HeartbeatService.class.getResource("/").getPath()); storeInfo.setDataPath(options.getDataPath()); this.pdProvider = options.getPdProvider(); - new Thread(() -> doStoreHeartbeat(), PoolNames.HEARTBEAT).start(); - new Thread(() -> doPartitionHeartbeat(), PoolNames.P_HEARTBEAT).start(); + storeHeartbeatThread = new Thread(this::doStoreHeartbeat, PoolNames.HEARTBEAT); + partitionHeartbeatThread = new Thread(this::doPartitionHeartbeat, PoolNames.P_HEARTBEAT); + storeHeartbeatThread.start(); + partitionHeartbeatThread.start(); return true; } @@ -137,7 +144,9 @@ protected void doStoreHeartbeat() { } synchronized (storeThreadLock) { - storeThreadLock.wait(timerNextDelay); + if (!terminated) { + storeThreadLock.wait(timerNextDelay); + } } } catch (Throwable e) { if (e instanceof PDRuntimeException && @@ -145,6 +154,9 @@ protected void doStoreHeartbeat() { log.error("store heartbeat error: PD UNREACHABLE"); synchronized (storeThreadLock) { try { + if (terminated) { + break; + } if (timerNextDelay < 10000) { storeThreadLock.wait(timerNextDelay); } else { @@ -171,7 +183,9 @@ protected void doPartitionHeartbeat() { } try { synchronized (partitionThreadLock) { - partitionThreadLock.wait(options.getPartitionHBInterval() * 1000L); + if (!terminated) { + partitionThreadLock.wait(options.getPartitionHBInterval() * 1000L); + } } } catch (InterruptedException e) { log.error("doPartitionHeartbeat error: ", e); @@ -215,15 +229,18 @@ protected void registerStore() { if (exceptCode == ErrorType.STORE_ID_NOT_EXIST_VALUE) { log.error("The store ID {} does not match the PD. Check that the correct PD is " + "connected, " + "and then delete the store ID!!!", storeInfo.getId()); - System.exit(-1); + requestFatalExit(); + return; } else if (exceptCode == ErrorType.STORE_HAS_BEEN_REMOVED_VALUE) { log.error("The store ID {} has been removed, please delete all data and restart!", storeInfo.getId()); - System.exit(-1); + requestFatalExit(); + return; } else if (exceptCode == ErrorType.STORE_PROHIBIT_DUPLICATE_VALUE) { log.error("The store ID {} maybe duplicated, please check out store raft address " + "and restart later!", storeInfo.getId()); - System.exit(-1); + requestFatalExit(); + return; } } } @@ -240,11 +257,13 @@ protected void storeHeartbeat() { if (exceptCode == ErrorType.STORE_ID_NOT_EXIST_VALUE) { log.error("The store ID {} does not match the PD. Check that the correct PD is " + "connected, and then delete the store ID!!!", storeInfo.getId()); - System.exit(-1); + requestFatalExit(); + return; } else if (exceptCode == ErrorType.STORE_HAS_BEEN_REMOVED_VALUE) { log.error("The store ID {} has been removed, please delete all data and restart!", storeInfo.getId()); - System.exit(-1); + requestFatalExit(); + return; } } if (clusterStats == null || clusterStats.getState() == null) { @@ -279,11 +298,19 @@ protected void storeHeartbeat() { } protected synchronized void onStateChanged(Metapb.StoreState newState) { + if (terminated) { + return; + } Utils.runInThread(() -> { - Metapb.StoreState oldState = this.storeInfo.getState(); - this.storeInfo.setState(newState); - stateListeners.forEach((e) -> - e.stateChanged(this.storeInfo, oldState, newState)); + synchronized (stateChangeLock) { + if (terminated) { + return; + } + Metapb.StoreState oldState = this.storeInfo.getState(); + this.storeInfo.setState(newState); + stateListeners.forEach((e) -> + e.stateChanged(this.storeInfo, oldState, newState)); + } }); } @@ -371,10 +398,65 @@ public void monitorMemory() { @Override public void shutdown() { log.info("HeartbeatService shutdown"); + boolean interrupted = Thread.interrupted(); + try { + stopProducers(); + interrupted |= joinHeartbeat(storeHeartbeatThread); + interrupted |= joinHeartbeat(partitionHeartbeatThread); + // Producers may have submitted callbacks that create partition owners. + // Never hold this monitor while joining the producer threads. + synchronized (stateChangeLock) { + // Acquiring the monitor waits for any in-flight state callback to finish. + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + private void requestFatalExit() { + requestExit(-1); + } + + void requestExit(int status) { + if (!fatalExitRequested.compareAndSet(false, true)) { + return; + } + stopProducers(); + // Hooks join producers and wait for state callbacks to release their lock. + // Let the requesting producer or callback return before shutdown waits. + new Thread(() -> System.exit(status), "store-fatal-exit").start(); + } + + private void stopProducers() { terminated = true; + synchronized (storeThreadLock) { + storeThreadLock.notifyAll(); + } synchronized (partitionThreadLock) { - partitionThreadLock.notify(); + partitionThreadLock.notifyAll(); + } + } + + private static boolean joinHeartbeat(Thread thread) { + if (thread == null) { + return false; + } + if (thread == Thread.currentThread()) { + throw new IllegalStateException("Cannot close the store from its heartbeat thread"); + } + boolean interrupted = false; + boolean joined = false; + while (!joined) { + try { + thread.join(); + joined = true; + } catch (InterruptedException e) { + interrupted = true; + } } + return interrupted; } @Override diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/HgStoreEngine.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/HgStoreEngine.java index eae08dfad7..4299e79cee 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/HgStoreEngine.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/HgStoreEngine.java @@ -207,32 +207,43 @@ public void shutdown() { if (rpcServer == null) { return; } - closing.set(true); - heartbeatService.shutdown(); - metricService.shutdown(); -// Use sequential processing for safer shutdown - partitionEngines.values().forEach(pe -> { - try { - Node raftNode = pe.getRaftNode(); - if (raftNode.isLeader(false)) { - Status status = raftNode.transferLeadershipTo(PeerId.ANY_PEER); - if (!status.isOk()) { - log.warn("transfer leader error: {}", status); + boolean interrupted = Thread.interrupted(); + try { + closing.set(true); + heartbeatService.shutdown(); + interrupted |= Thread.interrupted(); + metricService.shutdown(); + // Use sequential processing so each partition drains before its database closes. + for (PartitionEngine pe : partitionEngines.values()) { + try { + Node raftNode = pe.getRaftNode(); + if (raftNode.isLeader(false)) { + Status status = raftNode.transferLeadershipTo(PeerId.ANY_PEER); + if (!status.isOk()) { + log.warn("transfer leader error: {}", status); + } } + } catch (Exception e) { + log.error("transfer leader error: ", e); } - } catch (Exception e) { - log.error("transfer leader error: ", e); + pe.shutdown(); + interrupted |= Thread.interrupted(); + businessHandler.closeDB(pe.getGroupId()); } - pe.shutdown(); - businessHandler.closeDB(pe.getGroupId()); - }); - partitionEngines.clear(); - rpcServer.shutdown(); - // HgStoreEngine.init function check rpcServer whether is null, skipped if the instance - // exists even shut down. - rpcServer = null; - // close all db session - RocksDBFactory.getInstance().releaseAllGraphDB(); + partitionEngines.clear(); + rpcServer.shutdown(); + // HgStoreEngine.init skips RPC initialization while this field is non-null. + rpcServer = null; + try { + RocksDBFactory.getInstance().releaseAllGraphDB(); + } finally { + BusinessHandlerImpl.closeSchemaResources(); + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } } public void snapshotForTest() { @@ -250,7 +261,13 @@ public void stateChanged(Store store, Metapb.StoreState oldState, Metapb.StoreSt if (newState == Metapb.StoreState.Up) { // Status changes to online, record store information partitionManager.setStore(store); - partitionManager.loadPartition(); + try { + partitionManager.loadPartition(); + } catch (PartitionManager.InvalidShardException e) { + // Never wait for shutdown hooks while owning the state callback lock. + heartbeatService.requestExit(0); + return; + } restoreLocalPartitionEngine(); } } @@ -260,6 +277,12 @@ public void stateChanged(Store store, Metapb.StoreState oldState, Metapb.StoreSt * 1. Need to check the partition saved this time, delete the invalid partitions. */ public void restoreLocalPartitionEngine() { + // TODO: surface the outcome of this restore (a per-group ready signal, or a failed state + // reported to PD) instead of logging only; a Store is marked Up before this runs and a + // failed restore leaves it Up with missing shard groups. Paired with the TODO in + // StoreNodeService, which also says why this signal alone does not retire the manual + // Store rollout barrier in the Helm chart (helm/hugegraph) cluster preset. + // https://github.com/apache/hugegraph/issues/3229 try { if (!options.isFakePD()) { // FakePD mode does not require synchronization partitionManager.syncPartitionsFromPD(partition -> { diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/PartitionEngine.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/PartitionEngine.java index 78ac781375..85e43fea02 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/PartitionEngine.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/PartitionEngine.java @@ -95,7 +95,6 @@ import com.alipay.sofa.jraft.util.Endpoint; import com.alipay.sofa.jraft.util.ThreadId; import com.alipay.sofa.jraft.util.Utils; -import com.alipay.sofa.jraft.util.internal.ThrowUtil; import com.google.protobuf.CodedInputStream; import lombok.Getter; @@ -565,16 +564,28 @@ public void shutdown() { if (!this.started) { return; } - if (this.raftGroupService != null) { - this.raftGroupService.shutdown(); - try { - this.raftGroupService.join(); - } catch (final InterruptedException e) { - ThrowUtil.throwException(e); + boolean interrupted = false; + try { + if (this.raftGroupService != null) { + this.raftGroupService.shutdown(); + boolean terminated = false; + while (!terminated) { + try { + this.raftGroupService.join(); + terminated = true; + } catch (InterruptedException e) { + // Storage remains in use until Raft and its callbacks have stopped. + interrupted = true; + } + } + } + this.started = false; + log.info("PartitionEngine shutdown successfully: {}.", this); + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); } } - this.started = false; - log.info("PartitionEngine shutdown successfully: {}.", this); } /** diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/AbstractSelectIterator.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/AbstractSelectIterator.java index 40f909b0d2..47ea660ca7 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/AbstractSelectIterator.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/AbstractSelectIterator.java @@ -17,6 +17,9 @@ package org.apache.hugegraph.store.business; +import java.util.Objects; + +import org.apache.hugegraph.HugeGraphSupplier; import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.rocksdb.access.ScanIterator; import org.apache.hugegraph.serializer.BinaryElementSerializer; @@ -29,8 +32,10 @@ public abstract class AbstractSelectIterator implements ScanIterator { protected ScanIterator iterator; protected BinaryElementSerializer serializer; + protected final HugeGraphSupplier graph; - public AbstractSelectIterator() { + public AbstractSelectIterator(HugeGraphSupplier graph) { + this.graph = Objects.requireNonNull(graph, "graph"); this.serializer = new BinaryElementSerializer(); } @@ -39,9 +44,9 @@ public BaseElement parseEntry(BackendColumn column, boolean isVertex) { throw new IllegalArgumentException("BackendColumn cannot be null"); } if (isVertex) { - return serializer.parseVertex(null, column, null); + return serializer.parseSchemaVertex(this.graph, column, null); } else { - return serializer.parseEdge(null, column, null, true); + return serializer.parseSchemaEdge(this.graph, column, null, true); } } } diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandlerImpl.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandlerImpl.java index 38bad3b9e8..bc446afb31 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandlerImpl.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/BusinessHandlerImpl.java @@ -61,7 +61,6 @@ import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.hugegraph.HugeGraphSupplier; -import org.apache.hugegraph.SchemaGraph; import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.config.OptionSpace; @@ -112,6 +111,8 @@ import org.apache.hugegraph.store.query.QueryTypeParam; import org.apache.hugegraph.store.raft.RaftClosure; import org.apache.hugegraph.store.raft.RaftOperation; +import org.apache.hugegraph.store.schema.SchemaDriver; +import org.apache.hugegraph.store.schema.SchemaGraph; import org.apache.hugegraph.store.term.Bits; import org.apache.hugegraph.store.term.HgPair; import org.apache.hugegraph.store.util.DefaultThreadFactory; @@ -294,20 +295,32 @@ public static HugeGraphSupplier getGraphSupplier(String graph) { return mockGraphSupplier; } - if (GRAPH_SUPPLIER_CACHE.get(graph) == null) { + if (graph == null) { + throw new HgStoreException("Graph must include graph space and graph name"); + } + HugeGraphSupplier supplier = GRAPH_SUPPLIER_CACHE.get(graph); + if (supplier == null) { synchronized (BusinessHandlerImpl.class) { - if (GRAPH_SUPPLIER_CACHE.get(graph) == null) { + supplier = GRAPH_SUPPLIER_CACHE.get(graph); + if (supplier == null) { + if (HgStoreEngine.getInstance().isClosing().get()) { + throw new HgStoreException("Store is closing"); + } + String[] parts = graph.split("/", -1); + if (parts.length < 2 || parts[0].isEmpty() || parts[1].isEmpty()) { + throw new HgStoreException("Graph must include graph space and graph name: " + graph); + } var config = PDConfig.of(HgStoreEngine.getInstance().getOption().getPdAddress()); config.setAuthority(DefaultPdProvider.name, DefaultPdProvider.authority); - String[] parts = graph.split("/"); - assert (parts.length > 1); - GRAPH_SUPPLIER_CACHE.put(graph, new SchemaGraph(parts[0], parts[1], config)); + supplier = new SchemaGraph(parts[0], parts[1], config, + HgStoreEngine.getInstance().getOption().getPdCluster()); + GRAPH_SUPPLIER_CACHE.put(graph, supplier); } } } - return GRAPH_SUPPLIER_CACHE.get(graph); + return supplier; } @Override @@ -546,7 +559,17 @@ public ScanIterator scan(String graph, int code, String table, byte[] start, byt @Override public GraphStoreIterator scan(ScanPartitionRequest spr) throws HgStoreException { - return new GraphStoreIterator(scanOriginal(spr), spr); + ScanIterator original = scanOriginal(spr); + try { + return new GraphStoreIterator(original, spr); + } catch (RuntimeException failure) { + try { + original.close(); + } catch (RuntimeException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } } private ToLongFunction getBaseElementHashFunction() { @@ -739,7 +762,7 @@ public ScanIterator indexIntersection(String graph, String table, List { - if (filterTTL && isIndexExpire(column, now)) { + if (filterTTL && isIndexExpire(graph, column, now)) { return null; } - // todo: Use parseIndex(BackendColumn indexCol) later - var index = serializer.parseIndex(getGraphSupplier(graph), - BackendColumn.of(column.name, column.value), null); + var index = serializer.parseSchemaIndex(getGraphSupplier(graph), + BackendColumn.of(column.name, column.value)); if (param.getIdPrefix() != null && !Bytes.prefixWith(index.elementId().asBytes(), param.getIdPrefix())) { @@ -916,7 +938,7 @@ private ScanIterator scanIndexToBaseElement(String graph, QueryTypeParam param, param.getBoundary()) : scanPrefix(graph, param.getCode(), INDEX_TABLE, param.getStart(), param.getBoundary()), column -> { - if (filterTTL && isIndexExpire(column, now)) { + if (filterTTL && isIndexExpire(graph, column, now)) { return null; } @@ -934,8 +956,8 @@ private ScanIterator scanIndexToBaseElement(String graph, QueryTypeParam param, }, "trans-index-to-base-element"); } - private boolean isIndexExpire(RocksDBSession.BackendColumn column, long now) { - var e = directBinarySerializer.parseIndex(column.name, column.value); + private boolean isIndexExpire(String graph, RocksDBSession.BackendColumn column, long now) { + var e = directBinarySerializer.parseSchemaIndex(getGraphSupplier(graph), column.name, column.value); return e.expiredTime() > 0 && e.expiredTime() < now; } @@ -1131,9 +1153,13 @@ public void closeDB(int partId) { @Override public void closeAll() { log.warn("close all db!!! "); - factory.getGraphNames().forEach(dbName -> { - factory.releaseGraphDB(dbName); - }); + try { + factory.getGraphNames().forEach(dbName -> { + factory.releaseGraphDB(dbName); + }); + } finally { + closeSchemaResources(); + } } @Override @@ -1983,7 +2009,15 @@ private void release() { } } - public static void clearCache() { + public static synchronized void clearCache() { GRAPH_SUPPLIER_CACHE.clear(); } + + public static synchronized void closeSchemaResources() { + try { + SchemaDriver.destroy(); + } finally { + GRAPH_SUPPLIER_CACHE.clear(); + } + } } diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/DataManagerImpl.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/DataManagerImpl.java index 3a4b5ac26c..a27e6892ff 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/DataManagerImpl.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/DataManagerImpl.java @@ -51,6 +51,7 @@ import org.apache.hugegraph.store.raft.RaftOperation; import org.apache.hugegraph.store.term.Bits; import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.structure.BaseEdge; import org.apache.hugegraph.structure.BaseElement; import org.apache.hugegraph.structure.BaseVertex; @@ -292,17 +293,21 @@ public Status doBuildIndex(Metapb.BuildIndexParam param, Metapb.Partition source try { if (param.getIsVertexLabel()) { - element = serializer.parseVertex(graphSupplier, column, null); + element = serializer.parseSchemaVertex(graphSupplier, column, null); } else { - element = serializer.parseEdge(graphSupplier, column, null, true); + element = serializer.parseSchemaEdge(graphSupplier, column, null, true); } } catch (Exception e) { log.error("parse element failed, graph:{}, key:{}", graphName, e); continue; } - // filter by label id - if (!element.schemaLabel().id().equals(labelId)) { + boolean matchesLabel = element.schemaLabel().id().equals(labelId); + if (!matchesLabel && element instanceof BaseEdge) { + EdgeLabel edgeLabel = (EdgeLabel) element.schemaLabel(); + matchesLabel = edgeLabel.hasFather() && labelId.equals(edgeLabel.fatherId()); + } + if (!matchesLabel) { continue; } @@ -327,7 +332,7 @@ public Status doBuildIndex(Metapb.BuildIndexParam param, Metapb.Partition source } for (var index : array) { - var col = serializer.writeIndex(index); + var col = serializer.writeSchemaIndex(index); int code = PartitionUtils.calcHashcode(KeyUtil.getOwnerId(index.elementId())); // same partition id with element batch.add(partitionId, BatchPutRequest.KV.of(INDEX_TABLE, code, col.name, diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/FilterIterator.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/FilterIterator.java index eb53a2d254..531df3e953 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/FilterIterator.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/FilterIterator.java @@ -18,6 +18,7 @@ package org.apache.hugegraph.store.business; import org.apache.commons.lang3.ArrayUtils; +import org.apache.hugegraph.HugeGraphSupplier; import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.rocksdb.access.RocksDBSession; @@ -34,20 +35,32 @@ public class FilterIterator extends private final ConditionQuery query; T current = null; - public FilterIterator(ScanIterator iterator, ConditionQuery query) { - super(); + public FilterIterator(ScanIterator iterator, ConditionQuery query, HugeGraphSupplier graph) { + super(graph); this.iterator = iterator; this.query = query; // log.info("operator sinking is used to filter data:{}", // query.toString()); } - public static ScanIterator of(ScanIterator it, byte[] conditionQuery) { + public static ScanIterator of(ScanIterator it, byte[] conditionQuery, String graph) { if (ArrayUtils.isEmpty(conditionQuery)) { return it; } - ConditionQuery query = ConditionQuery.fromBytes(conditionQuery); - return new FilterIterator(it, query); + try { + ConditionQuery query = ConditionQuery.fromBytes(conditionQuery); + if (!query.resultType().isVertex() && !query.resultType().isEdge()) { + return it; + } + return new FilterIterator(it, query, BusinessHandlerImpl.getGraphSupplier(graph)); + } catch (RuntimeException failure) { + try { + it.close(); + } catch (RuntimeException closeFailure) { + failure.addSuppressed(closeFailure); + } + throw failure; + } } @Override @@ -60,11 +73,11 @@ public boolean hasNext() { current = iterator.next(); BaseElement element; if (this.query.resultType().isVertex()) { - element = serializer.parseVertex(null, + element = serializer.parseSchemaVertex(this.graph, BackendColumn.of(current.name, current.value), null); } else { - element = serializer.parseEdge(null, + element = serializer.parseSchemaEdge(this.graph, BackendColumn.of(current.name, current.value), null, true); } diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/GraphStoreIterator.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/GraphStoreIterator.java index 51b9c8d15c..254915032e 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/GraphStoreIterator.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/GraphStoreIterator.java @@ -83,7 +83,7 @@ public class GraphStoreIterator extends AbstractSelectIterator public GraphStoreIterator(ScanIterator iterator, ScanPartitionRequest scanRequest) { - super(); + super(BusinessHandlerImpl.getGraphSupplier(scanRequest.getScanRequest().getGraphName())); this.iter = iterator; this.scanRequest = scanRequest; this.request = this.scanRequest.getScanRequest(); diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/InnerKeyFilter.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/InnerKeyFilter.java index 368032f2ce..75572ec609 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/InnerKeyFilter.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/InnerKeyFilter.java @@ -31,13 +31,13 @@ public class InnerKeyFilter implements ScanIterator { final boolean codeFilter; ScanIterator iterator; T current = null; + private boolean initialized; public InnerKeyFilter(ScanIterator iterator) { this.iterator = iterator; this.codeFrom = Integer.MIN_VALUE; this.codeTo = Integer.MAX_VALUE; this.codeFilter = false; - moveNext(); } public InnerKeyFilter(ScanIterator iterator, boolean codeFilter) { @@ -45,7 +45,6 @@ public InnerKeyFilter(ScanIterator iterator, boolean codeFilter) { this.codeFrom = Integer.MIN_VALUE; this.codeTo = Integer.MAX_VALUE; this.codeFilter = codeFilter; - moveNext(); } public InnerKeyFilter(ScanIterator iterator, int codeFrom, int codeTo) { @@ -53,7 +52,14 @@ public InnerKeyFilter(ScanIterator iterator, int codeFrom, int codeTo) { this.codeFrom = codeFrom; this.codeTo = codeTo; this.codeFilter = true; - moveNext(); + } + + private void init() { + if (!this.initialized) { + // Publish the wrapper to its owner before prefetch can close or fail on the raw iterator. + this.initialized = true; + moveNext(); + } } private void moveNext() { @@ -76,16 +82,19 @@ private void moveNext() { @Override public boolean hasNext() { + init(); return current != null; } @Override public boolean isValid() { + init(); return iterator.isValid(); } @Override public T next() { + init(); T column = current; if (!codeFilter) // Remove the image ID and hash suffix @@ -103,11 +112,13 @@ public T next() { @Override public void close() { + this.initialized = true; iterator.close(); } @Override public long count() { + init(); return iterator.count(); } } diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/MultiPartitionIterator.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/MultiPartitionIterator.java index 72cc472b21..54fd80be72 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/MultiPartitionIterator.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/MultiPartitionIterator.java @@ -18,13 +18,13 @@ package org.apache.hugegraph.store.business; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; import java.util.Queue; import java.util.function.BiFunction; -import java.util.stream.Collectors; import org.apache.hugegraph.rocksdb.access.ScanIterator; @@ -45,6 +45,8 @@ public class MultiPartitionIterator implements ScanIterator { private Integer curPartitionId; private Integer positionPartitionId; private byte[] positionKey; + private RuntimeException cleanupFailure; + private boolean closed; private MultiPartitionIterator(List partitionIds, BiFunction supplier) { @@ -75,35 +77,31 @@ public static int toInt(byte[] bytes) { } private ScanIterator getIterator() { - if (this.partitions.isEmpty()) { - return null; - } - ScanIterator buf = null; - while (!partitions.isEmpty()) { - this.curPartitionId = partitions.poll(); + while (!this.partitions.isEmpty()) { + this.curPartitionId = this.partitions.poll(); if (!this.inPosition(this.curPartitionId)) { continue; } - buf = supplier.apply(this.curPartitionId, getPositionKey(this.curPartitionId)); - if (buf == null) { + ScanIterator child = this.supplier.apply(this.curPartitionId, + getPositionKey(this.curPartitionId)); + if (child == null) { continue; } - if (buf.hasNext()) { - break; + try { + if (child.hasNext()) { + return child; + } + } catch (RuntimeException | Error failure) { + closeAfterFailure(child, failure); + throw failure; } + closeCreatedIterator(child); } - if (buf == null) { - return null; - } - if (!buf.hasNext()) { - buf.close(); - buf = null; - } - return buf; + return null; } private void init() { - if (this.iterator == null) { + if (!this.closed && this.iterator == null) { this.iterator = this.getIterator(); } } @@ -128,8 +126,7 @@ public T next() { } T t = this.iterator.next(); if (!this.iterator.hasNext()) { - this.iterator.close(); - this.iterator = null; + closeCurrentIterator(); } return t; } @@ -137,11 +134,16 @@ public T next() { @Override public long count() { long count = 0; - this.iterator = this.getIterator(); - while (this.iterator != null) { - count += this.iterator.count(); - // this.iterator.close(); - this.iterator = this.getIterator(); + while (this.hasNext()) { + try { + count += this.iterator.count(); + } catch (RuntimeException | Error failure) { + ScanIterator child = this.iterator; + this.iterator = null; + closeAfterFailure(child, failure); + throw failure; + } + closeCurrentIterator(); } return count; } @@ -172,8 +174,33 @@ public void seek(byte[] position) { @Override public void close() { - if (this.iterator != null) { - this.iterator.close(); + this.closed = true; + try { + closeCurrentIterator(); + } catch (RuntimeException | Error failure) { + // closeCreatedIterator retains the failure even after ownership is released here. + throw this.cleanupFailure; + } + if (this.cleanupFailure != null) { + throw this.cleanupFailure; + } + } + + private void closeCurrentIterator() { + ScanIterator child = this.iterator; + this.iterator = null; + if (child != null) { + closeCreatedIterator(child); + } + } + + private void closeAfterFailure(ScanIterator child, Throwable failure) { + try { + closeCreatedIterator(child); + } catch (RuntimeException | Error closeFailure) { + if (closeFailure != failure) { + failure.addSuppressed(closeFailure); + } } } @@ -205,10 +232,50 @@ private byte[] getPositionKey(int partitionId) { * @return iteration list */ public List getIterators() { - return this.partitions.stream() - .map(id -> supplier.apply(id, getPositionKey(id))) - .filter(ScanIterator::hasNext) - .collect(Collectors.toList()); + List opened = new ArrayList<>(); + if (this.closed) { + return opened; + } + try { + for (int id : this.partitions) { + ScanIterator child = this.supplier.apply(id, getPositionKey(id)); + if (child == null) { + continue; + } + opened.add(child); + if (!child.hasNext()) { + opened.remove(opened.size() - 1); + closeCreatedIterator(child); + } + } + // Ownership transfers to the caller only when all partitions were opened. + return opened; + } catch (RuntimeException | Error failure) { + for (ScanIterator child : opened) { + try { + closeCreatedIterator(child); + } catch (RuntimeException | Error closeFailure) { + if (closeFailure != failure) { + failure.addSuppressed(closeFailure); + } + } + } + throw failure; + } + } + + private void closeCreatedIterator(ScanIterator child) { + try { + child.close(); + } catch (RuntimeException | Error failure) { + if (this.cleanupFailure == null) { + this.cleanupFailure = new IllegalStateException( + "partition iterator cleanup failed", failure); + } else { + this.cleanupFailure.addSuppressed(failure); + } + throw failure; + } } } diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/OrderedMultiPartitionIterator.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/OrderedMultiPartitionIterator.java index f7f013b89f..cba162a58b 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/OrderedMultiPartitionIterator.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/OrderedMultiPartitionIterator.java @@ -41,6 +41,7 @@ public final class OrderedMultiPartitionIterator implements ScanIterator { private boolean initialized; private boolean closed; + private Throwable cleanupFailure; private Integer currentPartitionId; private OrderedMultiPartitionIterator(List partitionIds, @@ -142,22 +143,29 @@ public void seek(byte[] position) { @Override public void close() { if (this.closed) { + rethrowCleanupFailure(); return; } this.closed = true; - Throwable failure = null; + Throwable failure = this.cleanupFailure; for (SourceEntry source : this.sources) { try { this.closeSource(source); } catch (RuntimeException | Error e) { if (failure == null) { failure = e; - } else { + } else if (failure != e) { failure.addSuppressed(e); } } } this.queue.clear(); + this.cleanupFailure = failure; + rethrowCleanupFailure(); + } + + private void rethrowCleanupFailure() { + Throwable failure = this.cleanupFailure; if (failure instanceof RuntimeException) { throw (RuntimeException) failure; } @@ -197,14 +205,23 @@ private void closeSource(SourceEntry source) { return; } source.closed = true; - source.iterator.close(); + try { + source.iterator.close(); + } catch (RuntimeException | Error failure) { + if (this.cleanupFailure == null) { + this.cleanupFailure = failure; + } + throw failure; + } } private void closeAfterFailure(Throwable failure) { try { this.close(); } catch (RuntimeException | Error closeFailure) { - failure.addSuppressed(closeFailure); + if (failure != closeFailure) { + failure.addSuppressed(closeFailure); + } } } diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/SelectIterator.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/SelectIterator.java index 2b51e98778..542b91b5e2 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/SelectIterator.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/business/SelectIterator.java @@ -17,56 +17,64 @@ package org.apache.hugegraph.store.business; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.Objects; import java.util.List; import java.util.Set; +import org.apache.hugegraph.HugeGraphSupplier; import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.rocksdb.access.RocksDBSession.BackendColumn; import org.apache.hugegraph.rocksdb.access.ScanIterator; import org.apache.hugegraph.serializer.BytesBuffer; -import org.apache.hugegraph.type.define.DataType; -import org.apache.hugegraph.type.define.SerialEnum; public class SelectIterator implements ScanIterator { - ScanIterator iter; - Set properties; + private final ScanIterator iter; + private final Set properties; + private final HugeGraphSupplier graph; + private final boolean isVertex; - public SelectIterator(ScanIterator iterator, List properties) { + public SelectIterator(ScanIterator iterator, List properties, + HugeGraphSupplier graph, boolean isVertex) { this.iter = iterator; - this.properties = new HashSet<>(properties); + this.properties = properties == null ? Collections.emptySet() : new HashSet<>(properties); + this.graph = this.properties.isEmpty() ? graph : Objects.requireNonNull(graph, "graph"); + this.isVertex = isVertex; } public BackendColumn select(BackendColumn column) { - int size; - if (properties == null || (size = properties.size()) == 0) { + if (this.properties.isEmpty() || column.value == null || column.value.length == 0) { return column; } - byte[] name = column.name; - byte[] value = column.value; - BytesBuffer buffer = BytesBuffer.wrap(value); - Id labelId = buffer.readId(); // label - int bpSize = buffer.readVInt(); // property - if (size == bpSize) { - return column; - } - BytesBuffer allocate = BytesBuffer.allocate(8 + 16 * size); - allocate.writeId(labelId); - allocate.writeVInt(size); - for (int i = 0; i < bpSize; i++) { + BytesBuffer buffer = BytesBuffer.wrap(column.value); + Id labelId = this.isVertex ? buffer.readId() : null; + int count = buffer.readVInt(); + List selected = new ArrayList<>(Math.min(count, this.properties.size())); + for (int i = 0; i < count; i++) { + int start = buffer.position(); int propertyId = buffer.readVInt(); - byte cat = buffer.read(); // cardinality and type - byte code = BytesBuffer.getType(cat); - DataType dataType = SerialEnum.fromCode(DataType.class, code); - Object bpValue = buffer.readProperty(dataType); - if (properties.contains(propertyId)) { - allocate.writeVInt(propertyId); - allocate.write(cat); - allocate.writeProperty(dataType, bpValue); + buffer.skipSchemaProperty(this.graph.propertyKey(IdGenerator.of(propertyId))); + if (this.properties.contains(propertyId)) { + // Preserve the stored bytes, including collection ordering and scalar representation. + selected.add(Arrays.copyOfRange(column.value, start, buffer.position())); } } - return BackendColumn.of(name, allocate.bytes()); + BytesBuffer output = BytesBuffer.allocate(column.value.length); + if (this.isVertex) { + output.writeId(labelId); + } + output.writeVInt(selected.size()); + for (byte[] property : selected) { + output.write(property); + } + // TTL belongs to the row, independently of which properties were selected. + output.write(buffer.remainingBytes()); + return BackendColumn.of(column.name, output.bytes()); } @Override diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/GraphIdManager.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/GraphIdManager.java index 3c95919591..76eada307b 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/GraphIdManager.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/GraphIdManager.java @@ -20,18 +20,25 @@ import static org.apache.hugegraph.store.constant.HugeServerTables.VERTEX_TABLE; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Arrays; -import java.util.List; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import org.apache.hugegraph.rocksdb.access.RocksDBSession; +import org.apache.hugegraph.rocksdb.access.SessionOperator; import org.apache.hugegraph.store.meta.base.DBSessionBuilder; import org.apache.hugegraph.store.meta.base.PartitionMetaStore; import org.apache.hugegraph.store.term.Bits; +import org.apache.hugegraph.store.util.Asserts; import org.apache.hugegraph.store.util.HgStoreException; import com.google.protobuf.Int64Value; +import com.google.protobuf.InvalidProtocolBufferException; import lombok.extern.slf4j.Slf4j; @@ -43,7 +50,7 @@ public class GraphIdManager extends PartitionMetaStore { protected static final String GRAPH_ID_PREFIX = "@GRAPH_ID@"; - // FIXME: we need to ensure the right num & proper logic for it (IMPORTANT) + // Missing-graph sentinel; allocatable graph IDs are [0, maxGraphID) protected static int maxGraphID = 65535 - 1; static Object graphIdLock = new Object(); static Object cidLock = new Object(); @@ -68,13 +75,18 @@ public long getGraphId(String graphName) { byte[] key = MetadataKeyHelper.getGraphIDKey(graphName); Int64Value id = get(Int64Value.parser(), key); if (id == null) { - id = Int64Value.of(maxGraphID); + l = (long) maxGraphID; + } else { + l = checkGraphId(graphName, id.getValue(), + this.partitionId); } - l = id.getValue(); graphIdCache.put(graphName, l); } } } + if (l != maxGraphID) { + checkGraphId(graphName, l, this.partitionId); + } return l; } @@ -87,26 +99,152 @@ public long getGraphIdOrCreate(String graphName) { byte[] key = MetadataKeyHelper.getGraphIDKey(graphName); Int64Value id = get(Int64Value.parser(), key); if (id == null) { - id = Int64Value.of(getCId(GRAPH_ID_PREFIX, maxGraphID - 1)); + id = Int64Value.of(getCId(GRAPH_ID_PREFIX, maxGraphID)); if (id.getValue() == -1) { throw new HgStoreException(HgStoreException.EC_FAIL, "The number of graphs exceeds the maximum " + - "65535"); + maxGraphID); } log.info("partition: {}, Graph ID {} is allocated for graph {}, stack: {}", this.partitionId, id.getValue(), graphName, Arrays.toString(Thread.currentThread().getStackTrace())); put(key, id); flush(); + } else { + checkGraphId(graphName, id.getValue(), + this.partitionId); } l = id.getValue(); graphIdCache.put(graphName, l); } } } + checkGraphId(graphName, l, this.partitionId); return l; } + public static long checkGraphId(String graphName, long graphId, + int partitionId) { + if (graphId < 0L || graphId >= maxGraphID) { + throw new HgStoreException( + HgStoreException.EC_FAIL, + "Invalid graph ID %s for graph '%s' in partition %s, " + + "expected a value in [0, %s)", + graphId, graphName, partitionId, maxGraphID); + } + return graphId; + } + + public static void checkGraphIds(Map graphIds, + int partitionId) { + Map graphNames = new HashMap<>(); + graphIds.forEach((graphName, graphId) -> { + if (graphId == null) { + throw new HgStoreException(HgStoreException.EC_FAIL, + "Invalid null graph ID for graph '%s' " + + "in partition %s", + graphName, partitionId); + } + checkGraphId(graphName, graphId, partitionId); + String previous = graphNames.putIfAbsent(graphId, graphName); + if (previous != null && !previous.equals(graphName)) { + throw new HgStoreException( + HgStoreException.EC_FAIL, + "Graph ID %s is assigned to multiple graphs '%s' and '%s' " + + "in partition %s", + graphId, previous, graphName, partitionId); + } + }); + } + + public void updateGraphIds(Map graphIds) { + Map updates = new HashMap<>(graphIds); + checkGraphIds(updates, this.partitionId); + synchronized (graphIdLock) { + Map finalGraphIds = this.graphIds(); + Set previousGraphIds = updates.keySet().stream() + .map(finalGraphIds::get) + .filter(graphId -> graphId != null && + graphId >= 0L && + graphId < maxGraphID) + .collect(Collectors.toSet()); + finalGraphIds.putAll(updates); + this.checkUniqueGraphIds(finalGraphIds); + this.writeGraphIds(updates, previousGraphIds); + this.graphIdCache.putAll(updates); + } + } + + private Map graphIds() { + byte[] prefix = MetadataKeyHelper.getGraphIDKey(""); + Map graphIds = new HashMap<>(); + for (RocksDBSession.BackendColumn column : scan(prefix)) { + String graphName = new String(column.name, prefix.length, + column.name.length - prefix.length, + StandardCharsets.UTF_8); + try { + long graphId = Int64Value.parseFrom(column.value).getValue(); + graphIds.put(graphName, graphId); + } catch (InvalidProtocolBufferException e) { + throw new HgStoreException(HgStoreException.EC_FAIL, e); + } + } + return graphIds; + } + + private void checkUniqueGraphIds(Map graphIds) { + Map graphNames = new HashMap<>(); + graphIds.forEach((graphName, graphId) -> { + // Ignore legacy invalid IDs here so this repair path can replace them. + if (graphId < 0L || graphId >= maxGraphID) { + return; + } + String previous = graphNames.putIfAbsent(graphId, graphName); + if (previous != null && !previous.equals(graphName)) { + throw new HgStoreException( + HgStoreException.EC_FAIL, + "Graph ID %s is assigned to multiple graphs '%s' and '%s' " + + "in partition %s", + graphId, previous, graphName, this.partitionId); + } + }); + } + + private void writeGraphIds(Map graphIds, + Set previousGraphIds) { + try (RocksDBSession dbSession = getRocksDBSession()) { + SessionOperator operator = dbSession.sessionOp(); + try { + operator.prepare(); + // A repaired mapping does not prove that the old graph's data + // prefix is empty. Keep its ID reserved even if the slot was + // absent in legacy metadata. + for (Long graphId : previousGraphIds) { + byte[] slotKey = genCIDSlotKey(GRAPH_ID_PREFIX, graphId); + operator.put(getCFName(), slotKey, + Int64Value.of(graphId).toByteArray()); + } + // Publish mappings only after all IDs are reserved in the same batch. + for (Map.Entry entry : graphIds.entrySet()) { + Int64Value value = Int64Value.of(entry.getValue()); + byte[] slotKey = genCIDSlotKey(GRAPH_ID_PREFIX, + entry.getValue()); + operator.put(getCFName(), slotKey, value.toByteArray()); + } + for (Map.Entry entry : graphIds.entrySet()) { + Int64Value value = Int64Value.of(entry.getValue()); + byte[] graphIdKey = + MetadataKeyHelper.getGraphIDKey(entry.getKey()); + operator.put(getCFName(), graphIdKey, value.toByteArray()); + } + operator.commit(); + } catch (RuntimeException e) { + operator.rollback(); + throw e; + } + } + } + /** * Release a graph id */ @@ -132,10 +270,11 @@ private boolean checkCount(long l) { Bits.putShort(start, 0, (short) l); try (var session = sessionBuilder.getSession(partitionId)) { if (!session.tableIsExist(VERTEX_TABLE)) { + // Scanning a missing table creates it and requires a write lock return true; } - try (var itr = session.sessionOp().scan(VERTEX_TABLE, start)) { - return itr == null || !itr.hasNext(); + try (var iterator = session.sessionOp().scan(VERTEX_TABLE, start)) { + return iterator == null || !iterator.hasNext(); } } } @@ -149,41 +288,58 @@ private boolean checkCount(long l) { * @return id */ protected long getCId(String key, long max) { + Asserts.isTrue(max > 0L, "The maximum cyclic ID must be positive"); byte[] cidNextKey = MetadataKeyHelper.getCidKey(key); synchronized (cidLock) { Int64Value value = get(Int64Value.parser(), cidNextKey); - long current = value != null ? value.getValue() : 0L; - long last = current == 0 ? max - 1 : current - 1; - // Find an unused cid - List ids = - scan(Int64Value.parser(), genCIDSlotKey(key, current), genCIDSlotKey(key, max)); - var idSet = ids.stream().map(Int64Value::getValue).collect(Collectors.toSet()); - - while (idSet.contains(current) || !checkCount(current)) { - current++; + long start = Math.floorMod(value != null ? value.getValue() : 0L, max); + Set assignedGraphIds = this.assignedGraphIds(key); + long current = this.findAvailableCId(key, start, max, + assignedGraphIds); + if (current == -1L && start > 0L) { + current = this.findAvailableCId(key, 0L, start, + assignedGraphIds); } - - if (current == max - 1) { - current = 0; - ids = scan(Int64Value.parser(), genCIDSlotKey(key, current), - genCIDSlotKey(key, last)); - idSet = ids.stream().map(Int64Value::getValue).collect(Collectors.toSet()); - while (idSet.contains(current) || !checkCount(current)) { - current++; - } + if (current == -1L) { + return -1L; } - if (current == last) { - return -1; - } // Save current id, mark as used put(genCIDSlotKey(key, current), Int64Value.of(current)); - // Save the id for the next traversal - put(cidNextKey, Int64Value.of(current + 1)); + // Keep the next traversal position inside [0, max) + long next = current + 1L; + put(cidNextKey, Int64Value.of(next == max ? 0L : next)); return current; } } + private long findAvailableCId(String key, long start, long end, + Set assignedGraphIds) { + Set idSet = scan(Int64Value.parser(), genCIDSlotKey(key, start), + genCIDSlotKey(key, end)) + .stream() + .map(Int64Value::getValue) + .collect(Collectors.toSet()); + idSet.addAll(assignedGraphIds); + for (long current = start; current < end; current++) { + if (!idSet.contains(current) && checkCount(current)) { + return current; + } + } + return -1L; + } + + private Set assignedGraphIds(String key) { + if (!GRAPH_ID_PREFIX.equals(key)) { + return Collections.emptySet(); + } + // A persisted mapping stays authoritative even if its slot is missing. + return scan(Int64Value.parser(), MetadataKeyHelper.getGraphIDKey("")) + .stream() + .map(Int64Value::getValue) + .collect(Collectors.toSet()); + } + /** * Return key with used Cid */ diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/PartitionManager.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/PartitionManager.java index d078466c68..9325ff71d9 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/PartitionManager.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/meta/PartitionManager.java @@ -100,6 +100,15 @@ public void load() { deletedFileManager.load(); } + public static final class InvalidShardException extends IllegalStateException { + + private static final long serialVersionUID = 1L; + + private InvalidShardException() { + super("Local partition does not belong to this store"); + } + } + public void loadPartition() { loadPartitions(); } @@ -299,7 +308,7 @@ private void loadPartitions() { // businessHandler.dbCompaction(graph, partId); log.error("partition {}-{} is illegal. store id {} not in valid shard group:{}", graph, partId, getStore().getId(), shards2Peers(shards)); - System.exit(0); + throw new InvalidShardException(); } } diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/metric/SystemMetricService.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/metric/SystemMetricService.java index c75d32e343..fbae045518 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/metric/SystemMetricService.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/metric/SystemMetricService.java @@ -163,8 +163,10 @@ private void loadRocksDbInfo(Map map) { Set names = dbFactory.getGraphNames(); if (names != null) { for (String name : names) { - try { - RocksDBSession session = dbFactory.queryGraphDB(name); + try (RocksDBSession session = dbFactory.queryGraphDB(name)) { + if (session == null) { + continue; + } Statistics statistics = session.getRocksDbStats(); map.put( "rocksdb.graph." + name + "." + diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/options/HgStoreEngineOptions.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/options/HgStoreEngineOptions.java index aa5a1af109..e15070bc1b 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/options/HgStoreEngineOptions.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/options/HgStoreEngineOptions.java @@ -53,6 +53,8 @@ public class HgStoreEngineOptions { private QueryPushDownOption queryPushDownOption = new QueryPushDownOption(); // pd server address private String pdAddress; + // PD metadata namespace; must match Server cluster/pd.cluster. + private String pdCluster = "hg"; // External service address private String grpcAddress; // Raft external service address diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/SchemaDriver.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/schema/SchemaDriver.java similarity index 91% rename from hugegraph-struct/src/main/java/org/apache/hugegraph/SchemaDriver.java rename to hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/schema/SchemaDriver.java index 11849b80ae..c81b6e8868 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/SchemaDriver.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/schema/SchemaDriver.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.hugegraph; +package org.apache.hugegraph.store.schema; import java.io.IOException; import java.util.ArrayList; @@ -32,6 +32,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import org.apache.hugegraph.HugeGraphSupplier; import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.id.Id; @@ -64,7 +65,7 @@ public class SchemaDriver { public static final String META_PATH_HUGEGRAPH = "HUGEGRAPH"; public static final String META_PATH_GRAPHSPACE = "GRAPHSPACE"; public static final String META_PATH_GRAPH = "GRAPH"; - public static final String META_PATH_CLUSTER = "hg"; + public static final String DEFAULT_CLUSTER = "hg"; public static final String META_PATH_SCHEMA = "SCHEMA"; public static final String META_PATH_GRAPH_CONF = "GRAPH_CONF"; public static final String META_PATH_PROPERTY_KEY = "PROPERTY_KEY"; @@ -84,16 +85,23 @@ public class SchemaDriver { private static CountDownLatch destroyCompletion = new CountDownLatch(0); // Client for accessing PD private final KvClient client; + private final String cluster; private final SchemaCaches caches; - private SchemaDriver(PDConfig pdConfig, int cacheSize, + private SchemaDriver(PDConfig pdConfig, String cluster, int cacheSize, long expiration) { - this(new KvClient<>(pdConfig), cacheSize, expiration); + this(new KvClient<>(pdConfig), cluster, cacheSize, expiration); } - SchemaDriver(KvClient client, int cacheSize, + SchemaDriver(KvClient client, int cacheSize, long expiration) { + this(client, DEFAULT_CLUSTER, cacheSize, expiration); + } + + SchemaDriver(KvClient client, String cluster, int cacheSize, long expiration) { + E.checkArgument(cluster != null && !cluster.isEmpty(), "PD cluster cannot be empty"); + this.cluster = cluster; this.client = Objects.requireNonNull(client, "client"); this.caches = new SchemaCaches(cacheSize, expiration); try { @@ -117,6 +125,11 @@ public static void init(PDConfig pdConfig) { } public static void init(PDConfig pdConfig, int cacheSize, long expiration) { + init(pdConfig, DEFAULT_CLUSTER, cacheSize, expiration); + } + + public static void init(PDConfig pdConfig, String cluster, int cacheSize, long expiration) { + E.checkArgument(cluster != null && !cluster.isEmpty(), "PD cluster cannot be empty"); synchronized (LIFECYCLE_LOCK) { if (destroying) { throw new NotAllowException("The SchemaDriver is being destroyed"); @@ -129,7 +142,25 @@ public static void init(PDConfig pdConfig, int cacheSize, long expiration) { "allowed to be initialized again", instance.caches.limit(), instance.caches.expiration(), instance.client); } - INSTANCE.set(new SchemaDriver(pdConfig, cacheSize, expiration)); + INSTANCE.set(new SchemaDriver(pdConfig, cluster, cacheSize, expiration)); + } + } + + public static SchemaDriver getOrInit(PDConfig pdConfig, String cluster) { + E.checkArgument(cluster != null && !cluster.isEmpty(), "PD cluster cannot be empty"); + synchronized (LIFECYCLE_LOCK) { + if (destroying) { + throw new NotAllowException("The SchemaDriver is being destroyed"); + } + SchemaDriver instance = INSTANCE.get(); + if (instance == null) { + instance = new SchemaDriver(pdConfig, cluster, 300, 300 * 1000); + INSTANCE.set(instance); + } else if (!instance.cluster.equals(cluster)) { + throw new NotAllowException("SchemaDriver already uses PD cluster '%s', requested '%s'", + instance.cluster, cluster); + } + return instance; } } @@ -206,13 +237,32 @@ private void listenMetaChanges() { private void schemaCacheClearHandler(T response) { List names = this.extractValuesFromResponse(response); - for (String gs : names) { - String[] arr = gs.split(DELIMITER); - assert arr.length == 2; - this.caches.clear(arr[0], arr[1]); + for (String value : names) { + String graph = schemaCacheClearGraph(value); + if (graph == null) { + continue; + } + this.caches.clearByGraphName(graph); log.info(String.format( "Graph '%s' schema clear event is received, deleting all " + - "schema caches under '%s'", gs, gs)); + "schema caches under '%s'", graph, graph)); + } + } + + private String schemaCacheClearGraph(String value) { + if (value == null || value.isEmpty()) { + return null; + } + String payload = value.trim(); + if (!payload.startsWith("{")) { + return value; + } + try { + Object graph = fromJson(payload, Map.class).get("graph"); + return graph instanceof String && !((String) graph).isEmpty() ? (String) graph : null; + } catch (HugeException e) { + log.warn("Ignoring malformed schema cache clear event", e); + return null; } } @@ -380,7 +430,7 @@ private String propertyKeyPrefix(String graphSpace, String graph) { // }/SCHEMA/PROPERTY_KEY/NAME return stringJoin(META_PATH_DELIMITER, META_PATH_HUGEGRAPH, - META_PATH_CLUSTER, + this.cluster, META_PATH_GRAPHSPACE, graphSpace, graph, @@ -394,7 +444,7 @@ private String vertexLabelPrefix(String graphSpace, String graph) { // }/SCHEMA/VERTEX_LABEL/NAME return stringJoin(META_PATH_DELIMITER, META_PATH_HUGEGRAPH, - META_PATH_CLUSTER, + this.cluster, META_PATH_GRAPHSPACE, graphSpace, graph, @@ -408,7 +458,7 @@ private String edgeLabelPrefix(String graphSpace, String graph) { // }/SCHEMA/EDGELABEL/NAME return stringJoin(META_PATH_DELIMITER, META_PATH_HUGEGRAPH, - META_PATH_CLUSTER, + this.cluster, META_PATH_GRAPHSPACE, graphSpace, graph, @@ -422,7 +472,7 @@ private String indexLabelPrefix(String graphSpace, String graph) { // }/SCHEMA/INDEX_LABEL/NAME return stringJoin(META_PATH_DELIMITER, META_PATH_HUGEGRAPH, - META_PATH_CLUSTER, + this.cluster, META_PATH_GRAPHSPACE, graphSpace, graph, @@ -681,7 +731,7 @@ private String graphSpaceRemoveKey() { // HUGEGRAPH/{cluster}/EVENT/GRAPHSPACE/REMOVE return stringJoin(META_PATH_DELIMITER, META_PATH_HUGEGRAPH, - META_PATH_CLUSTER, + this.cluster, META_PATH_EVENT, META_PATH_GRAPHSPACE, META_PATH_REMOVE); @@ -691,7 +741,7 @@ private String graphConfKey(String graphSpace, String graph) { // HUGEGRAPH/{cluster}/GRAPHSPACE/{graphspace}/GRAPH_CONF/{graph} return stringJoin(META_PATH_DELIMITER, META_PATH_HUGEGRAPH, - META_PATH_CLUSTER, + this.cluster, META_PATH_GRAPHSPACE, graphSpace, META_PATH_GRAPH_CONF, @@ -704,7 +754,7 @@ private String nameKey(String graphSpace, String graph, // /{META_PATH_TYPE}/NAME/{name} return stringJoin(META_PATH_DELIMITER, META_PATH_HUGEGRAPH, - META_PATH_CLUSTER, + this.cluster, META_PATH_GRAPHSPACE, graphSpace, graph, @@ -720,7 +770,7 @@ private String idKey(String graphSpace, String graph, // /{META_PATH_TYPE}/ID/{id} return stringJoin(META_PATH_DELIMITER, META_PATH_HUGEGRAPH, - META_PATH_CLUSTER, + this.cluster, META_PATH_GRAPHSPACE, graphSpace, graph, @@ -734,7 +784,7 @@ private String schemaCacheClearKey() { // HUGEGRAPH/{cluster}/EVENT/GRAPH/SCHEMA/CLEAR return stringJoin(META_PATH_DELIMITER, META_PATH_HUGEGRAPH, - META_PATH_CLUSTER, + this.cluster, META_PATH_EVENT, META_PATH_GRAPH, META_PATH_SCHEMA, @@ -745,7 +795,7 @@ private String graphClearKey() { // HUGEGRAPH/{cluster}/EVENT/GRAPH/CLEAR return stringJoin(META_PATH_DELIMITER, META_PATH_HUGEGRAPH, - META_PATH_CLUSTER, + this.cluster, META_PATH_EVENT, META_PATH_GRAPH, META_PATH_CLEAR); @@ -755,7 +805,7 @@ private String graphRemoveKey() { // HUGEGRAPH/{cluster}/EVENT/GRAPH/REMOVE return stringJoin(META_PATH_DELIMITER, META_PATH_HUGEGRAPH, - META_PATH_CLUSTER, + this.cluster, META_PATH_EVENT, META_PATH_GRAPH, META_PATH_REMOVE); @@ -899,9 +949,11 @@ public void clearAll() { } public void clear(String graphSpace, String graph) { - ConcurrentHashMap - schemaCaches = - this.caches.get(stringJoin(DELIMITER, graphSpace, graph)); + this.clearByGraphName(stringJoin(DELIMITER, graphSpace, graph)); + } + + public void clearByGraphName(String graphName) { + ConcurrentHashMap schemaCaches = this.caches.get(graphName); if (schemaCaches != null) { schemaCaches.clear(); } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/SchemaGraph.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/schema/SchemaGraph.java similarity index 91% rename from hugegraph-struct/src/main/java/org/apache/hugegraph/SchemaGraph.java rename to hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/schema/SchemaGraph.java index ed7241f7ad..ba9270e280 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/SchemaGraph.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/schema/SchemaGraph.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.hugegraph; +package org.apache.hugegraph.store.schema; import java.util.ArrayList; import java.util.Collection; @@ -26,6 +26,7 @@ import org.apache.commons.configuration2.Configuration; import org.apache.commons.configuration2.MapConfiguration; +import org.apache.hugegraph.HugeGraphSupplier; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.pd.client.PDConfig; @@ -40,30 +41,21 @@ public class SchemaGraph implements HugeGraphSupplier { private final String graphSpace; private final String graph; - private final PDConfig pdConfig; private final HugeConfig config; private final SchemaDriver schemaDriver; public SchemaGraph(String graphSpace, String graph, PDConfig pdConfig) { + this(graphSpace, graph, pdConfig, SchemaDriver.DEFAULT_CLUSTER); + } + + public SchemaGraph(String graphSpace, String graph, PDConfig pdConfig, String cluster) { this.graphSpace = graphSpace; this.graph = graph; - this.pdConfig = pdConfig; - this.schemaDriver = schemaDriverInit(); + this.schemaDriver = SchemaDriver.getOrInit(pdConfig, cluster); this.config = this.loadConfig(); } - private SchemaDriver schemaDriverInit() { - if (SchemaDriver.getInstance() == null) { - synchronized (SchemaDriver.class) { - if (SchemaDriver.getInstance() == null) { - SchemaDriver.init(this.pdConfig); - } - } - } - return SchemaDriver.getInstance(); - } - private HugeConfig loadConfig() { // Load configuration from PD Map configs = diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/util/IpUtil.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/util/IpUtil.java index ce0ef1a180..f9c5591e9e 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/util/IpUtil.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/util/IpUtil.java @@ -34,12 +34,13 @@ public class IpUtil { /** - * Get all IPv4 addresses + * Get local IPv4 addresses. * - * @return all ipv4 addr + * @param forFallback whether link-local and loopback addresses are excluded + * @return ipv4 addr * @throws SocketException io error or no network interface */ - private static List getIpAddress() throws SocketException { + private static List getIpAddress(boolean forFallback) throws SocketException { List list = new LinkedList<>(); Enumeration enumeration = NetworkInterface.getNetworkInterfaces(); while (enumeration.hasMoreElements()) { @@ -47,9 +48,14 @@ private static List getIpAddress() throws SocketException { Enumeration addresses = network.getInetAddresses(); while (addresses.hasMoreElements()) { InetAddress address = (InetAddress) addresses.nextElement(); - if (address != null && (address instanceof Inet4Address)) { - list.add(address.getHostAddress()); + if (!(address instanceof Inet4Address)) { + continue; } + if (forFallback && (address.isLoopbackAddress() || + address.isLinkLocalAddress())) { + continue; + } + list.add(address.getHostAddress()); } } return list; @@ -63,8 +69,21 @@ private static List getIpAddress() throws SocketException { */ public static String getNearestAddress(String raftAddress) { try { - List ipv4s = getIpAddress(); String[] tmp = raftAddress.split(":"); + if (!isDottedIpv4(tmp[0])) { + return raftAddress; + } + + // A configured literal that is bound locally stays, including + // loopback. The default raft address is 127.0.0.1 and the + // partition engine uses that same value as its PeerId. + // Link-local and loopback are dropped only from fallback + // candidates, when the configured IPv4 is not local. + if (getIpAddress(false).contains(tmp[0])) { + return raftAddress; + } + + List ipv4s = getIpAddress(true); if (ipv4s.size() == 0) { throw new Exception("no available ipv4"); } @@ -94,8 +113,35 @@ public static String getNearestAddress(String raftAddress) { } catch (SocketException e) { log.error("getIpAddress, get ip failed, {}", e.getMessage()); } catch (Exception e) { - log.error("getRaftAddress, got exception, {}", e.getMessage()); + log.error("getNearestAddress, got exception, {}", e.getMessage()); } return raftAddress; } + + /** + * A dotted IPv4 literal is four numeric octets. Hostnames must not be + * parsed as addresses; {@code Integer.parseInt} on a DNS label is not a + * signal to log or to replace the configured host. + */ + private static boolean isDottedIpv4(String host) { + String[] parts = host.split("\\.", -1); + if (parts.length != 4) { + return false; + } + for (String part : parts) { + if (part.isEmpty() || part.length() > 3) { + return false; + } + for (int i = 0; i < part.length(); i++) { + char c = part.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + if (Integer.parseInt(part) > 255) { + return false; + } + } + return true; + } } diff --git a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/util/UnsafeUtil.java b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/util/UnsafeUtil.java index bff726d4fd..e36565c858 100644 --- a/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/util/UnsafeUtil.java +++ b/hugegraph-store/hg-store-core/src/main/java/org/apache/hugegraph/store/util/UnsafeUtil.java @@ -52,11 +52,7 @@ public static boolean hasUnsafe() { public static Object getUnsafe0() { Object unsafe; try { - // Accessing the Unsafe class and its singleton instance - final Class unsafeClass = Class.forName("sun.misc.Unsafe"); - final Field unsafeField = unsafeClass.getDeclaredField("theUnsafe"); - unsafeField.setAccessible(true); // Bypass access checks - unsafe = unsafeField.get(null); // Get the Unsafe instance + unsafe = org.apache.hugegraph.util.UnsafeUtil.getUnsafe(); } catch (final Throwable t) { // Log a warning if Unsafe is not available if (log.isWarnEnabled()) { diff --git a/hugegraph-store/hg-store-dist/pom.xml b/hugegraph-store/hg-store-dist/pom.xml index 12acb0ba9e..ab53403f3f 100644 --- a/hugegraph-store/hg-store-dist/pom.xml +++ b/hugegraph-store/hg-store-dist/pom.xml @@ -38,10 +38,18 @@ hg-store-node + + com.taobao.arthas + arthas-packaging + ${arthas.version} + provided + org.apache.hugegraph hg-store-node ${revision} + exec + runtime @@ -91,6 +99,33 @@ maven-antrun-plugin 1.3 + + prepare-arthas + prepare-package + + run + + + + + + + + + + + + + + + + + + + + package diff --git a/hugegraph-store/hg-store-dist/src/assembly/descriptor/server-assembly.xml b/hugegraph-store/hg-store-dist/src/assembly/descriptor/server-assembly.xml index 522a93c917..ee2b6af9bf 100644 --- a/hugegraph-store/hg-store-dist/src/assembly/descriptor/server-assembly.xml +++ b/hugegraph-store/hg-store-dist/src/assembly/descriptor/server-assembly.xml @@ -25,7 +25,10 @@ - + + ${project.build.directory}/arthas + lib/arthas + ${assembly.static.dir}/bin bin @@ -46,15 +49,44 @@ + + + ${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/bin/ArthasAttach.java + bin + 644 + + + ${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/bin/arthas.sh + bin + 755 + + + ${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/bin/prepare-topling.sh + bin + 755 + + + ${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/bin/preload-topling.sh + bin + 755 + + + ${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/conf/toplingdb.yaml + conf + 644 + + + /lib + ${artifact.artifactId}-${artifact.version}.jar false runtime false - org.apache.hugegraph:${executable.jar.name}:jar:* + org.apache.hugegraph:${executable.jar.name}:jar:exec:* diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/jvm-module.options b/hugegraph-store/hg-store-dist/src/assembly/static/bin/jvm-module.options new file mode 100644 index 0000000000..b41209f448 --- /dev/null +++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/jvm-module.options @@ -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. +# + +# Hessian reflects into StackTraceElement when serializing Raft operations. +--add-opens=java.base/java.lang=ALL-UNNAMED diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh index 88165e47b6..d1ccc651c7 100755 --- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh +++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh @@ -36,9 +36,26 @@ LOGS="$TOP/logs" OUTPUT=${LOGS}/hugegraph-store-server.log GITHUB="https://github.com" PID_FILE="$BIN/pid" +JVM_MODULE_OPTIONS="$BIN/jvm-module.options" + +if [[ ! -f "$JVM_MODULE_OPTIONS" || ! -r "$JVM_MODULE_OPTIONS" ]]; then + echo "Error: JVM module options file is missing or unreadable: $JVM_MODULE_OPTIONS" >&2 + exit 1 +fi . "$BIN"/util.sh +# Keep caller preloads visible to the runtime selector, including conflicting JNI. +function preload_jemalloc() { + local inherited="${LD_PRELOAD//:/ }" entry + local -a preloads + read -r -a preloads <<< "${inherited//$'\n'/ }" + for entry in "${preloads[@]-}"; do + [ "$entry" != "$1" ] || return 0 + done + export LD_PRELOAD="$1${LD_PRELOAD:+:$LD_PRELOAD}" +} + arch=$(uname -m) echo "Current arch: $arch" @@ -47,7 +64,7 @@ if [[ $arch == "aarch64" || $arch == "arm64" ]]; then download_url="${GITHUB}/apache/hugegraph-doc/raw/binary-1.5/dist/server/libjemalloc_aarch64.so" expected_md5="2a631d2f81837f9d5864586761c5e380" if download_and_verify "$download_url" "$lib_file" "$expected_md5"; then - export LD_PRELOAD="$lib_file" + preload_jemalloc "$lib_file" else echo "Failed to verify or download $lib_file, skip it" fi @@ -56,7 +73,7 @@ elif [[ $arch == "x86_64" ]]; then download_url="${GITHUB}/apache/hugegraph-doc/raw/binary-1.5/dist/server/libjemalloc.so" expected_md5="fd61765eec3bfea961b646c269f298df" if download_and_verify "$download_url" "$lib_file" "$expected_md5"; then - export LD_PRELOAD="$lib_file" + preload_jemalloc "$lib_file" else echo "Failed to verify or download $lib_file, skip it" fi @@ -112,7 +129,7 @@ while getopts "d:g:j:y:" arg; do # Telemetry is used to collect metrics, traces and logs y) OPEN_TELEMETRY="$OPTARG" ;; d) DAEMON="$OPTARG" ;; - ?) echo "USAGE: $0 [-d true|false] [-g g1] [-j xxx] [-y true|false]" && exit 1 ;; + ?) echo "USAGE: $0 [-d true|false] [-g g1|ZGC] [-j xxx] [-y true|false]" && exit 1 ;; esac done @@ -122,7 +139,7 @@ ensure_path_writable "$PLUGINS" # The maximum and minimum heap memory that service can use (for production env set it 36GB) MAX_MEM=$((2 * 1024)) MIN_MEM=$((1 * 512)) -EXPECT_JDK_VERSION=11 +EXPECT_JDK_VERSION=17 # Change to $BIN's parent cd ${TOP} || exit @@ -135,8 +152,11 @@ else fi # check jdk version -JAVA_VERSION=$($JAVA -version 2>&1 | awk 'NR==1{gsub(/"/,""); print $3}' | awk -F'_' '{print $1}') -if [[ $? -ne 0 || $JAVA_VERSION < $EXPECT_JDK_VERSION ]]; then +JAVA_VERSION=$($JAVA -version 2>&1 | + awk -F'"' '/^(java|openjdk) version "/ {print $2; exit}' | + sed 's/^1\.//' | cut -d'.' -f1) +JAVA_VERSION="${JAVA_VERSION%%[!0-9]*}" +if [[ -z $JAVA_VERSION || $JAVA_VERSION -lt $EXPECT_JDK_VERSION ]]; then echo "Please make sure that the JDK is installed and the version >= $EXPECT_JDK_VERSION" >> ${OUTPUT} exit 1 fi @@ -156,22 +176,27 @@ if [ "$JAVA_OPTIONS" = "" ]; then JAVA_OPTIONS="${JAVA_OPTIONS} -Xlog:gc=info:file=./logs/gc.log:time,uptime,level,tags:filecount=3,filesize=100m" fi -# Using G1GC as the default garbage collector (Recommended for large memory machines) +# Keep JVM/caller GC selection by default; explicitly select G1 when requested. case "$GC_OPTION" in - "") - echo "Using G1GC as the default garbage collector" - JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+ParallelRefProcEnabled \ - -XX:InitiatingHeapOccupancyPercent=50 -XX:G1RSetUpdatingPauseTimePercent=5" + ""|g1|G1) + if [[ "$GC_OPTION" == g1 || "$GC_OPTION" == G1 ]]; then + echo "Using G1GC" + JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseG1GC" + else + echo "Using JVM garbage collector configuration" + fi + JAVA_OPTIONS="-XX:+ParallelRefProcEnabled -XX:InitiatingHeapOccupancyPercent=50 \ + -XX:G1RSetUpdatingPauseTimePercent=5 ${JAVA_OPTIONS}" ;; zgc|ZGC) - echo "Using ZGC as the default garbage collector (Only support Java 11+)" + echo "Using ZGC as the default garbage collector (requires Java 17 or later)" JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseZGC -XX:+UnlockExperimentalVMOptions \ -XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 \ -XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 \ -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive" ;; *) - echo "Unrecognized gc option: '$GC_OPTION', default use g1, options only support 'ZGC' now" >> ${OUTPUT} + echo "Unrecognized gc option: '$GC_OPTION', supported options: g1, ZGC" >> ${OUTPUT} exit 1 esac @@ -226,17 +251,28 @@ fi echo "Starting HG-StoreServer..." +source "$BIN/preload-topling.sh" || exit 1 +BOOT_JARS=("${LIB}"/hg-store-node-*.jar) +if [ "${#BOOT_JARS[@]}" -ne 1 ] || [ ! -f "${BOOT_JARS[0]}" ]; then + echo "Error: expected one component executable JAR in $LIB" >&2 + exit 1 +fi +JAVA_MAIN=(-jar "${BOOT_JARS[0]}") +if [ -n "${TOPLING_RUNTIME_CLASSPATH:-}" ]; then + JAVA_MAIN=(-cp "${TOPLING_RUNTIME_CLASSPATH}:${BOOT_JARS[0]}" org.springframework.boot.loader.JarLauncher) +fi + # Turn on security check if [[ $DAEMON == "true" ]]; then echo "Starting HugeGraphStoreServer in daemon mode..." if [[ "${STDOUT_MODE:-false}" == "true" ]]; then - exec ${JAVA} -Dname="HugeGraphStore" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ + exec ${JAVA} "@$JVM_MODULE_OPTIONS" -Dname="HugeGraphStore" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ -Dspring.config.location=${CONF}/application.yml \ - ${LIB}/hg-store-node-*.jar & + "${JAVA_MAIN[@]}" "$@" & else - exec ${JAVA} -Dname="HugeGraphStore" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ + exec ${JAVA} "@$JVM_MODULE_OPTIONS" -Dname="HugeGraphStore" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ -Dspring.config.location=${CONF}/application.yml \ - ${LIB}/hg-store-node-*.jar >> ${OUTPUT} 2>&1 & + "${JAVA_MAIN[@]}" "$@" >> ${OUTPUT} 2>&1 & fi PID="$!" # Write pid to file @@ -248,12 +284,12 @@ else echo "$$" > "$PID_FILE" echo "[+pid] $$" if [[ "${STDOUT_MODE:-false}" == "true" ]]; then - exec ${JAVA} -Dname="HugeGraphStore" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ + exec ${JAVA} "@$JVM_MODULE_OPTIONS" -Dname="HugeGraphStore" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ -Dspring.config.location=${CONF}/application.yml \ - ${LIB}/hg-store-node-*.jar + "${JAVA_MAIN[@]}" "$@" else - exec ${JAVA} -Dname="HugeGraphStore" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ + exec ${JAVA} "@$JVM_MODULE_OPTIONS" -Dname="HugeGraphStore" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ -Dspring.config.location=${CONF}/application.yml \ - ${LIB}/hg-store-node-*.jar >> ${OUTPUT} 2>&1 + "${JAVA_MAIN[@]}" "$@" >> ${OUTPUT} 2>&1 fi fi diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh index 8f898df7da..e9f503b02a 100644 --- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh +++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh @@ -41,8 +41,8 @@ if [ ! -f ${PID_FILE} ]; then fi PID=`cat $PID_FILE` -kill_process_and_wait "HugeGraphStoreServer" "$PID" "$SERVER_SHUTDOWN_TIMEOUT_S" - -if [ $? -eq 0 ]; then +if kill_process_and_wait "HugeGraphStoreServer" "$PID" "$SERVER_SHUTDOWN_TIMEOUT_S"; then rm "$PID_FILE" +else + exit 1 fi diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh index e45249a04b..d905db9a8d 100644 --- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh +++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh @@ -330,7 +330,9 @@ download_and_verify() { local tmp # mktemp, not $$: a PID is shared by concurrent background subshells. tmp=$(mktemp -- "${filepath}.XXXXXX") || return 1 - if curl -fL -o "$tmp" -- "$url"; then + # jemalloc is optional: an unavailable or stalled mirror must not block + # Store startup indefinitely. Bound both connection and complete transfer. + if curl -fL --connect-timeout 5 --max-time 30 -o "$tmp" -- "$url"; then actual_md5=$(md5sum -- "$tmp" | awk '{ print $1 }') if [[ "$actual_md5" != "$expected_md5" ]]; then echo "MD5 checksum verification failed for $filepath after download. Expected: $expected_md5, but got: $actual_md5" diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/conf/application-pd.yml b/hugegraph-store/hg-store-dist/src/assembly/static/conf/application-pd.yml index 0315c4b4fe..56cbdd8e63 100644 --- a/hugegraph-store/hg-store-dist/src/assembly/static/conf/application-pd.yml +++ b/hugegraph-store/hg-store-dist/src/assembly/static/conf/application-pd.yml @@ -26,7 +26,18 @@ management: include: "*" rocksdb: + # Match the explicit launcher opt-in when enabling Topling. + provider: rocksdb # rocksdb total memory usage, force flush to disk when reaching this value + # TODO: default this lower, or size it from the container memory limit, or map it from an + # HG_STORE_* variable in the Docker entrypoint. A default of 0 only makes AppConfig fall back + # to the JVM max heap, which is another heap-sized native budget, not a safe container total. + # It is the capacity of the RocksDB block and write LRU caches (RaftRocksdbOptions), not an + # upfront allocation, but pinned at 32 GB those native caches can grow past a smaller + # container memory limit under load and get the Store OOM-killed. Until then the only + # override is the JVM system property -Drocksdb.total_memory_size= (JAVA_OPTS, the + # Helm chart's store.javaOpts; helm/hugegraph). + # https://github.com/apache/hugegraph/issues/3254 total_memory_size: 32000000000 # memtable size used by rocksdb write_buffer_size: 32000000 diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/conf/application.yml b/hugegraph-store/hg-store-dist/src/assembly/static/conf/application.yml index 8e2b8d4f74..90a5cecbb2 100644 --- a/hugegraph-store/hg-store-dist/src/assembly/static/conf/application.yml +++ b/hugegraph-store/hg-store-dist/src/assembly/static/conf/application.yml @@ -62,3 +62,6 @@ logging: config: 'file:./conf/log4j2.xml' level: root: info + + +# HTTP Arthas activation is disabled. Use bin/arthas.sh locally without restarting Store. diff --git a/hugegraph-store/hg-store-dist/src/assembly/travis/test-store-preloads.sh b/hugegraph-store/hg-store-dist/src/assembly/travis/test-store-preloads.sh new file mode 100755 index 0000000000..e5ad4e2495 --- /dev/null +++ b/hugegraph-store/hg-store-dist/src/assembly/travis/test-store-preloads.sh @@ -0,0 +1,143 @@ +#!/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. +# +# Complete launcher fixtures with Java/download substitutes, not business JVM or JNI execution. +set -Eeuo pipefail +SOURCE_BIN="$(cd "$(dirname "${BASH_SOURCE[0]}")/../static/bin" && pwd)" +REPO_ROOT="$(cd "$SOURCE_BIN/../../../../../.." && pwd)" +SHARED_BIN="$REPO_ROOT/hugegraph-server/hugegraph-dist/src/assembly/static/bin" +TEST_ROOT=$(mktemp -d) +TEST_ROOT=$(cd "$TEST_ROOT" && pwd -P) +trap 'rm -rf "$TEST_ROOT"' EXIT +mkdir -p "$TEST_ROOT/fake-bin" "$TEST_ROOT/java/bin" +cat > "$TEST_ROOT/fake-bin/uname" <<'MOCK' +#!/bin/bash +case "$1" in -m) echo "$FIXTURE_ARCH" ;; *) echo Linux ;; esac +MOCK +cat > "$TEST_ROOT/fake-bin/ps" <<'MOCK' +#!/bin/bash +exit 0 +MOCK +cat > "$TEST_ROOT/java/bin/java" <<'MOCK' +#!/bin/bash +if [ "$1" = -version ]; then + printf '%s\n' "${LD_PRELOAD:-}" > "$VERSION_CAPTURE" + echo 'openjdk version "17.0.1"' >&2 +else + printf '%s\n' "${LD_PRELOAD:-}" > "$MAIN_CAPTURE" + printf '%s\n' "$@" > "$MAIN_CAPTURE.args" +fi +MOCK +chmod +x "$TEST_ROOT/fake-bin/"* "$TEST_ROOT/java/bin/java" + +run_launcher() { + local arch="$1" download="$2" provider="$3" inherited="$4" name="$5" + local top="$TEST_ROOT/$name" + local -a provider_env=(-u TOPLINGDB_ROCKSDB_PROVIDER) + [ "$provider" = default ] || provider_env=("TOPLINGDB_ROCKSDB_PROVIDER=$provider") + mkdir -p "$top/bin" "$top/conf" "$top/lib" "$top/topling" + cp "$SOURCE_BIN/start-hugegraph-store.sh" "$SOURCE_BIN/jvm-module.options" "$top/bin/" + cp "$SHARED_BIN/preload-topling.sh" "$top/bin/" + cat > "$top/bin/util.sh" <<'MOCK' +# Substitute only utility calls needed by this launcher fixture. +ensure_path_writable() { mkdir -p "$1"; } +download_and_verify() { + [ "$FIXTURE_DOWNLOAD" = success ] || return 1 + touch "$2" +} +MOCK + touch "$top/lib/hg-store-node-fixture.jar" "$top/topling/rocksdbjni.jar" \ + "$top/topling/librocksdbjni-linux64.so" + (cd "$top/topling" && sha256sum rocksdbjni.jar librocksdbjni-linux64.so > easy-migrate.sha256) + printf '%s\n' 'http: {auto_start_http: false}' > "$top/conf/toplingdb.yaml" + VERSION_CAPTURE="$top/version-preloads" MAIN_CAPTURE="$top/main-preloads" \ + FIXTURE_ARCH="$arch" FIXTURE_DOWNLOAD="$download" JAVA_HOME="$TEST_ROOT/java" \ + JAVA_OPTIONS=-Xmx64m STDOUT_MODE=true LD_PRELOAD="$inherited" \ + PATH="$TEST_ROOT/fake-bin:$PATH" \ + env -u TOPLING_ACTIVE_NATIVE -u TOPLING_RUNTIME_CLASSPATH -u TOPLINGDB_EASY_MIGRATE_CONF \ + -u TOPLING_AUTO_MIGRATE_CONF "${provider_env[@]}" bash "$top/bin/start-hugegraph-store.sh" -d false \ + > "$top/launcher.log" 2>&1 +} + +# Both allocation-download paths preserve tracing/allocator entries before the selector runs. +for arch in x86_64 aarch64 arm64; do + for download in success failure; do + for separator in colon space; do + inherited=/caller/libtrace.so:/caller/liballoc.so + [ "$separator" != space ] || inherited='/caller/libtrace.so /caller/liballoc.so' + name="$arch-$download-$separator" + top="$TEST_ROOT/$name" + jemalloc="$top/bin/libjemalloc.so" + [ "$arch" = x86_64 ] || jemalloc="$top/bin/libjemalloc_aarch64.so" + expected="$inherited" + [ "$download" != success ] || expected="$jemalloc:$inherited" + run_launcher "$arch" "$download" rocksdb "$inherited" "$name" || { + cat "$top/launcher.log" >&2; exit 1; + } + test "$(cat "$top/version-preloads")" = "$expected" + test "$(cat "$top/main-preloads")" = "$expected" + grep -qx -- '-jar' "$top/main-preloads.args" + + default_top="$TEST_ROOT/$name-default" + default_expected="${expected//$top/$default_top}" + run_launcher "$arch" "$download" default "$inherited" "$name-default" + test "$(cat "$default_top/main-preloads")" = "$default_expected" + + # A successful allocator download must not hide recognizable competing JNI. + for provider in default rocksdb topling; do + bad="$name-conflict-$provider" + if run_launcher "$arch" "$download" "$provider" \ + "$inherited:/other/librocksdbjni.so" "$bad"; then + echo "FAIL: competing JNI escaped the full $arch/$download launcher" >&2 + exit 1 + fi + grep -q 'competing RocksDB JNI in LD_PRELOAD' "$TEST_ROOT/$bad/launcher.log" + test ! -e "$TEST_ROOT/$bad/main-preloads" + grep -q '/caller/libtrace.so' "$TEST_ROOT/$bad/version-preloads" + grep -q '/caller/liballoc.so' "$TEST_ROOT/$bad/version-preloads" + done + done + done + + # A caller already using the same allocator keeps its order without a duplicate entry. + name="$arch-already-loaded" + jemalloc="$TEST_ROOT/$name/bin/libjemalloc.so" + [ "$arch" = x86_64 ] || jemalloc="$TEST_ROOT/$name/bin/libjemalloc_aarch64.so" + inherited="/caller/libtrace.so:$jemalloc:/caller/liballoc.so" + run_launcher "$arch" success rocksdb "$inherited" "$name" + test "$(cat "$TEST_ROOT/$name/main-preloads")" = "$inherited" +done + +# Topling is supported only on x86_64; selected JNI precedes allocator and caller libraries. +for download in success failure; do + name="topling-$download" + top="$TEST_ROOT/$name" + inherited=/caller/libtrace.so:/caller/liballoc.so + expected="$inherited" + [ "$download" != success ] || expected="$top/bin/libjemalloc.so:$expected" + run_launcher x86_64 "$download" topling "$inherited" "$name" + test "$(cat "$top/main-preloads")" = "$top/topling/librocksdbjni-linux64.so:$expected" + grep -qx -- '-cp' "$top/main-preloads.args" + grep -qx 'org.springframework.boot.loader.JarLauncher' "$top/main-preloads.args" + name="arm-topling-$download" + if run_launcher aarch64 "$download" topling "$inherited" "$name"; then + echo 'FAIL: Topling was accepted on unsupported ARM' >&2; exit 1 + fi + grep -q 'Topling requires Linux x86_64' "$TEST_ROOT/$name/launcher.log" + test ! -e "$TEST_ROOT/$name/main-preloads" +done +echo 'PASS: complete Store launcher preserves preloads and rejects competing JNI across allocator paths' diff --git a/hugegraph-store/hg-store-grpc/pom.xml b/hugegraph-store/hg-store-grpc/pom.xml index 75f4594a0d..edf96a82a9 100644 --- a/hugegraph-store/hg-store-grpc/pom.xml +++ b/hugegraph-store/hg-store-grpc/pom.xml @@ -32,7 +32,8 @@ 1.6.2 - 1.39.0 + + 1.39.0 3.17.2 0.6.1 @@ -89,7 +90,7 @@ grpc-java - io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:${grpc.codegen.version}:exe:${os.detected.classifier} ${project.basedir}/src/main/proto diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/SchemaStatus.java b/hugegraph-store/hg-store-grpc/src/main/dev/org/apache/hugegraph/store/grpc/query/QueryResultFormat.java similarity index 50% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/SchemaStatus.java rename to hugegraph-store/hg-store-grpc/src/main/dev/org/apache/hugegraph/store/grpc/query/QueryResultFormat.java index 4a80a6a68c..132833cf9e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/SchemaStatus.java +++ b/hugegraph-store/hg-store-grpc/src/main/dev/org/apache/hugegraph/store/grpc/query/QueryResultFormat.java @@ -15,49 +15,25 @@ * limitations under the License. */ -package org.apache.hugegraph.type.define; +package org.apache.hugegraph.store.grpc.query; -public enum SchemaStatus implements SerialEnum { - - CREATED(1, "created"), - - CREATING(2, "creating"), - - REBUILDING(3, "rebuilding"), - - DELETING(4, "deleting"), - - UNDELETED(5, "undeleted"), - - INVALID(6, "invalid"); - - private byte code = 0; - private String name = null; - - static { - SerialEnum.register(SchemaStatus.class); - } - - SchemaStatus(int code, String name) { - assert code < 256; - this.code = (byte) code; - this.name = name; - } - - public boolean ok() { - return this == CREATED; - } +/** + * Existing query responses carry either persistent rows or tagged graph elements. + * Keep their selection aligned with the node's deserialization stage. + */ +public final class QueryResultFormat { - public boolean deleting() { - return this == DELETING || this == UNDELETED; + private QueryResultFormat() { } - @Override - public byte code() { - return this.code; + public static boolean returnsTaggedElements(QueryRequest request) { + return request.getScanType() == ScanType.NO_SCAN && request.getLoadPropertyFromIndex() || + requiresElementDeserialization(request); } - public String string() { - return this.name; + public static boolean requiresElementDeserialization(QueryRequest request) { + return request.getOrderByCount() > 0 || request.getPropertyCount() > 0 || + !request.getCondition().isEmpty() || + request.getFunctionsCount() > 0 && !request.getGroupBySchemaLabel(); } } diff --git a/hugegraph-store/hg-store-node/pom.xml b/hugegraph-store/hg-store-node/pom.xml index aff68d0db0..2a20008500 100644 --- a/hugegraph-store/hg-store-node/pom.xml +++ b/hugegraph-store/hg-store-node/pom.xml @@ -35,7 +35,7 @@ org.springframework.boot spring-boot-starter - 2.5.14 + ${spring-boot.version} org.springframework.boot @@ -46,7 +46,7 @@ org.springframework.boot spring-boot-starter-actuator - 2.5.14 + ${spring-boot.version} io.micrometer @@ -61,7 +61,7 @@ org.springframework.boot spring-boot-starter-web - 2.5.14 + ${spring-boot.version} @@ -73,7 +73,7 @@ org.springframework.boot spring-boot-starter-jetty - 2.5.14 + ${spring-boot.version} io.github.lognet @@ -87,7 +87,7 @@ org.springframework.boot spring-boot-starter-test - 2.5.14 + ${spring-boot.version} test @@ -99,7 +99,7 @@ com.google.protobuf protobuf-java-util - 3.17.2 + ${protobuf.runtime.version} org.apache.hugegraph @@ -121,17 +121,6 @@ hg-store-core - - com.taobao.arthas - arthas-agent-attach - 3.6.4 - - - com.taobao.arthas - arthas-packaging - 3.6.4 - - org.apache.logging.log4j log4j-core @@ -148,6 +137,12 @@ log4j-jul 2.17.2 + + org.apache.hugegraph + hg-store-client + ${revision} + test + @@ -163,6 +158,7 @@ repackage + exec org.apache.hugegraph.store.node.StoreNodeApplication diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/AppConfig.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/AppConfig.java index 3f1624c087..0bc57d22dd 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/AppConfig.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/AppConfig.java @@ -40,6 +40,9 @@ public class AppConfig { @Value("${pdserver.address}") private String pdServerAddress; + @Value("${pd.cluster:hg}") + private String pdCluster = "hg"; + @Value("${grpc.host}") private String host; @@ -211,10 +214,10 @@ public class ArthasConfig { @Value("${arthas.httpPort:8565}") private String httpPort; - @Value("${arthas.ip:0.0.0.0}") + @Value("${arthas.ip:127.0.0.1}") private String arthasip; - @Value("${arthas.disabledCommands:jad}") + @Value("${arthas.disabledCommands:}") private String disCmd; } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/StoreNodeApplication.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/StoreNodeApplication.java index c793ed96f2..2f96a34d0c 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/StoreNodeApplication.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/StoreNodeApplication.java @@ -17,8 +17,8 @@ package org.apache.hugegraph.store.node; -import org.apache.hugegraph.store.node.listener.ContextClosedListener; import org.apache.hugegraph.store.node.listener.PdConfigureListener; +import org.apache.hugegraph.util.RocksDBRuntime; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @@ -31,14 +31,15 @@ @SpringBootApplication public class StoreNodeApplication { - //TODO Is this OK? - private final AppShutdownHook shutdownHook = new AppShutdownHook(Thread.currentThread()); - public static void main(String[] args) { - start(); + start(args); } public static void start() { + start(new String[0]); + } + + public static void start(String... args) { // Set the log location for the slot usage String logPath = System.getProperty("logging.path"); if (StringUtils.isBlank(logPath)) { @@ -55,10 +56,11 @@ public static void start() { } SpringApplication application = new SpringApplication(StoreNodeApplication.class); PdConfigureListener listener = new PdConfigureListener(); - ContextClosedListener closedListener = new ContextClosedListener(); application.addListeners(listener); - application.addListeners(closedListener); - ConfigurableApplicationContext context = application.run(); + // PD environment overrides are applied before context initialization. + application.addInitializers(context -> RocksDBRuntime.verify( + context.getEnvironment().getProperty("rocksdb.provider", "rocksdb"))); + ConfigurableApplicationContext context = application.run(args); listener.setContext(context); System.out.println("StoreNodeApplication started."); } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/FixGraphIdController.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/FixGraphIdController.java index 43a97e814c..e721d66469 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/FixGraphIdController.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/FixGraphIdController.java @@ -106,16 +106,14 @@ public String getNextId(@PathVariable(value = "partition_id") int pid) throws IO @PostMapping(value = "/update_graph_id/{partition_id}", produces = "application/json") public String updateGraphId(@PathVariable(value = "partition_id") int pid, @RequestBody Map idMap) throws IOException { + GraphIdManager.checkGraphIds(idMap, pid); + var handler = (BusinessHandlerImpl) nodeService.getStoreEngine().getBusinessHandler(); try (var manager = new GraphIdManager(handler, pid)) { - idMap.forEach((graphName, graphId) -> { - log.info("update graph id of {} to {}, partition, {}", graphName, graphId, pid); - var graphIdKey = MetadataKeyHelper.getGraphIDKey(graphName); - var slotKey = manager.genCIDSlotKey(GRAPH_ID_PREFIX, graphId); - var value = Int64Value.of(graphId); - manager.put(graphIdKey, value); - manager.put(slotKey, value); - }); + idMap.forEach((graphName, graphId) -> + log.info("update graph id of {} to {}, partition, {}", + graphName, graphId, pid)); + manager.updateGraphIds(idMap); manager.flush(); } handler.getKeyCreator().clearCache(pid); diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/PartitionAPI.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/PartitionAPI.java index 39f500ad93..59cff949e2 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/PartitionAPI.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/PartitionAPI.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,11 +34,12 @@ import org.apache.hugegraph.store.business.InnerKeyCreator; import org.apache.hugegraph.store.meta.Partition; import org.apache.hugegraph.store.metric.HgStoreMetric; -import org.apache.hugegraph.store.node.AppConfig; import org.apache.hugegraph.store.node.grpc.HgStoreNodeService; import org.apache.hugegraph.util.Bytes; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -47,7 +49,6 @@ import com.alipay.sofa.jraft.entity.PeerId; import com.alipay.sofa.jraft.util.Endpoint; -import com.taobao.arthas.agent.attach.ArthasAgent; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -60,9 +61,6 @@ public class PartitionAPI { @Autowired HgStoreNodeService nodeService; - @Autowired - AppConfig appConfig; - @GetMapping(value = "/partitions", produces = "application/json") public Map getPartitions( @RequestParam(required = false, defaultValue = "") String flags) { @@ -198,19 +196,11 @@ public Map cleanPartition(@PathVariable(value = "id") int id) th } @GetMapping(value = "/arthasstart", produces = "application/json") - public Map arthasstart( - @RequestParam(required = false, defaultValue = "") String flags) { - HashMap configMap = new HashMap<>(); - configMap.put("arthas.telnetPort", appConfig.getArthasConfig().getTelnetPort()); - configMap.put("arthas.httpPort", appConfig.getArthasConfig().getHttpPort()); - configMap.put("arthas.ip", appConfig.getArthasConfig().getArthasip()); - configMap.put("arthas.disabledCommands", appConfig.getArthasConfig().getDisCmd()); - ArthasAgent.attach(configMap); - // TODO: remove this commented-out line, retPose is never used -// DashResponse retPose = new DashResponse(); - List ret = new ArrayList<>(); - ret.add("Arthas started successfully"); - return okMap("arthasstart", ret); + public ResponseEntity> arthasstart() { + // Store has no administrator authentication. Reject all HTTP callers, + // including loopback proxies, and use OS permissions for local attach. + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Collections.singletonMap( + "message", "HTTP Arthas activation is disabled; run bin/arthas.sh on the Store host")); } @PostMapping("/compat") diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/BatchScanIterator.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/BatchScanIterator.java index f7e02481fe..4d26c7d839 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/BatchScanIterator.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/BatchScanIterator.java @@ -41,6 +41,7 @@ public final class BatchScanIterator implements ScanIterator { private final AtomicBoolean closed = new AtomicBoolean(); private ScanIterator iterator; private boolean hasNext = false; + private Throwable cleanupFailure; private long curCount; private long curLimit; @@ -65,13 +66,14 @@ private ScanIterator getIterator() { do { buf = this.batchSupplier.get().getValue(); + this.iterator = buf; if (buf == null) { break; } if (!buf.hasNext()) { - buf.close(); + closeIterator(); buf = null; } @@ -99,13 +101,17 @@ private ScanIterator getIterator() { @Override public boolean hasNext() { + rethrowCleanupFailure(); + if (this.closed.get()) { + return false; + } if (this.iterator == null) { this.iterator = this.getIterator(); } else if (!this.iterator.hasNext()) { - this.iterator.close(); + closeIterator(); this.iterator = this.getIterator(); } else if (this.curCount == this.curLimit) { - this.iterator.close(); + closeIterator(); this.iterator = this.getIterator(); } @@ -135,10 +141,31 @@ public T next() { @Override public void close() { if (!this.closed.getAndSet(true)) { - if (this.iterator != null) { - this.iterator.close(); + closeIterator(); + } + rethrowCleanupFailure(); + } + + private void closeIterator() { + ScanIterator current = this.iterator; + this.iterator = null; + if (current != null) { + try { + current.close(); + } catch (RuntimeException | Error failure) { + this.cleanupFailure = failure; } } + rethrowCleanupFailure(); + } + + private void rethrowCleanupFailure() { + if (this.cleanupFailure instanceof RuntimeException) { + throw (RuntimeException) this.cleanupFailure; + } + if (this.cleanupFailure != null) { + throw (Error) this.cleanupFailure; + } } @Override diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/FusingScanIterator.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/FusingScanIterator.java index 56a1cc8c58..afdcb2e59c 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/FusingScanIterator.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/FusingScanIterator.java @@ -33,6 +33,8 @@ final class FusingScanIterator implements ScanIterator { private long accumulator; private Supplier supplier; private ScanIterator iterator; + private Throwable cleanupFailure; + private boolean closed; private byte[] position = EMPTY_BYTES; private FusingScanIterator() { @@ -47,14 +49,13 @@ public static FusingScanIterator maxOf(long maxThreshold, } private ScanIterator getIterator() { - ScanIterator buf = this.supplier.get(); - if (buf == null) { - return null; + while ((this.iterator = this.supplier.get()) != null) { + if (this.iterator.hasNext()) { + return this.iterator; + } + closeCurrent(); } - if (!buf.hasNext()) { - buf = null; - } - return buf; + return null; } private void init() { @@ -65,7 +66,8 @@ private void init() { @Override public boolean hasNext() { - if (this.isThresholdExceeded()) { + rethrowCleanupFailure(); + if (this.closed || this.isThresholdExceeded()) { return false; } if (this.iterator == null) { @@ -104,16 +106,36 @@ public T next() { position = this.iterator.position(); this.accumulator++; if (!this.iterator.hasNext() || this.isThresholdExceeded()) { - this.iterator.close(); - this.iterator = null; + closeCurrent(); } return t; } @Override public void close() { - if (this.iterator != null) { - this.iterator.close(); + this.closed = true; + closeCurrent(); + } + + private void closeCurrent() { + ScanIterator current = this.iterator; + this.iterator = null; + if (current != null) { + try { + current.close(); + } catch (RuntimeException | Error failure) { + this.cleanupFailure = failure; + } + } + rethrowCleanupFailure(); + } + + private void rethrowCleanupFailure() { + if (this.cleanupFailure instanceof RuntimeException) { + throw (RuntimeException) this.cleanupFailure; + } + if (this.cleanupFailure != null) { + throw (Error) this.cleanupFailure; } } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/GRpcServerConfig.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/GRpcServerConfig.java index 27cb69a1de..1d22764aba 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/GRpcServerConfig.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/GRpcServerConfig.java @@ -38,9 +38,10 @@ public class GRpcServerConfig extends GRpcServerBuilderConfigurer { @Override public void configure(ServerBuilder serverBuilder) { AppConfig.ThreadPoolGrpc grpc = appConfig.getThreadPoolGrpc(); + // Terminal callbacks must still be queued when all dispatch threads are busy. serverBuilder.executor( HgExecutorUtil.createExecutor(EXECUTOR_NAME, grpc.getCore(), grpc.getMax(), - grpc.getQueue()) + Integer.MAX_VALUE) ); } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/GrpcShutdownBarrier.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/GrpcShutdownBarrier.java new file mode 100644 index 0000000000..a9a6a9f95a --- /dev/null +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/GrpcShutdownBarrier.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.hugegraph.store.node.grpc; + +import java.util.HashSet; +import java.util.Set; + +import org.lognet.springboot.grpc.GRpcGlobalInterceptor; +import org.springframework.stereotype.Component; + +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 lombok.extern.slf4j.Slf4j; + +/** Waits for terminal application callbacks, not just transport termination. */ +@Slf4j +@Component +@GRpcGlobalInterceptor +public class GrpcShutdownBarrier implements ServerInterceptor { + + private final Set calls = new HashSet<>(); + private boolean closing; + + public synchronized void stopAcceptingCalls() { + this.closing = true; + } + + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, + ServerCallHandler next) { + Object token = new Object(); + synchronized (this) { + if (this.closing) { + call.close(Status.UNAVAILABLE.withDescription("Store is stopping"), new Metadata()); + return new ServerCall.Listener() { }; + } + this.calls.add(token); + } + try { + return new ForwardingServerCallListener.SimpleForwardingServerCallListener( + next.startCall(call, headers)) { + @Override + public void onCancel() { + try { + super.onCancel(); + } finally { + finished(token); + } + } + + @Override + public void onComplete() { + try { + super.onComplete(); + } finally { + finished(token); + } + } + }; + } catch (RuntimeException | Error e) { + finished(token); + throw e; + } + } + + private synchronized void finished(Object token) { + this.calls.remove(token); + this.notifyAll(); + } + + public synchronized void awaitCallbacks() { + boolean interrupted = false; + try { + while (!this.calls.isEmpty()) { + try { + this.wait(5000); + if (!this.calls.isEmpty()) { + log.warn("Waiting for {} RPC callbacks before closing databases", this.calls.size()); + } + } catch (InterruptedException e) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/HgStoreNodeService.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/HgStoreNodeService.java index 16592882b2..93268cd64f 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/HgStoreNodeService.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/HgStoreNodeService.java @@ -35,6 +35,7 @@ import org.apache.hugegraph.store.node.AppConfig; import org.apache.hugegraph.store.options.HgStoreEngineOptions; import org.apache.hugegraph.store.options.RaftRocksdbOptions; +import org.apache.hugegraph.util.RocksDBRuntime; import org.apache.hugegraph.store.raft.RaftClosure; import org.apache.hugegraph.store.raft.RaftOperation; import org.apache.hugegraph.store.raft.RaftTaskHandler; @@ -84,6 +85,7 @@ public void init() { setDataPath(appConfig.getDataPath()); setRaftPath(appConfig.getRaftPath()); setPdAddress(appConfig.getPdServerAddress()); + setPdCluster(appConfig.getPdCluster()); setFakePD(appConfig.isFakePd()); setRocksdbConfig(appConfig.getRocksdbConfig()); setGrpcAddress(appConfig.getStoreServerAddress()); @@ -119,6 +121,7 @@ public void init() { setJobConfig(appConfig.getJobOptions()); }}; + RocksDBRuntime.verify(String.valueOf(options.getRocksdbConfig().getOrDefault("rocksdb.provider", "rocksdb"))); RaftRocksdbOptions.initRocksdbGlobalConfig(options.getRocksdbConfig()); options.getLabels().put("rest.port", Integer.toString(appConfig.getRestPort())); diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/HgStoreStreamImpl.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/HgStoreStreamImpl.java index 7d01fa3db4..24a7d3c4d2 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/HgStoreStreamImpl.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/HgStoreStreamImpl.java @@ -17,9 +17,19 @@ package org.apache.hugegraph.store.node.grpc; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Consumer; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.hugegraph.store.business.BusinessHandler; +import org.apache.hugegraph.store.grpc.Graphpb.ScanPartitionRequest; +import org.apache.hugegraph.store.grpc.Graphpb.ScanResponse; +import org.apache.hugegraph.store.node.grpc.scan.ScanResponseObserver; import org.apache.hugegraph.store.grpc.state.ScanState; import org.apache.hugegraph.store.grpc.stream.HgStoreStreamGrpc; import org.apache.hugegraph.store.grpc.stream.KvPageRes; @@ -31,6 +41,8 @@ import org.lognet.springboot.grpc.GRpcService; import org.springframework.beans.factory.annotation.Autowired; +import io.grpc.Context; +import io.grpc.Status; import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; @@ -47,6 +59,191 @@ public class HgStoreStreamImpl extends HgStoreStreamGrpc.HgStoreStreamImplBase { private AppConfig appConfig; private HgStoreWrapperEx wrapper; private ThreadPoolExecutor executor; + private boolean closing; + private final Map scans = new ConcurrentHashMap<>(); + + /** Close admission before cancelling the gRPC server. */ + public synchronized void stopAcceptingScans() { + this.closing = true; + } + + /** Cancel streams, then drain queued cleanup tasks. Never discard the queue. */ + public void shutdownScans() { + stopAcceptingScans(); + for (Runnable cancel : this.scans.values().toArray(new Runnable[0])) { + try { + cancel.run(); + } catch (RuntimeException | Error failure) { + log.warn("Failed to cancel scan; continuing other cancellations", failure); + } + } + ThreadPoolExecutor current = getRealExecutor(); + if (current != null) { + current.shutdown(); + } + } + + private synchronized void checkAcceptingScans() { + if (this.closing) { + throw Status.UNAVAILABLE.withDescription("Store scans are stopping") + .asRuntimeException(); + } + } + + /** Wait independently of executor termination: failed native release is sticky. */ + public void awaitScanCleanup() { + boolean interrupted = false; + long nextLog = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + synchronized (this.scans) { + while (!this.scans.isEmpty()) { + try { + this.scans.wait(5000L); + } catch (InterruptedException e) { + interrupted = true; + } + if (!this.scans.isEmpty() && System.nanoTime() - nextLog >= 0) { + log.warn("Still waiting for {} scans to clean up before closing databases", + this.scans.size()); + for (ScanLifecycle scan : this.scans.keySet()) { + Throwable failure = scan.cleanupFailure(); + if (failure != null) { + log.warn("Scan cleanup failed; database close stays blocked", failure); + } + } + nextLog = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + } + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + + private StreamObserver register( + ScanLifecycle lifecycle, Function> factory) { + Context context = Context.current(); + StreamObserver observer = factory.apply(lifecycle); + Runnable cancel = () -> { + if (lifecycle.tryEnterCancellation()) { + try { + observer.onError(Status.CANCELLED.asRuntimeException()); + } finally { + lifecycle.leave(); + } + } + }; + this.scans.put(lifecycle, cancel); + Context.CancellationListener listener = ignored -> cancel.run(); + lifecycle.onFinished(() -> { + this.scans.remove(lifecycle); + context.removeListener(listener); + synchronized (this.scans) { + this.scans.notifyAll(); + } + }); + context.addListener(listener, Runnable::run); + return new StreamObserver() { + @Override + public void onNext(T value) { + if (lifecycle.tryEnter()) { + try { + observer.onNext(value); + } finally { + lifecycle.leave(); + } + } + } + + @Override + public void onError(Throwable failure) { + if (lifecycle.tryEnterCancellation()) { + try { + observer.onError(failure); + } finally { + lifecycle.leave(); + } + } + } + + @Override + public void onCompleted() { + if (lifecycle.tryEnter()) { + try { + observer.onCompleted(); + } finally { + lifecycle.leave(); + } + } + } + }; + } + + public StreamObserver scanGraphPartition( + StreamObserver response, + BusinessHandler handler) { + synchronized (this) { + checkAcceptingScans(); + return register(new ScanLifecycle(), lifecycle -> + new ScanResponseObserver<>( + lifecycle.response(response), handler, + task -> lifecycle.execute(getExecutor(), task), lifecycle::failedCleanup)); + } + } + + private void oneShot(StreamObserver response, Consumer action) { + ScanLifecycle lifecycle = new ScanLifecycle(); + Thread caller = Thread.currentThread(); + boolean alreadyInterrupted = caller.isInterrupted(); + AtomicBoolean interruptedByCancellation = new AtomicBoolean(); + Context context = Context.current(); + Runnable cancel = () -> { + synchronized (lifecycle) { + if (lifecycle.tryCancel()) { + // Wake interruptible reads; native reads retain ownership until they return. + if (!caller.isInterrupted()) { + interruptedByCancellation.set(true); + caller.interrupt(); + } + } + } + }; + Context.CancellationListener listener = ignored -> cancel.run(); + synchronized (this) { + checkAcceptingScans(); + lifecycle.enter(); + this.scans.put(lifecycle, cancel); + lifecycle.onFinished(() -> { + this.scans.remove(lifecycle); + synchronized (this.scans) { + this.scans.notifyAll(); + } + }); + } + try { + context.addListener(listener, Runnable::run); + action.accept(lifecycle); + } finally { + try { + // An iterator may close automatically during hasNext(), after which the + // scan exits without entering its ordinary response-completion path. + if (lifecycle.isCancelled()) { + lifecycle.response(response).onError(Status.CANCELLED + .withDescription("Store scan cancelled before completion") + .asRuntimeException()); + } + } finally { + context.removeListener(listener); + // A cancellation snapshot must not interrupt this executor thread after reuse. + synchronized (lifecycle) { + lifecycle.finishWithoutResponse(); + lifecycle.leave(); + if (!alreadyInterrupted && interruptedByCancellation.get()) { + Thread.interrupted(); + } + } + } + } + } private HgStoreWrapperEx getWrapper() { if (this.wrapper == null) { @@ -60,20 +257,16 @@ private HgStoreWrapperEx getWrapper() { return this.wrapper; } - public ThreadPoolExecutor getRealExecutor() { - return executor; + public synchronized ThreadPoolExecutor getRealExecutor() { + return this.executor; } - public ThreadPoolExecutor getExecutor() { + public synchronized ThreadPoolExecutor getExecutor() { + checkAcceptingScans(); if (this.executor == null) { - synchronized (this) { - if (this.executor == null) { - AppConfig.ThreadPoolScan scan = this.appConfig.getThreadPoolScan(); - this.executor = - HgExecutorUtil.createExecutor("hg-scan", scan.getCore(), scan.getMax(), - scan.getQueue()); - } - } + AppConfig.ThreadPoolScan scan = this.appConfig.getThreadPoolScan(); + this.executor = HgExecutorUtil.createExecutor("hg-scan", scan.getCore(), + scan.getMax(), scan.getQueue()); } return this.executor; } @@ -95,27 +288,39 @@ public ScanState getState() { } @Override - public StreamObserver scan(StreamObserver response) { - return ScanStreamResponse.of(response, getWrapper(), getExecutor(), appConfig); + public synchronized StreamObserver scan(StreamObserver response) { + checkAcceptingScans(); + return register(new ScanLifecycle(), lifecycle -> + new ScanStreamResponse(response, getWrapper(), getExecutor(), appConfig, lifecycle)); } @Override public void scanOneShot(ScanStreamReq request, StreamObserver response) { - ScanOneShotResponse.scanOneShot(request, response, getWrapper()); + oneShot(response, lifecycle -> ScanOneShotResponse.scanOneShot(request, response, getWrapper(), lifecycle)); } @Override - public StreamObserver scanBatch(StreamObserver response) { - return ScanBatchResponse3.of(response, getWrapper(), getExecutor()); + public synchronized StreamObserver scanBatch(StreamObserver response) { + checkAcceptingScans(); + return register(new ScanLifecycle(), lifecycle -> + ScanBatchResponse3.of(response, getWrapper(), getExecutor(), lifecycle)); } @Override public StreamObserver scanBatch2(StreamObserver response) { - return ScanBatchResponseFactory.of(response, getWrapper(), getExecutor()); + StreamObserver observer; + synchronized (this) { + checkAcceptingScans(); + observer = register(new ScanLifecycle(), lifecycle -> + ScanBatchResponseFactory.of(response, getWrapper(), getExecutor(), lifecycle)); + } + ScanBatchResponseFactory.getInstance().checkStreamActive(); + return observer; } @Override public void scanBatchOneShot(ScanStreamBatchReq request, StreamObserver response) { - ScanBatchOneShotResponse.scanOneShot(request, response, getWrapper()); + oneShot(response, lifecycle -> + ScanBatchOneShotResponse.scanOneShot(request, response, getWrapper(), lifecycle)); } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/HgStoreWrapperEx.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/HgStoreWrapperEx.java index eae9534e89..54afcfda07 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/HgStoreWrapperEx.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/HgStoreWrapperEx.java @@ -52,7 +52,7 @@ public boolean doClean(String graph, int partId) { public ScanIterator scanAll(String graph, String table, byte[] query) { ScanIterator scanIterator = this.handler.scanAll(graph, table, query); - return FilterIterator.of(scanIterator, query); + return FilterIterator.of(scanIterator, query, graph); } public ScanIterator scan(String graph, int partId, String table, byte[] start, byte[] end, @@ -60,14 +60,14 @@ public ScanIterator scan(String graph, int partId, String table, byte[] start, b byte[] query) { ScanIterator scanIterator = this.handler.scan(graph, partId, table, start, end, scanType, query); - return FilterIterator.of(scanIterator, query); + return FilterIterator.of(scanIterator, query, graph); } public ScanIterator scanOrdered(String graph, String table, byte[] start, byte[] end, int scanType, byte[] query) { ScanIterator scanIterator = this.handler.scanOrdered(graph, table, start, end, scanType); - return FilterIterator.of(scanIterator, query); + return FilterIterator.of(scanIterator, query, graph); } public void batchGet(String graph, String table, Supplier> s, @@ -82,7 +82,7 @@ public ScanIterator scanPrefix(String graph, int partition, String table, byte[] byte[] query) { ScanIterator scanIterator = this.handler.scanPrefix(graph, partition, table, prefix, scanType); - return FilterIterator.of(scanIterator, query); + return FilterIterator.of(scanIterator, query, graph); } public void doBatch(String graph, int partId, List entryList) { diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ParallelScanIterator.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ParallelScanIterator.java index 56ce1f45f3..65823da8ab 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ParallelScanIterator.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ParallelScanIterator.java @@ -27,6 +27,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; +import java.util.function.Consumer; import org.apache.hugegraph.pd.common.KVPair; import org.apache.hugegraph.rocksdb.access.RocksDBSession; @@ -67,11 +68,33 @@ public class ParallelScanIterator implements ScanIterator { private int maxInQueue = maxWorkThreads * 2; private volatile boolean finished; private List current = null; + private final Consumer cleanupReporter; + private volatile Throwable scanFailure; + private volatile Throwable cleanupFailure; + + private static void rethrow(Throwable failure) { + if (failure instanceof RuntimeException) { + throw (RuntimeException) failure; + } + if (failure != null) { + throw (Error) failure; + } + } + + private synchronized void failedCleanup(Throwable failure) { + if (this.cleanupFailure == null) { + this.cleanupFailure = failure; + } + this.cleanupReporter.accept(failure); + this.scanFailure = this.cleanupFailure; + requestStop(); + } private ParallelScanIterator(Supplier> iteratorSupplier, Supplier limitSupplier, ScanQueryRequest query, - ThreadPoolExecutor executor) { + ThreadPoolExecutor executor, Consumer cleanupReporter) { + this.cleanupReporter = cleanupReporter; this.executor = executor; this.batchSupplier = iteratorSupplier; this.limitSupplier = limitSupplier; @@ -88,7 +111,12 @@ private ParallelScanIterator(Supplier> iter this.maxInQueue = maxWorkThreads * 2; // Edge sorted requires a larger queue queue = new LinkedBlockingQueue<>(maxInQueue * 2); - createScanner(); + try { + createScanner(); + } catch (RuntimeException e) { + close(); + throw e; + } } public static ParallelScanIterator of( @@ -98,7 +126,13 @@ public static ParallelScanIterator of( ThreadPoolExecutor executor) { HgAssert.isArgumentNotNull(iteratorSupplier, "iteratorSupplier"); HgAssert.isArgumentNotNull(limitSupplier, "limitSupplier"); - return new ParallelScanIterator(iteratorSupplier, limitSupplier, query, executor); + return of(iteratorSupplier, limitSupplier, query, executor, ignored -> { }); + } + + static ParallelScanIterator of(Supplier> iteratorSupplier, + Supplier limitSupplier, ScanQueryRequest query, + ThreadPoolExecutor executor, Consumer cleanupReporter) { + return new ParallelScanIterator(iteratorSupplier, limitSupplier, query, executor, cleanupReporter); } @Override @@ -126,6 +160,7 @@ public boolean hasNext() { log.error("Wait data timeout!!!, scanner is {}/{}", scanners.size(), pauseScanners.size()); } + rethrow(this.scanFailure); return current != null && current != NO_DATA; } @@ -144,20 +179,30 @@ public List next() { return t; } + void requestStop() { + this.finished = true; + } + @Override public void close() { - finished = true; + requestStop(); + List pending; synchronized (scanners) { - scanners.forEach(scanner -> { - scanner.close(); - }); + pending = new ArrayList<>(scanners); + } + // Stop active scanner reads before close() waits for their iterator locks. + for (KVScanner scanner : pending) { + scanner.interruptReader(); + } + // Never hold a registry lock while waiting for a scanner's iterator lock. + for (KVScanner scanner : pending) { + scanner.close(); } synchronized (pauseScanners) { - pauseScanners.forEach(s -> { - s.close(); - }); + pauseScanners.clear(); } queue.clear(); + rethrow(this.cleanupFailure); } /** @@ -179,10 +224,15 @@ private void createScanner() { */ private void wakeUpScanner() { synchronized (pauseScanners) { - if (!pauseScanners.isEmpty()) { + if (!finished && !pauseScanners.isEmpty()) { KVScanner scanner = pauseScanners.poll(); if (scanner != null) { - executor.execute(() -> scanner.scanKV()); + try { + executor.execute(() -> scanner.scanKV()); + } catch (java.util.concurrent.RejectedExecutionException e) { + scanner.close(); + throw e; + } } } } @@ -195,7 +245,11 @@ private void wakeUpScanner() { */ private void suspendScanner(KVScanner scanner) { synchronized (pauseScanners) { - pauseScanners.add(scanner); + if (!finished) { + pauseScanners.add(scanner); + } else { + scanner.close(); + } } } @@ -218,30 +272,43 @@ private void quitScanner(KVScanner scanner) { */ private boolean putData(List data) { try { - this.queue.put(data); + while (!finished && !this.queue.offer(data, 100, TimeUnit.MILLISECONDS)) { + // Cancellation must release a producer whose client stopped consuming. + } } catch (InterruptedException e) { log.error("exception ", e); this.finished = true; return false; } - return this.queue.size() < maxInQueue; + return !finished && this.queue.size() < maxInQueue; } private boolean putData(List data, boolean hasNext) { + boolean locked = false; try { - queueLock.lock(); - this.queue.put(data); + while (!finished && !(locked = queueLock.tryLock(100, TimeUnit.MILLISECONDS))) { + // An ordered producer may be waiting behind a cancelled scanner. + } + if (!locked) { + return false; + } + while (!finished && !this.queue.offer(data, 100, TimeUnit.MILLISECONDS)) { + // Recheck cancellation while the output queue is full. + } } catch (InterruptedException e) { - log.error("exception ", e); + Thread.currentThread().interrupt(); this.finished = true; return false; } finally { - if (!hasNext) { + if (locked && finished) { + while (queueLock.isHeldByCurrentThread()) { + queueLock.unlock(); + } + } else if (locked && !hasNext) { queueLock.unlock(); } } - // Data not ended, thread continues to execute - return hasNext || this.queue.size() < maxInQueue; + return !finished && (hasNext || this.queue.size() < maxInQueue); } private synchronized KVPair getIterator() { @@ -300,18 +367,45 @@ public int size() { class KVScanner { private final ReentrantLock iteratorLock = new ReentrantLock(); + private final Object readerLock = new Object(); + private Thread reader; private ScanIterator iterator = null; private QueryCondition query = null; private long limit; private long counter; private volatile boolean closed = false; + private void interruptReader() { + synchronized (this.readerLock) { + if (this.reader != null && this.reader != Thread.currentThread()) { + this.reader.interrupt(); + } + } + } + + private boolean startReader() { + synchronized (this.readerLock) { + if (this.reader != null) { + return false; + } + this.reader = Thread.currentThread(); + return true; + } + } + + private void finishReader() { + synchronized (this.readerLock) { + if (this.reader == Thread.currentThread()) { + this.reader = null; + } + } + } + private ScanIterator getIterator() { // Iterator has no data, or the point has reached the limit, switch to a new iterator. if (iterator == null || !iterator.hasNext() || counter >= limit) { - if (iterator != null) { - iterator.close(); - } + closeIterator(); + rethrow(cleanupFailure); KVPair pair = ParallelScanIterator.this.getIterator(); query = pair.getKey(); iterator = pair.getValue(); @@ -325,15 +419,18 @@ public void scanKV() { boolean canNext = true; ArrayList dataList = new ArrayList<>(batchSize); dataList.ensureCapacity(batchSize); + if (!startReader()) { + return; + } iteratorLock.lock(); try { long entriesSize = 0, bodySize = 0; - while (canNext && !closed) { + while (canNext && !closed && !finished) { iterator = this.getIterator(); if (iterator == null) { break; } - while (iterator.hasNext() && entriesSize < batchSize && + while (!closed && !finished && iterator.hasNext() && entriesSize < batchSize && bodySize < maxBodySize && counter < limit && !closed) { KV kv = KV.of(iterator.next()); @@ -363,11 +460,20 @@ public void scanKV() { putData(dataList); } } - } catch (Exception e) { - log.error("exception {}", e); + } catch (RuntimeException | Error e) { + scanFailure = e; + requestStop(); } finally { + // putData(..., true) intentionally keeps this lock across batches to + // serialize ordered output. The scanner must always release it before + // suspending or quitting, including when the next iterator is empty. + while (queueLock.isHeldByCurrentThread()) { + queueLock.unlock(); + } iteratorLock.unlock(); - if (iterator != null && counter < limit && !closed) { + // Clear ownership before a paused scanner can be scheduled again. + finishReader(); + if (iterator != null && counter < limit && !closed && !finished) { suspendScanner(this); } else { quitScanner(this); @@ -379,12 +485,22 @@ public void close() { closed = true; iteratorLock.lock(); try { - if (iterator != null) { - iterator.close(); - } + closeIterator(); } finally { iteratorLock.unlock(); } } + + private void closeIterator() { + ScanIterator current = this.iterator; + this.iterator = null; + if (current != null) { + try { + current.close(); + } catch (RuntimeException | Error failure) { + failedCleanup(failure); + } + } + } } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchOneShotResponse.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchOneShotResponse.java index 1234a5b3de..bedb7eaf86 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchOneShotResponse.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchOneShotResponse.java @@ -30,6 +30,7 @@ import com.google.protobuf.ByteString; +import io.grpc.Context; import io.grpc.Status; import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; @@ -51,10 +52,19 @@ public class ScanBatchOneShotResponse { public static void scanOneShot(ScanStreamBatchReq request, StreamObserver responseObserver, HgStoreWrapperEx wrapper) { + scanOneShot(request, responseObserver, wrapper, new ScanLifecycle()); + } + + static void scanOneShot(ScanStreamBatchReq request, StreamObserver output, + HgStoreWrapperEx wrapper, ScanLifecycle lifecycle) { + StreamObserver responseObserver = lifecycle.response(output); String graph = request.getHeader().getGraph(); ScanQueryRequest queryRequest = request.getQueryRequest(); - ScanIterator iterator = getIterator(graph, queryRequest, wrapper); + if ((Context.current().isCancelled() || lifecycle.isCancelled())) { + return; + } + ScanIterator iterator = getIterator(graph, queryRequest, wrapper, lifecycle::failedCleanup); KvPageRes.Builder resBuilder = KvPageRes.newBuilder(); Kv.Builder kvBuilder = Kv.newBuilder(); @@ -68,9 +78,11 @@ public static void scanOneShot(ScanStreamBatchReq request, } int count = 0; + boolean cleanupAttempted = false; try { - while (iterator.hasNext()) { + while (!(Context.current().isCancelled() || lifecycle.isCancelled()) && + !Thread.currentThread().isInterrupted() && iterator.hasNext()) { if (++count > limit) { break; @@ -86,6 +98,19 @@ public static void scanOneShot(ScanStreamBatchReq request, } + if ((Context.current().isCancelled() || lifecycle.isCancelled())) { + return; + } + if (Thread.currentThread().isInterrupted()) { + responseObserver.onError(HgGrpc.toErr(Status.Code.CANCELLED, "Scanning interrupted")); + return; + } + cleanupAttempted = true; + if (!lifecycle.close(iterator)) { + responseObserver.onError(Status.INTERNAL.withDescription("Failed to close scan iterator") + .withCause(lifecycle.cleanupFailure()).asRuntimeException()); + return; + } responseObserver.onNext(resBuilder.build()); responseObserver.onCompleted(); @@ -94,7 +119,10 @@ public static void scanOneShot(ScanStreamBatchReq request, responseObserver.onError( HgGrpc.toErr(Status.Code.INTERNAL, msg, t)); } finally { - iterator.close(); + if (!cleanupAttempted && !lifecycle.close(iterator)) { + responseObserver.onError(Status.INTERNAL.withDescription("Failed to close scan iterator") + .withCause(lifecycle.cleanupFailure()).asRuntimeException()); + } } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchResponse.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchResponse.java index f4485e1527..25606b5b02 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchResponse.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchResponse.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -33,6 +34,7 @@ import org.apache.hugegraph.store.node.util.HgGrpc; import org.apache.hugegraph.store.node.util.PropertyUtil; +import io.grpc.Status; import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; @@ -59,7 +61,7 @@ public class ScanBatchResponse implements StreamObserver { private final Object stateLock = new Object(); private final Lock iteratorLock = new ReentrantLock(); // Currently traversing iterator - private ScanIterator iterator; + private volatile ScanIterator iterator; // Next send sequence number private volatile int seqNo; // Client consumed sequence number @@ -72,10 +74,21 @@ public class ScanBatchResponse implements StreamObserver { // Last read data time private long activeTime; private volatile State state; + private final AtomicBoolean cancelled = new AtomicBoolean(); + private final ScanLifecycle lifecycle; + private boolean iteratorClosed; + private volatile boolean inputCompleted; + private boolean queryReceived; public ScanBatchResponse(StreamObserver response, HgStoreWrapperEx wrapper, ThreadPoolExecutor executor) { - this.sender = response; + this(response, wrapper, executor, new ScanLifecycle()); + } + + ScanBatchResponse(StreamObserver response, HgStoreWrapperEx wrapper, + ThreadPoolExecutor executor, ScanLifecycle lifecycle) { + this.lifecycle = lifecycle; + this.sender = lifecycle.response(response); this.wrapper = wrapper; this.executor = executor; this.iterator = null; @@ -92,45 +105,70 @@ public ScanBatchResponse(StreamObserver response, HgStoreWrapperEx wra */ @Override public void onNext(ScanStreamBatchReq request) { + if (this.cancelled.get()) { + return; + } switch (request.getQueryCase()) { case QUERY_REQUEST: // query conditions - executor.execute(() -> { + synchronized (this.stateLock) { + this.queryReceived = true; + } + submit(() -> { startQuery(request.getHeader().getGraph(), request.getQueryRequest()); }); break; case RECEIPT_REQUEST: // Message asynchronous response this.clientSeqNo = request.getReceiptRequest().getTimes(); if (seqNo - clientSeqNo < maxInFlightCount) { + boolean send = false; + boolean done; synchronized (stateLock) { + done = state == State.DONE; if (state == State.IDLE) { state = State.DOING; - executor.execute(() -> { - sendEntries(); - }); - } else if (state == State.DONE) { - sendNoDataEntries(); + send = true; } } + if (send) { + submit(this::sendEntries); + } else if (done) { + sendNoDataEntries(); + } } break; case CANCEL_REQUEST: // close stream - closeQuery(); + this.lifecycle.tryCancel(); + closeQuery(Status.CANCELLED.withDescription("Batch scan cancelled").asRuntimeException()); break; default: - sender.onError( - HgGrpc.toErr("Unsupported sub-request: [ " + request + " ]")); + closeQuery(HgGrpc.toErr("Unsupported sub-request: [ " + request + " ]")); } } @Override public void onError(Throwable t) { log.error("onError ", t); - closeQuery(); + this.lifecycle.tryCancel(); + closeQuery(t); } @Override public void onCompleted() { - closeQuery(); + boolean finish; + boolean resume = false; + synchronized (this.stateLock) { + this.inputCompleted = true; + finish = !this.queryReceived || this.state == State.DONE; + if (!finish && this.state == State.IDLE && this.iterator != null) { + this.state = State.DOING; + resume = true; + } + } + if (finish) { + closeQuery(); + } else if (resume) { + submit(this::sendEntries); + } } /** @@ -138,18 +176,43 @@ public void onCompleted() { * * @param request */ + private void submit(Runnable task) { + if (this.cancelled.get()) { + return; + } + try { + this.lifecycle.execute(this.executor, task); + } catch (io.grpc.StatusRuntimeException e) { + closeQuery(e); + } + } + private void startQuery(String graphName, ScanQueryRequest request) { - this.query = request; - this.limit = request.getLimit(); - this.count = 0; - this.iterator = getParallelIterator(graphName, request, this.wrapper, executor); - synchronized (stateLock) { - if (state == State.IDLE) { + this.iteratorLock.lock(); + try { + if (this.cancelled.get() || this.query != null) { + return; + } + this.query = request; + this.limit = request.getLimit(); + this.count = 0; + this.iterator = getParallelIterator(graphName, request, this.wrapper, executor, this.lifecycle); + if (this.cancelled.get()) { + closeIter(); + return; + } + synchronized (stateLock) { + if (state != State.IDLE) { + return; + } state = State.DOING; - executor.execute(() -> { - sendEntries(); - }); } + submit(this::sendEntries); + } catch (RuntimeException e) { + closeQuery(e); + log.warn("Failed to start scan", e); + } finally { + this.iteratorLock.unlock(); } } @@ -157,25 +220,51 @@ private void startQuery(String graphName, ScanQueryRequest request) { * Generate iterator */ private void closeQuery() { + closeQuery(null); + } + + private void closeQuery(Throwable error) { + if (error instanceof java.util.concurrent.RejectedExecutionException) { + error = ScanLifecycle.rejected(this.executor, error); + } + if (!this.cancelled.compareAndSet(false, true)) { + return; + } setStateDone(); + ScanIterator current = this.iterator; + if (current instanceof ParallelScanIterator) { + ((ParallelScanIterator) current).requestStop(); + } try { closeIter(); - this.sender.onCompleted(); - } catch (Exception e) { - log.error("exception ", e); + if (this.lifecycle.cleanupFailure() != null) { + error = Status.INTERNAL.withDescription("Failed to close batch scan iterator") + .withCause(this.lifecycle.cleanupFailure()).asRuntimeException(); + } + if (error == null) { + this.sender.onCompleted(); + } else { + this.sender.onError(error); + } + } catch (RuntimeException | Error e) { + log.error("Failed to terminate batch scan response", e); + } finally { + int active = ScanBatchResponseFactory.getInstance().removeStreamObserver(this); + log.info("ScanBatchResponse closeQuery, active count is {}", active); } - int active = ScanBatchResponseFactory.getInstance().removeStreamObserver(this); - log.info("ScanBatchResponse closeQuery, active count is {}", active); } private void closeIter() { + this.iteratorLock.lock(); try { - if (this.iterator != null) { - this.iterator.close(); - this.iterator = null; + if (this.iterator != null && !this.iteratorClosed) { + this.iteratorClosed = true; + if (this.lifecycle.close(this.iterator)) { + this.iterator = null; + } } - } catch (Exception e) { - + } finally { + this.iteratorLock.unlock(); } } @@ -209,24 +298,38 @@ private void sendEntries() { this.sender.onNext(dataBuilder.build()); this.activeTime = System.currentTimeMillis(); } - if (!iterator.hasNext() || this.count >= limit || state == State.DONE) { + if (this.cancelled.get()) { + return; + } + if (this.count >= limit || !iterator.hasNext()) { closeIter(); + if (this.lifecycle.cleanupFailure() != null) { + closeQuery(this.lifecycle.cleanupFailure()); + return; + } this.sender.onNext(KvStream.newBuilder().setOver(true).build()); setStateDone(); + if (this.inputCompleted) { + closeQuery(); + } } else { - setStateIdle(); + boolean halfClosed; + synchronized (this.stateLock) { + halfClosed = this.inputCompleted; + if (!halfClosed && this.state != State.DONE) { + this.state = State.IDLE; + } + } + if (halfClosed) { + closeQuery(Status.FAILED_PRECONDITION + .withDescription("Batch scan half-closed before sufficient receipts") + .asRuntimeException()); + } } } catch (Throwable e) { if (this.state != State.DONE) { log.error(" send data exception: ", e); - setStateIdle(); - if (this.sender != null) { - try { - this.sender.onError(e); - } catch (Exception ex) { - log.warn("Error when call sender.onError {}", e.getMessage()); - } - } + closeQuery(e); } } finally { iteratorLock.unlock(); @@ -234,9 +337,13 @@ private void sendEntries() { } private void sendNoDataEntries() { + this.iteratorLock.lock(); try { - this.sender.onNext(KvStream.newBuilder().setOver(true).build()); - } catch (Exception e) { + if (!this.cancelled.get()) { + this.sender.onNext(KvStream.newBuilder().setOver(true).build()); + } + } finally { + this.iteratorLock.unlock(); } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchResponse3.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchResponse3.java index d0c52b372e..69304dbf60 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchResponse3.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchResponse3.java @@ -61,7 +61,13 @@ public static StreamObserver of(StreamObserver responseObserver, HgStoreWrapperEx wrapper, ThreadPoolExecutor executor) { HgAssert.isArgumentNotNull(responseObserver, "responseObserver"); HgAssert.isArgumentNotNull(wrapper, "wrapper"); - return new Broker(responseObserver, wrapper, executor); + return of(responseObserver, wrapper, executor, new ScanLifecycle()); + } + + static StreamObserver of(StreamObserver responseObserver, + HgStoreWrapperEx wrapper, ThreadPoolExecutor executor, + ScanLifecycle lifecycle) { + return new Broker(lifecycle.response(responseObserver), wrapper, executor, lifecycle); } private enum OrderState { @@ -81,10 +87,12 @@ private static class Broker implements StreamObserver { private final HgStoreWrapperEx wrapper; private final ThreadPoolExecutor executor; private final OrderManager manager = new OrderManager(); + private final ScanLifecycle lifecycle; private String graph; Broker(StreamObserver responseObserver, HgStoreWrapperEx wrapper, - ThreadPoolExecutor executor) { + ThreadPoolExecutor executor, ScanLifecycle lifecycle) { + this.lifecycle = lifecycle; this.responseObserver = responseObserver; this.wrapper = wrapper; this.executor = executor; @@ -101,23 +109,27 @@ public void onNext(ScanStreamBatchReq request) { this.manager.receipt(request.getReceiptRequest().getTimes()); break; case CANCEL_REQUEST: - this.manager.finished(); + this.lifecycle.tryCancel(); + this.onError(Status.CANCELLED.withDescription("Batch scan cancelled").asRuntimeException()); break; default: - responseObserver.onError( - HgGrpc.toErr("Unsupported sub-request: [ " + request + " ]")); + this.onError(HgGrpc.toErr("Unsupported sub-request: [ " + request + " ]")); } } @Override public void onError(Throwable t) { log.warn(t.getMessage()); + this.lifecycle.tryCancel(); this.manager.breakdown(); + this.responseObserver.onError(t); } @Override public void onCompleted() { - this.manager.finished(); + if (this.manager.inputCompleted()) { + this.responseObserver.onCompleted(); + } } private void handleHeader(ScanStreamBatchReq request) { @@ -145,9 +157,9 @@ private void makeADeal(ScanQueryRequest request) { OrderWorker worker = new OrderWorker( request.getLimit(), request.getPageSize(), - ScanUtil.getIterator(this.graph, request, this.wrapper), + ScanUtil.getIterator(this.graph, request, this.wrapper, this.lifecycle::failedCleanup), deliverer, - this.executor); + this.executor, this.lifecycle); this.manager.deal(worker, deliverer); } @@ -160,36 +172,64 @@ private static class OrderManager { OrderState state = OrderState.NEW; OrderWorker worker; OrderDeliverer deliverer; + boolean cancelled; - synchronized void deal(OrderWorker worker, OrderDeliverer deliverer) { + void deal(OrderWorker worker, OrderDeliverer deliverer) { if (log.isDebugEnabled()) { log.debug("Receiving query request."); } - if (this.state == OrderState.NEW) { - this.worker = worker; - this.deliverer = deliverer; - this.worker.hereWeGo(); - this.state = OrderState.WORKING; + boolean accepted; + synchronized (this) { + accepted = !this.cancelled && this.state == OrderState.NEW; + if (accepted) { + this.worker = worker; + this.deliverer = deliverer; + this.state = OrderState.WORKING; + } + } + if (accepted) { + worker.hereWeGo(); + } else { + worker.closeIterator(); } } - synchronized void receipt(int receiptTimes) { + void receipt(int receiptTimes) { if (log.isDebugEnabled()) { log.debug("Receiving receipt request."); } - this.worker.setReceipt(receiptTimes); + OrderWorker current; + synchronized (this) { + current = this.worker; + } + if (current != null) { + current.setReceipt(receiptTimes); + } } - synchronized void finished() { - if (log.isDebugEnabled()) { - log.debug("Receiving finished request."); + boolean inputCompleted() { + OrderWorker current; + synchronized (this) { + if (this.cancelled) { + return false; + } + current = this.worker; + if (current == null) { + return true; + } } - this.breakdown(); + current.inputCompleted(); + return false; } - synchronized void breakdown() { - if (this.worker != null) { - this.worker.breakdown(); + void breakdown() { + OrderWorker current; + synchronized (this) { + this.cancelled = true; + current = this.worker; + } + if (current != null) { + current.breakdown(); } } } @@ -240,9 +280,12 @@ void error(String msg) { } void error(String msg, Throwable t) { + this.error(Status.INTERNAL, msg, t); + } + + void error(Status status, String msg, Throwable t) { if (!finishFlag.getAndSet(true)) { - this.responseObserver.onError(HgGrpc.toErr(Status.INTERNAL, - msg, t)); + this.responseObserver.onError(HgGrpc.toErr(status, msg, t)); } } } @@ -261,11 +304,17 @@ private static class OrderWorker { private final AtomicInteger curTimes = new AtomicInteger(); private final ThreadPoolExecutor executor; private final long limit; + private final ScanLifecycle lifecycle; + private final AtomicBoolean iteratorClosed = new AtomicBoolean(); + private final Object readerLock = new Object(); + private Thread reader; private long packageSize; private long counter; + private volatile boolean inputCompleted; OrderWorker(long limit, long packageSize, ScanIterator iterator, OrderDeliverer deliverer, - ThreadPoolExecutor executor) { + ThreadPoolExecutor executor, ScanLifecycle lifecycle) { + this.lifecycle = lifecycle; this.limit = limit; this.packageSize = packageSize; this.iterator = iterator; @@ -297,7 +346,13 @@ void hereWeGo() { return; } - executor.execute(() -> working()); + try { + this.lifecycle.execute(executor, this::working); + } catch (io.grpc.StatusRuntimeException e) { + this.completeFlag.set(true); + closeIterator(); + this.deliverer.error(e.getStatus(), "Store scan task rejected", e); + } Thread.yield(); } @@ -318,18 +373,51 @@ void continueWorking() { } } + void inputCompleted() { + this.inputCompleted = true; + synchronized (this.iterator) { + this.iterator.notify(); + } + } + void breakdown() { this.breakdown.set(true); + // Interrupt before waiting for the iterator monitor held during hasNext(). + synchronized (this.readerLock) { + if (this.reader != null && this.reader != Thread.currentThread()) { + this.reader.interrupt(); + } + } synchronized (this.iterator) { this.iterator.notify(); } } + private void closeIterator() { + if (this.iteratorClosed.compareAndSet(false, true) && + !this.lifecycle.close(this.iterator)) { + this.deliverer.error("Failed to close scan iterator", this.lifecycle.cleanupFailure()); + } + } + private void working() { if (this.isWorking.getAndSet(true)) { return; } + boolean startReading; + synchronized (this.readerLock) { + startReading = !this.breakdown.get() && !this.completeFlag.get(); + if (startReading) { + this.reader = Thread.currentThread(); + } + } + if (!startReading) { + this.completeFlag.set(true); + closeIterator(); + return; + } + this.workingLock.lock(); try { @@ -338,7 +426,8 @@ private void working() { Kv.Builder kvBuilder = Kv.newBuilder(); long packageCount = 0; - while (iterator.hasNext()) { + while (!this.breakdown.get() && !Thread.currentThread().isInterrupted() && + iterator.hasNext()) { if (++this.counter > limit) { this.completeFlag.set(true); break; @@ -353,25 +442,26 @@ private void working() { deliverer.deliver(dataBuilder, curTimes.incrementAndGet(), false); Thread.yield(); - if (!this.checkContinue()) { + if (!this.breakdown.get() && !this.checkContinue()) { long start = System.currentTimeMillis(); - iterator.wait( - HgStoreConst.SCAN_WAIT_CLIENT_TAKING_TIME_OUT_SECONDS * - 1000); - - if (System.currentTimeMillis() - start - >= - HgStoreConst.SCAN_WAIT_CLIENT_TAKING_TIME_OUT_SECONDS * 1000) { - throw new TimeoutException("Waiting continue more than " - + - HgStoreConst.SCAN_WAIT_CLIENT_TAKING_TIME_OUT_SECONDS + - " seconds."); + while (!this.breakdown.get() && !this.checkContinue()) { + if (this.inputCompleted) { + throw Status.FAILED_PRECONDITION.withDescription( + "Batch scan half-closed before sufficient receipts") + .asRuntimeException(); + } + long remaining = HgStoreConst.SCAN_WAIT_CLIENT_TAKING_TIME_OUT_SECONDS * + 1000L - (System.currentTimeMillis() - start); + if (remaining <= 0) { + throw new TimeoutException("Waiting continue more than " + + HgStoreConst.SCAN_WAIT_CLIENT_TAKING_TIME_OUT_SECONDS + + " seconds."); + } + iterator.wait(remaining); } - if (this.breakdown.get()) { break; } - } packageCount = 1; @@ -390,15 +480,24 @@ private void working() { } + if (!this.breakdown.get() && Thread.currentThread().isInterrupted()) { + throw new InterruptedException("Scanning interrupted"); + } this.completeFlag.set(true); - deliverer.deliver(dataBuilder, curTimes.incrementAndGet(), true); + closeIterator(); + if (!this.breakdown.get() && this.lifecycle.cleanupFailure() == null) { + deliverer.deliver(dataBuilder, curTimes.incrementAndGet(), true); + } } + } catch (io.grpc.StatusRuntimeException e) { + this.deliverer.error(e.getStatus(), e.getMessage(), e); } catch (InterruptedException e) { log.error("Interrupted waiting of iterator, canceled while.", e); - this.deliverer.error("Failed to finish scanning, cause by InterruptedException."); + this.deliverer.error(Status.CANCELLED, + "Failed to finish scanning, cause by InterruptedException.", e); } catch (TimeoutException t) { log.info(t.getMessage()); this.deliverer.error("Sever waiting exceeded [" @@ -408,8 +507,16 @@ private void working() { log.error("Failed to do while for scanning, cause by:", t); this.deliverer.error("Failed to finish scanning ", t); } finally { - this.workingLock.unlock(); - this.iterator.close(); + try { + this.workingLock.unlock(); + closeIterator(); + } finally { + synchronized (this.readerLock) { + if (this.reader == Thread.currentThread()) { + this.reader = null; + } + } + } } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchResponseFactory.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchResponseFactory.java index 43abfd97ce..dbb5406387 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchResponseFactory.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanBatchResponseFactory.java @@ -37,12 +37,19 @@ public static ScanBatchResponseFactory getInstance() { public static StreamObserver of(StreamObserver responseObserver, HgStoreWrapperEx wrapper, ThreadPoolExecutor executor) { - StreamObserver observer = new ScanBatchResponse(responseObserver, wrapper, executor); - getInstance().addStreamObserver(observer); + StreamObserver observer = of(responseObserver, wrapper, executor, new ScanLifecycle()); getInstance().checkStreamActive(); return observer; } + static StreamObserver of(StreamObserver responseObserver, + HgStoreWrapperEx wrapper, ThreadPoolExecutor executor, + ScanLifecycle lifecycle) { + StreamObserver observer = new ScanBatchResponse(responseObserver, wrapper, executor, lifecycle); + getInstance().addStreamObserver(observer); + return observer; + } + public int addStreamObserver(StreamObserver observer) { streamObservers.add(observer); return streamObservers.size(); diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanLifecycle.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanLifecycle.java new file mode 100644 index 0000000000..6cce65ad02 --- /dev/null +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanLifecycle.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.hugegraph.store.node.grpc; + +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hugegraph.rocksdb.access.ScanIterator; + +import io.grpc.Status; +import io.grpc.stub.StreamObserver; + +/** Per-call response and cleanup accounting; a failed release remains registered. */ +final class ScanLifecycle { + + // 0 = open, 1 = cancellation won, 2 = response terminated. + private final AtomicInteger terminal = new AtomicInteger(); + private final Object responseLock = new Object(); + private int activities; + private volatile Throwable cleanupFailure; + private Runnable finished = () -> { }; + + synchronized void onFinished(Runnable action) { + this.finished = action; + } + + synchronized void enter() { + this.activities++; + } + + synchronized boolean tryEnter() { + if (this.terminal.get() != 0) { + return false; + } + this.activities++; + return true; + } + + synchronized boolean tryEnterCancellation() { + if (!tryCancel()) { + return false; + } + this.activities++; + return true; + } + + synchronized void leave() { + this.activities--; + checkFinished(); + } + + synchronized void failedCleanup(Throwable failure) { + if (this.cleanupFailure == null) { + this.cleanupFailure = failure; + } + } + + Throwable cleanupFailure() { + return this.cleanupFailure; + } + + private synchronized void checkFinished() { + if (this.terminal.get() == 2 && this.activities == 0 && this.cleanupFailure == null) { + this.finished.run(); + } + } + + boolean tryCancel() { + return this.terminal.compareAndSet(0, 1); + } + + boolean isCancelled() { + return this.terminal.get() == 1; + } + + void finishWithoutResponse() { + this.terminal.set(2); + checkFinished(); + } + + boolean close(ScanIterator iterator) { + if (iterator == null) { + return true; + } + try { + iterator.close(); + return true; + } catch (RuntimeException | Error failure) { + failedCleanup(failure); + return false; + } + } + + void execute(ThreadPoolExecutor executor, Runnable task) { + enter(); + AtomicBoolean released = new AtomicBoolean(); + Runnable release = () -> { + if (released.compareAndSet(false, true)) { + leave(); + } + }; + try { + executor.execute(() -> { + try { + task.run(); + } finally { + release.run(); + } + }); + } catch (RuntimeException | Error failure) { + release.run(); + if (failure instanceof RejectedExecutionException) { + throw rejected(executor, failure); + } + throw failure; + } + } + + static RuntimeException rejected(ThreadPoolExecutor executor, Throwable failure) { + Status status = executor.isShutdown() ? Status.UNAVAILABLE : Status.RESOURCE_EXHAUSTED; + return status.withDescription("Store scan task rejected").withCause(failure) + .asRuntimeException(); + } + + StreamObserver response(StreamObserver delegate) { + return new StreamObserver() { + @Override + public void onNext(T value) { + enter(); + try { + synchronized (responseLock) { + if (terminal.get() == 0) { + delegate.onNext(value); + } + } + } finally { + leave(); + } + } + + @Override + public void onError(Throwable failure) { + enter(); + try { + synchronized (responseLock) { + if (terminal.getAndSet(2) != 2) { + delegate.onError(failure); + } + } + } finally { + leave(); + } + } + + @Override + public void onCompleted() { + enter(); + try { + synchronized (responseLock) { + int previous = terminal.getAndSet(2); + if (previous == 0) { + delegate.onCompleted(); + } else if (previous == 1) { + delegate.onError(Status.CANCELLED + .withDescription("Store scan cancelled before completion") + .asRuntimeException()); + } + } + } finally { + leave(); + } + } + }; + } +} diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanOneShotResponse.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanOneShotResponse.java index 5b77a4449a..e800cc1166 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanOneShotResponse.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanOneShotResponse.java @@ -27,6 +27,7 @@ import com.google.protobuf.ByteString; +import io.grpc.Context; import io.grpc.Status; import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; @@ -48,6 +49,12 @@ public class ScanOneShotResponse { public static void scanOneShot(ScanStreamReq request, StreamObserver responseObserver, HgStoreWrapperEx wrapper) { + scanOneShot(request, responseObserver, wrapper, new ScanLifecycle()); + } + + static void scanOneShot(ScanStreamReq request, StreamObserver output, + HgStoreWrapperEx wrapper, ScanLifecycle lifecycle) { + StreamObserver responseObserver = lifecycle.response(output); KvPageRes.Builder resBuilder = KvPageRes.newBuilder(); Kv.Builder kvBuilder = Kv.newBuilder(); @@ -57,12 +64,17 @@ public static void scanOneShot(ScanStreamReq request, responseObserver.onError(HgGrpc.toErr("limit<=0, please to invoke stream scan.")); return; } - ScanIterator iterator = ScanUtil.getIterator(request, wrapper); + if ((Context.current().isCancelled() || lifecycle.isCancelled())) { + return; + } + ScanIterator iterator = ScanUtil.getIterator(request, wrapper, lifecycle::failedCleanup); int count = 0; + boolean cleanupAttempted = false; try { - while (iterator.hasNext()) { + while (!(Context.current().isCancelled() || lifecycle.isCancelled()) && + !Thread.currentThread().isInterrupted() && iterator.hasNext()) { if (++count > limit) { break; @@ -78,6 +90,19 @@ public static void scanOneShot(ScanStreamReq request, } + if ((Context.current().isCancelled() || lifecycle.isCancelled())) { + return; + } + if (Thread.currentThread().isInterrupted()) { + responseObserver.onError(HgGrpc.toErr(Status.Code.CANCELLED, "Scanning interrupted")); + return; + } + cleanupAttempted = true; + if (!lifecycle.close(iterator)) { + responseObserver.onError(Status.INTERNAL.withDescription("Failed to close scan iterator") + .withCause(lifecycle.cleanupFailure()).asRuntimeException()); + return; + } responseObserver.onNext( resBuilder.setVersion(ScanUtil.responseVersion(request)) .build()); @@ -87,7 +112,10 @@ public static void scanOneShot(ScanStreamReq request, String msg = "an exception occurred during data scanning"; responseObserver.onError(HgGrpc.toErr(Status.INTERNAL, msg, t)); } finally { - iterator.close(); + if (!cleanupAttempted && !lifecycle.close(iterator)) { + responseObserver.onError(Status.INTERNAL.withDescription("Failed to close scan iterator") + .withCause(lifecycle.cleanupFailure()).asRuntimeException()); + } } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanStreamResponse.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanStreamResponse.java index e572862823..8628e0c92b 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanStreamResponse.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanStreamResponse.java @@ -61,6 +61,21 @@ public class ScanStreamResponse implements StreamObserver { private final int waitTime; private final HgChannel channel; private ScanIterator iterator; + private final Object cancellationLock = new Object(); + private final Object responseLock = new Object(); + private Thread worker; + private final ScanLifecycle lifecycle; + + private void cancel() { + this.isStop.set(true); + this.channel.close(); + Thread current = Thread.currentThread(); + synchronized (this.cancellationLock) { + if (this.worker != null && this.worker != current) { + this.worker.interrupt(); + } + } + } private long limit = 0; private int times = 0; private long pageSize = 0; @@ -72,7 +87,14 @@ public class ScanStreamResponse implements StreamObserver { ScanStreamResponse(StreamObserver responseObserver, HgStoreWrapperEx wrapper, ThreadPoolExecutor executor, AppConfig appConfig) { - this.responseObserver = responseObserver; + this(responseObserver, wrapper, executor, appConfig, new ScanLifecycle()); + } + + ScanStreamResponse(StreamObserver responseObserver, + HgStoreWrapperEx wrapper, ThreadPoolExecutor executor, + AppConfig appConfig, ScanLifecycle lifecycle) { + this.lifecycle = lifecycle; + this.responseObserver = lifecycle.response(responseObserver); this.wrapper = wrapper; this.executor = executor; this.config = appConfig; @@ -91,6 +113,9 @@ public static ScanStreamResponse of(StreamObserver responseObserver, @Override public void onNext(ScanStreamReq request) { + if (this.isStop.get()) { + return; + } try { this.responseVersion = Math.max( this.responseVersion, ScanUtil.responseVersion(request)); @@ -100,22 +125,20 @@ public void onNext(ScanStreamReq request) { next(request); } } catch (Exception e) { - responseObserver.onError(e); + this.failServer(e); } } @Override public void onError(Throwable t) { - this.isStop.set(true); - this.finishServer(); + this.cancelServer(false); log.warn("onError from client [ graph: {} , table: {}]; Reason: {}]", graph, table, t.getMessage()); } @Override public void onCompleted() { - this.isStop.set(true); - this.finishServer(); + this.cancelServer(false); } private void initIterator(ScanStreamReq request) { @@ -123,7 +146,6 @@ private void initIterator(ScanStreamReq request) { if (this.isStarted.getAndSet(true)) { return; } - this.iterator = getIterator(request, this.wrapper); this.graph = request.getHeader().getGraph(); this.table = request.getTable(); this.limit = request.getLimit(); @@ -143,7 +165,15 @@ private void initIterator(ScanStreamReq request) { Kv.Builder kvBuilder = Kv.newBuilder(); int pageCount = 0; try { - while (iterator.hasNext()) { + synchronized (this.cancellationLock) { + if (this.isStop.get()) { + return; + } + this.worker = Thread.currentThread(); + } + this.iterator = getIterator(request, this.wrapper, this.lifecycle::failedCleanup); + while (!this.isStop.get() && !Thread.currentThread().isInterrupted() && + iterator.hasNext()) { if (limit > 0 && ++this.total > limit) { break; } @@ -164,32 +194,45 @@ private void initIterator(ScanStreamReq request) { } dataBuilder.addData(toKv(kvBuilder, iterator.next(), iterator.position())); } + if (this.isStop.get()) { + return; + } + if (Thread.currentThread().isInterrupted()) { + this.failServer(HgGrpc.toErr(Status.Code.CANCELLED, "Scanning interrupted")); + return; + } this.channel.send(dataBuilder); } catch (Throwable t) { + if (this.isStop.get()) { + return; + } String msg = "an exception occurred while scanning data:"; - StatusRuntimeException ex = - HgGrpc.toErr(Status.INTERNAL, msg + t.getMessage(), t); - responseObserver.onError(ex); + Status status = t instanceof InterruptedException || + Thread.currentThread().isInterrupted() ? + Status.CANCELLED : Status.INTERNAL; + StatusRuntimeException ex = HgGrpc.toErr(status, msg + t.getMessage(), t); + this.failServer(ex); } finally { try { - this.iterator.close(); + if (!this.lifecycle.close(this.iterator)) { + this.failServer(Status.INTERNAL.withDescription("Failed to close scan iterator") + .withCause(this.lifecycle.cleanupFailure()) + .asRuntimeException()); + } + } finally { this.channel.close(); - } catch (Exception e) { - + synchronized (this.cancellationLock) { + this.worker = null; + } } } }; - this.executor.execute(scanning); + this.lifecycle.execute(this.executor, scanning); } catch (Exception e) { - StatusRuntimeException ex = HgGrpc.toErr(Status.INTERNAL, null, e); - responseObserver.onError(ex); - try { - this.iterator.close(); - this.channel.close(); - } catch (Exception exception) { - - } + StatusRuntimeException ex = e instanceof StatusRuntimeException ? + (StatusRuntimeException) e : HgGrpc.toErr(Status.INTERNAL, null, e); + this.failServer(ex); } /*** Scanning loop end ***/ @@ -205,19 +248,7 @@ private Kv toKv(Kv.Builder kvBuilder, RocksDBSession.BackendColumn col, } private void close() { - this.isStop.set(true); - this.channel.close(); - if (!this.finishFlag.get()) { - responseObserver.onNext(KvPageRes.newBuilder() - .addAllData(Collections.EMPTY_LIST) - .setOver(true) - .setTimes(++times) - .setVersion(this.responseVersion) - .build() - ); - } - - this.finishServer(); + this.cancelServer(true); } private void next(ScanStreamReq request) { @@ -225,12 +256,19 @@ private void next(ScanStreamReq request) { KvPageRes.Builder resBuilder; try { + if (this.isStop.get()) { + return; + } resBuilder = this.channel.receive(); times++; } catch (Exception e) { + if (this.isStop.get()) { + return; + } String msg = "failed to poll a page of data, cause by:"; + Status status = Thread.currentThread().isInterrupted() ? Status.CANCELLED : Status.INTERNAL; log.error(msg, e); - responseObserver.onError(HgGrpc.toErr(msg + e.getMessage())); + this.failServer(HgGrpc.toErr(status, msg + e.getMessage(), e)); return; } boolean isOver = false; @@ -239,31 +277,65 @@ private void next(ScanStreamReq request) { isOver = true; resBuilder = KvPageRes.newBuilder().addAllData(Collections.EMPTY_LIST); } - if (!this.finishFlag.get()) { - responseObserver.onNext(resBuilder.setOver(isOver) - .setTimes(times) - .setVersion(this.responseVersion) - .build()); - } + this.sendPage(resBuilder.setOver(isOver) + .setTimes(times) + .setVersion(this.responseVersion) + .build()); if (isOver) { this.finishServer(); } } + private void cancelServer(boolean sendFinalPage) { + // Cancellation can make the worker throw; establish the normal terminal first. + boolean complete = this.finishFlag.compareAndSet(false, true); + this.cancel(); + if (complete) { + synchronized (this.responseLock) { + if (sendFinalPage) { + this.responseObserver.onNext(KvPageRes.newBuilder() + .setOver(true) + .setTimes(++times) + .setVersion(this.responseVersion) + .build()); + } + this.responseObserver.onCompleted(); + } + } + } + + private void sendPage(KvPageRes page) { + synchronized (this.responseLock) { + if (!this.finishFlag.get()) { + this.responseObserver.onNext(page); + } + } + } + + private void failServer(Throwable failure) { + // Claim the terminal signal before waking a receiver on the closed channel. + boolean reportFailure = this.finishFlag.compareAndSet(false, true); + this.cancel(); + if (reportFailure) { + synchronized (this.responseLock) { + this.responseObserver.onError(failure); + } + } + } + private void finishServer() { if (!this.finishFlag.getAndSet(true)) { - responseObserver.onCompleted(); + synchronized (this.responseLock) { + this.responseObserver.onCompleted(); + } } } private void timeoutSever() { - if (!this.finishFlag.getAndSet(true)) { - String msg = "server wait time exceeds the threshold[" + waitTime + - "] seconds."; - responseObserver.onError( - HgGrpc.toErr(Status.Code.DEADLINE_EXCEEDED, msg)); - } + String msg = "server wait time exceeds the threshold[" + waitTime + + "] seconds."; + this.failServer(HgGrpc.toErr(Status.Code.DEADLINE_EXCEEDED, msg)); } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanUtil.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanUtil.java index a1a706edcc..8f873cbaeb 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanUtil.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/ScanUtil.java @@ -29,13 +29,16 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; +import java.util.function.Consumer; import java.util.stream.Collectors; import javax.annotation.concurrent.NotThreadSafe; import org.apache.hugegraph.pd.common.KVPair; import org.apache.hugegraph.rocksdb.access.ScanIterator; +import org.apache.hugegraph.store.business.BusinessHandlerImpl; import org.apache.hugegraph.store.business.SelectIterator; +import org.apache.hugegraph.store.constant.HugeServerTables; import org.apache.hugegraph.store.grpc.common.ScanMethod; import org.apache.hugegraph.store.grpc.common.ScanOrderType; import org.apache.hugegraph.store.grpc.stream.ScanQueryRequest; @@ -56,6 +59,11 @@ class ScanUtil { private final static Map tableKeyMap = new HashMap<>(); static ScanIterator getIterator(ScanStreamReq request, HgStoreWrapperEx wrapper) { + return getIterator(request, wrapper, ignored -> { }); + } + + static ScanIterator getIterator(ScanStreamReq request, HgStoreWrapperEx wrapper, + Consumer cleanupFailure) { boolean ordered = isOrdered(request); if (ordered && !request.getPosition().isEmpty()) { throw new IllegalArgumentException( @@ -99,8 +107,27 @@ static ScanIterator getIterator(ScanStreamReq request, HgStoreWrapperEx wrapper) if (selects != null) { properties = selects.getPropertiesList(); } - iter = new SelectIterator(iter, properties); - iter.seek(request.getPosition().toByteArray()); + if (properties != null && !properties.isEmpty()) { + boolean isVertex = HugeServerTables.VERTEX_TABLE.equals(table) || + HugeServerTables.TASK_TABLE.equals(table); + try { + if (!isVertex && !HugeServerTables.isEdgeTable(table)) { + throw new IllegalArgumentException("Property selection requires a vertex or edge table: " + table); + } + iter = new SelectIterator(iter, properties, BusinessHandlerImpl.getGraphSupplier(graph), isVertex); + } catch (RuntimeException | Error failure) { + try { + iter.close(); + } catch (RuntimeException | Error closeFailure) { + cleanupFailure.accept(closeFailure); + if (failure != closeFailure) { + failure.addSuppressed(closeFailure); + } + } + throw failure; + } + } + seek(iter, request.getPosition().toByteArray(), cleanupFailure); return iter; } @@ -119,6 +146,11 @@ static int responseVersion(ScanStreamReq request) { } static ScanIterator getIterator(ScanQuery sq, HgStoreWrapperEx wrapper) { + return getIterator(sq, wrapper, ignored -> { }); + } + + private static ScanIterator getIterator(ScanQuery sq, HgStoreWrapperEx wrapper, + Consumer cleanupFailure) { if (log.isDebugEnabled()) { log.debug("{}", sq); } @@ -143,12 +175,29 @@ static ScanIterator getIterator(ScanQuery sq, HgStoreWrapperEx wrapper) { iter = new EmptyIterator(); } - iter.seek(sq.position); + seek(iter, sq.position, cleanupFailure); return iter; } + private static void seek(ScanIterator iterator, byte[] position, + Consumer cleanupFailure) { + try { + iterator.seek(position); + } catch (RuntimeException | Error failure) { + try { + iterator.close(); + } catch (RuntimeException | Error closing) { + cleanupFailure.accept(closing); + if (failure != closing) { + failure.addSuppressed(closing); + } + } + throw failure; + } + } + static ScanQuery toSq(ScanStreamReq request) { ScanQuery res = ScanQuery.of(); @@ -174,7 +223,12 @@ static ScanQuery toSq(ScanStreamReq request) { static ScanIterator getIterator(String graph, ScanQueryRequest request, HgStoreWrapperEx wrapper) { - ScanIteratorSupplier supplier = new ScanIteratorSupplier(graph, request, wrapper); + return getIterator(graph, request, wrapper, ignored -> { }); + } + + static ScanIterator getIterator(String graph, ScanQueryRequest request, + HgStoreWrapperEx wrapper, Consumer cleanupFailure) { + ScanIteratorSupplier supplier = new ScanIteratorSupplier(graph, request, wrapper, cleanupFailure); return BatchScanIterator.of(supplier, supplier.getLimitSupplier()); } @@ -182,10 +236,11 @@ static ScanIterator getIterator(String graph, ScanQueryRequest request, * Support for multi-iterators with parallel reading */ static ScanIterator getParallelIterator(String graph, ScanQueryRequest request, - HgStoreWrapperEx wrapper, ThreadPoolExecutor executor) { - ScanIteratorSupplier supplier = new ScanIteratorSupplier(graph, request, wrapper); + HgStoreWrapperEx wrapper, ThreadPoolExecutor executor, + ScanLifecycle lifecycle) { + ScanIteratorSupplier supplier = new ScanIteratorSupplier(graph, request, wrapper, lifecycle::failedCleanup); return ParallelScanIterator.of(supplier, supplier.getLimitSupplier(), - request, executor); + request, executor, lifecycle::failedCleanup); } @NotThreadSafe @@ -198,6 +253,7 @@ private static class ScanIteratorSupplier implements private final long perKeyMax; private final long skipDegree; private final HgStoreWrapperEx wrapper; + private final Consumer cleanupFailure; private long perKeyLimit; private List sqs = new LinkedList<>(); private Iterator sqIterator; @@ -205,7 +261,9 @@ private static class ScanIteratorSupplier implements private ScanQueryProducer scanQueryProducer; private Iterator scanQueryIterator; - ScanIteratorSupplier(String graph, ScanQueryRequest request, HgStoreWrapperEx wrapper) { + ScanIteratorSupplier(String graph, ScanQueryRequest request, HgStoreWrapperEx wrapper, + Consumer cleanupFailure) { + this.cleanupFailure = cleanupFailure; this.graph = graph; this.perKeyLimit = request.getPerKeyLimit(); this.perKeyMax = request.getPerKeyMax(); @@ -293,7 +351,7 @@ public KVPair get1() { ScanQuery query = null; if (this.sqIterator != null && this.sqIterator.hasNext()) { query = this.sqIterator.next(); - iterator = getIterator(query, this.wrapper); + iterator = getIterator(query, this.wrapper, this.cleanupFailure); } else { this.sqs.clear(); this.sqIterator = null; @@ -351,7 +409,7 @@ public ScanIterator get() { if (index + 1 > queries.length) { return null; } - return getIterator(queries[index++], wrapper); + return getIterator(queries[index++], wrapper, cleanupFailure); } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryObserver.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryObserver.java index 0ba569cb92..973ab459f3 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryObserver.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryObserver.java @@ -21,13 +21,18 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.rocksdb.access.RocksDBSession; @@ -47,6 +52,8 @@ import com.google.protobuf.ByteString; +import io.grpc.Context; +import io.grpc.Status; import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; @@ -65,18 +72,193 @@ public class AggregativeQueryObserver implements StreamObserver { private volatile ScanIterator iterator = null; private QueryPlan plan = null; private String queryId; + private final Consumer completion; + private final Set workers = new HashSet<>(); + private int pendingTasks; + private boolean finished; + private volatile boolean requestCompleted; + private final Object responseLock = new Object(); + private boolean responseFinished; + private boolean completeResponse = true; + private QueryResponse finalResponse; + private boolean errorReported; + private final AtomicReference cleanupFailure = new AtomicReference<>(); public AggregativeQueryObserver(StreamObserver sender, ExecutorService threadPool, long timeout, int batchSize) { + this(sender, threadPool, timeout, batchSize, ignored -> { }); + } + + AggregativeQueryObserver(StreamObserver sender, + ExecutorService threadPool, long timeout, int batchSize, + Consumer completion) { this.sender = sender; this.threadPool = threadPool; this.batchSize = batchSize; this.timeout = timeout; + this.completion = completion; + } + + ScanIterator getIterator(QueryRequest request) { + return QueryUtil.getIterator(request); + } + + ScanIterator getCountIterator(QueryRequest request) { + return new QueryUtil().getHandler().scanAll(request.getGraph(), request.getTable()); + } + + void runUnary(QueryRequest request, boolean countOnly) { + synchronized (this) { + if (this.finished || this.clientCanceled.get()) { + return; + } + this.queryId = request.getQueryId(); + this.pendingTasks = 1; + this.workers.add(Thread.currentThread()); + } + Context context = Context.current(); + Context.CancellationListener cancellation = ignored -> onTransportCancel(); + Throwable failure = null; + try { + // Unary callbacks occupy the call executor while reading. Context cancellation + // must interrupt that read without waiting for the queued onCancel callback. + context.addListener(cancellation, Runnable::run); + if (this.clientCanceled.get()) { + return; + } + this.iterator = countOnly ? getCountIterator(request) : getIterator(request); + QueryResponse.Builder response = getBuilder(); + Kv.Builder kv = getKvBuilder(); + long count = countOnly ? countPartitions() : 0; + while (!countOnly && !this.clientCanceled.get() && this.iterator.hasNext()) { + Object next = this.iterator.next(); + if (next != null) { + RocksDBSession.BackendColumn column = (RocksDBSession.BackendColumn) next; + response.addData(kv.setKey(ByteString.copyFrom(column.name)) + .setValue(column.value == null ? ByteString.EMPTY : + ByteString.copyFrom(column.value)).build()); + } + } + if (countOnly) { + List counts = new ArrayList<>(); + for (int i = 0; i < request.getFunctionsCount(); i++) { + counts.add(new AtomicLong(count)); + } + response.addData(kv.setKey(ByteString.copyFrom(KvSerializer.toBytes(List.of()))) + .setValue(ByteString.copyFrom(KvSerializer.toBytes(counts))).build()); + } + synchronized (this.responseLock) { + this.finalResponse = response.setQueryId(this.queryId).setIsOk(true) + .setIsFinished(true).build(); + } + } catch (RuntimeException | Error e) { + failure = e; + synchronized (this.responseLock) { + this.finalResponse = errorResponse(getBuilder(), this.queryId, e); + } + } finally { + try { + context.removeListener(cancellation); + } finally { + workerFinished(failure); + } + } + } + + private long countIterator(ScanIterator source) { + long count = 0; + while (!this.clientCanceled.get() && source.hasNext()) { + source.next(); + count++; + } + return count; + } + + private long countPartitions() { + if (!(this.iterator instanceof MultiPartitionIterator)) { + return countIterator(this.iterator); + } + List children = ((MultiPartitionIterator) this.iterator).getIterators(); + CountDownLatch completed = new CountDownLatch(children.size()); + AtomicLong total = new AtomicLong(); + AtomicReference failure = new AtomicReference<>(); + synchronized (this) { + this.pendingTasks += children.size(); + } + int submitted = 0; + try { + for (ScanIterator child : children) { + this.threadPool.execute(() -> { + taskStarted(); + try { + total.addAndGet(countIterator(child)); + } catch (RuntimeException | Error e) { + recordCountFailure(failure, e); + } finally { + try { + cleanup(child::close); + } finally { + workerFinished(); + completed.countDown(); + } + } + }); + submitted++; + } + } catch (RuntimeException | Error e) { + recordCountFailure(failure, e); + for (int i = submitted; i < children.size(); i++) { + ScanIterator child = children.get(i); + try { + cleanup(child::close); + } finally { + taskFinished(); + completed.countDown(); + } + } + } + boolean interrupted = false; + for (;;) { + try { + completed.await(); + break; + } catch (InterruptedException e) { + interrupted = true; + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + Throwable error = failure.get(); + if (error instanceof RuntimeException) { + throw (RuntimeException) error; + } + if (error instanceof Error) { + throw (Error) error; + } + return total.get(); + } + + private static void recordCountFailure(AtomicReference failure, Throwable error) { + synchronized (failure) { + if (failure.get() == null) { + failure.set(error); + } else if (failure.get() != error) { + failure.get().addSuppressed(error); + } + } + } + + QueryPlan buildPlan(QueryRequest request) { + return QueryUtil.buildPlan(request); } @Override - public void onNext(QueryRequest request) { + public synchronized void onNext(QueryRequest request) { + if (this.clientCanceled.get() || this.finished || this.requestCompleted) { + return; + } if (this.queryId == null) { log.debug("got request: {}", request); this.queryId = request.getQueryId(); @@ -85,9 +267,23 @@ public void onNext(QueryRequest request) { // the first request, start the sending thread if (iterator == null) { long current = System.nanoTime(); - iterator = QueryUtil.getIterator(request); - plan = QueryUtil.buildPlan(request); - threadPool.submit(this::sendData); + this.pendingTasks = 1; + try { + iterator = getIterator(request); + plan = buildPlan(request); + threadPool.execute(this::sendData); + } catch (RuntimeException | Error e) { + // The framework must report the original synchronous failure, not normal completion. + synchronized (this.responseLock) { + this.completeResponse = false; + } + taskFinished(); + Throwable failure = this.cleanupFailure.get(); + if (failure != null && failure != e) { + e.addSuppressed(failure); + } + throw e; + } log.debug("query id: {}, init data cost: {} ms", queryId, (System.nanoTime() - current) * 1.0 / 1000000); } else { @@ -98,18 +294,162 @@ public void onNext(QueryRequest request) { @Override public void onError(Throwable t) { - // Stop calculating when channel got error - this.clientCanceled.set(true); + onTransportCancel(); log.error("AggregativeQueryService, query id: {}, got error", this.queryId, t); } + void onTransportCancel() { + // Both transport cancellation and request onError can report the same event. + synchronized (this.responseLock) { + if (this.responseFinished) { + return; + } + this.completeResponse = false; + this.responseFinished = true; + } + cancel(); + } + @Override - public void onCompleted() { - // client my be cancelled earlier + public synchronized void onCompleted() { + // Half-close ends feedback, not work already supported by its existing credit. + this.requestCompleted = true; + if (this.pendingTasks == 0 && !this.finished) { + this.finished = true; + finishResponse(); + } + } + + void cancelForShutdown() { + try { + synchronized (this.responseLock) { + if (!this.responseFinished) { + this.responseFinished = true; + this.completeResponse = false; + this.sender.onError(Status.UNAVAILABLE.withDescription("Store queries are stopping") + .asRuntimeException()); + } + } + } finally { + cancel(); + } + } + + public void cancel() { this.clientCanceled.set(true); + synchronized (this) { + for (Thread worker : this.workers) { + worker.interrupt(); + } + // An RPC with no first request has no iterator or cleanup task. + if (this.pendingTasks == 0 && !this.finished) { + this.finished = true; + finishResponse(); + } + } + } + + private synchronized void taskStarted() { + this.workers.add(Thread.currentThread()); + } + + private void workerFinished() { + workerFinished(null); + } + + private void workerFinished(Throwable failure) { + synchronized (this) { + this.workers.remove(Thread.currentThread()); + } + taskFinished(failure); + } + + private void taskFinished() { + taskFinished(null); + } + + private void taskFinished(Throwable primaryFailure) { + boolean cleanup; + synchronized (this) { + cleanup = --this.pendingTasks == 0; + if (cleanup) { + this.finished = true; + } + } + if (cleanup) { + if (this.plan != null) { + cleanup(this.plan::clear); + } + if (this.iterator != null) { + cleanup(this.iterator::close); + } + Throwable releaseFailure = this.cleanupFailure.get(); + if (primaryFailure != null && releaseFailure != null && primaryFailure != releaseFailure) { + primaryFailure.addSuppressed(releaseFailure); + } + finishResponse(); + } + } + + private void cleanup(Runnable release) { + try { + release.run(); + } catch (RuntimeException | Error failure) { + synchronized (this.cleanupFailure) { + Throwable first = this.cleanupFailure.get(); + if (first == null) { + this.cleanupFailure.set(failure); + } else if (first != failure) { + first.addSuppressed(failure); + } + } + log.error("Aggregate query {} failed to release resources; Store shutdown remains blocked", + this.queryId, failure); + } + } + + private void finishResponse() { + try { + synchronized (this.responseLock) { + if (this.completeResponse && !this.responseFinished) { + this.responseFinished = true; + Throwable failure = this.cleanupFailure.get(); + if (this.finalResponse != null && !this.finalResponse.getIsOk() && + !this.clientCanceled.get()) { + this.sender.onNext(this.finalResponse); + } else if (failure != null && !this.errorReported) { + this.sender.onNext(errorResponse(getBuilder(), this.queryId, failure)); + } else if (failure == null && this.finalResponse != null && + !this.clientCanceled.get()) { + this.sender.onNext(this.finalResponse); + } + this.sender.onCompleted(); + } + } + } finally { + // A terminal RPC does not prove that its native resources were released. + if (this.cleanupFailure.get() == null) { + this.completion.accept(this); + } + } + } + + private void sendResponse(QueryResponse response) { + synchronized (this.responseLock) { + if (!this.responseFinished && !this.clientCanceled.get()) { + if (response.getIsOk() && response.getIsFinished()) { + // The client treats this batch as success; publish it only after cleanup. + this.finalResponse = response; + } else { + this.errorReported |= !response.getIsOk(); + this.sender.onNext(response); + } + } + } } public void sendData() { + taskStarted(); try { long lastSend = System.currentTimeMillis(); var responseBuilder = getBuilder(); @@ -118,12 +458,18 @@ public void sendData() { while (!this.clientCanceled.get()) { // produces more result than consumer, just waiting if (sendCount.get() - consumeCount.get() >= RESULT_COUNT) { + if (this.requestCompleted) { + sendResponse(errorResponse(getBuilder(), queryId, + new IllegalStateException( + "Request completed without enough feedback"))); + cancel(); + return; + } // read timeout, takes long time not to read data if (System.currentTimeMillis() - lastSend > timeout) { - this.sender.onNext(errorResponse(getBuilder(), queryId, - new RuntimeException( - "sending-timeout, server closed"))); - this.sender.onCompleted(); + sendResponse(errorResponse(getBuilder(), queryId, + new RuntimeException("sending-timeout, server closed"))); + cancel(); return; } @@ -141,23 +487,33 @@ public void sendData() { } else { try { builder.setQueryId(queryId); - sender.onNext(builder.build()); + sendResponse(builder.build()); this.sendCount.incrementAndGet(); lastSend = System.currentTimeMillis(); } catch (Exception e) { log.error("send data got error: ", e); + cancel(); break; } } - if (builder.getIsFinished() || !builder.getIsOk()) { + if (!builder.getIsOk()) { + // Report the internal error before cancelling remaining partition work. + cancel(); + break; + } + if (builder.getIsFinished()) { break; } } + } catch (Exception e) { + try { + sendResponse(errorResponse(getBuilder(), queryId, e)); + } finally { + cancel(); + } } finally { - this.plan.clear(); - this.iterator.close(); - this.sender.onCompleted(); + workerFinished(); } } @@ -188,32 +544,64 @@ private QueryResponse.Builder readBatchData(QueryResponse.Builder builder, return builder; } else if (this.plan.hasIteratorResult()) { checkIterator = false; - AtomicReference exception = new AtomicReference<>(); + AtomicReference exception = new AtomicReference<>(); if (this.iterator instanceof MultiPartitionIterator) { var iterators = ((MultiPartitionIterator) this.iterator).getIterators(); CountDownLatch latch = new CountDownLatch(iterators.size()); - for (var itr2 : iterators) { - threadPool.execute(() -> { + synchronized (this) { + this.pendingTasks += iterators.size(); + } + int submitted = 0; + try { + for (var itr2 : iterators) { + threadPool.execute(() -> { + taskStarted(); + try { + execute(itr2); + } catch (RuntimeException | Error e) { + exception.compareAndSet(null, e); + } finally { + try { + cleanup(itr2::close); + Throwable failure = this.cleanupFailure.get(); + if (failure != null) { + exception.compareAndSet(null, new RuntimeException( + "partition iterator cleanup failed", failure)); + } + } finally { + latch.countDown(); + workerFinished(); + } + } + }); + submitted++; + } + } catch (RuntimeException | Error failure) { + // Rejection must also release iterators whose tasks were not accepted. + for (int i = submitted; i < iterators.size(); i++) { try { - execute(itr2); - } catch (Exception e) { - exception.set(e); + cleanup(iterators.get(i)::close); } finally { - // MultiPartitionIterator close() not working - itr2.close(); latch.countDown(); + taskFinished(); } - }); + } + throw failure; + } + if (!latch.await(timeout, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("partition query timed out"); } - latch.await(timeout, TimeUnit.MILLISECONDS); if (exception.get() != null) { - throw exception.get(); + throw new RuntimeException("partition query failed", exception.get()); } } else { // can't be parallel, but has agg like stage execute(this.iterator); } + if (this.clientCanceled.get()) { + return builder.setIsOk(false).setIsFinished(false); + } try { // last empty element itr = (ScanIterator) plan.execute(PipelineResult.EMPTY); @@ -227,7 +615,7 @@ private QueryResponse.Builder readBatchData(QueryResponse.Builder builder, builder.clear(); List batchResult = new ArrayList<>(); - while (itr.hasNext() && !this.clientCanceled.get()) { + while (!this.clientCanceled.get() && itr.hasNext()) { if (count >= batchSize) { break; } @@ -265,6 +653,9 @@ private QueryResponse.Builder readBatchData(QueryResponse.Builder builder, builder.addAllData(batchResult); } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } log.error("readBatchData got error: ", e); return builder.setIsOk(false).setIsFinished(false).setMessage("Store Server Error: " + Arrays.toString( @@ -273,7 +664,7 @@ private QueryResponse.Builder readBatchData(QueryResponse.Builder builder, if (checkIterator) { // check the iterator - finish = !itr.hasNext(); + finish = this.clientCanceled.get() || !itr.hasNext(); } log.debug("query id: {}, finished batch, with size :{}, finish:{}, cost: {} ms", queryId, count, @@ -320,7 +711,7 @@ public void close() { private void execute(ScanIterator itr) { long recordCount = 0; long current = System.nanoTime(); - while (itr.hasNext() && !this.clientCanceled.get()) { + while (!this.clientCanceled.get() && itr.hasNext()) { try { recordCount++; executePipeline(itr.next()); diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryService.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryService.java index 3eb81be851..7b024e69cc 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryService.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryService.java @@ -17,24 +17,20 @@ package org.apache.hugegraph.store.node.grpc.query; -import java.util.ArrayList; -import java.util.List; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.atomic.AtomicLong; -import org.apache.hugegraph.rocksdb.access.RocksDBSession; import org.apache.hugegraph.store.HgStoreEngine; import org.apache.hugegraph.store.consts.PoolNames; -import org.apache.hugegraph.store.grpc.common.Kv; import org.apache.hugegraph.store.grpc.query.QueryRequest; import org.apache.hugegraph.store.grpc.query.QueryResponse; import org.apache.hugegraph.store.grpc.query.QueryServiceGrpc; -import org.apache.hugegraph.store.query.KvSerializer; import org.apache.hugegraph.store.util.ExecutorUtil; import org.lognet.springboot.grpc.GRpcService; -import com.google.protobuf.ByteString; - +import io.grpc.Status; +import io.grpc.stub.ServerCallStreamObserver; import io.grpc.stub.StreamObserver; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -43,6 +39,9 @@ @GRpcService public class AggregativeQueryService extends QueryServiceGrpc.QueryServiceImplBase { + private final Set queries = new HashSet<>(); + private boolean closing; + private final int batchSize; private final Long timeout; @@ -62,6 +61,65 @@ public AggregativeQueryService() { 10000, true); } + AggregativeQueryService(ThreadPoolExecutor threadPool, long timeout, int batchSize) { + this.threadPool = threadPool; + this.timeout = timeout; + this.batchSize = batchSize; + } + + public synchronized void stopAcceptingQueries() { + this.closing = true; + } + + /** Cancel queries, but let every queued parent and partition release its iterator. */ + public void shutdownQueries() { + AggregativeQueryObserver[] active; + synchronized (this) { + this.closing = true; + active = this.queries.toArray(new AggregativeQueryObserver[0]); + } + for (AggregativeQueryObserver query : active) { + try { + query.cancelForShutdown(); + } catch (RuntimeException | Error failure) { + // Response callbacks cannot skip cancellation or cleanup waits for other queries. + log.error("Failed to cancel aggregate query response; continuing shutdown", failure); + } + } + boolean interrupted = false; + synchronized (this) { + try { + while (!this.queries.isEmpty()) { + try { + this.wait(5000); + if (!this.queries.isEmpty()) { + log.warn("Waiting for {} aggregate queries to release iterators", + this.queries.size()); + } + } catch (InterruptedException e) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + // Parents can still enqueue partition cleanup until the last query is finished. + this.threadPool.shutdown(); + } + + private synchronized void finished(AggregativeQueryObserver observer) { + this.queries.remove(observer); + this.notifyAll(); + } + + AggregativeQueryObserver newObserver(StreamObserver sender) { + return new AggregativeQueryObserver(sender, this.threadPool, this.timeout, + this.batchSize, this::finished); + } + /** * Generate error response. * @@ -79,73 +137,26 @@ public static QueryResponse errorResponse(QueryResponse.Builder builder, String } @Override - public StreamObserver query(StreamObserver observer) { - return new AggregativeQueryObserver(observer, threadPool, timeout, batchSize); + public synchronized StreamObserver query(StreamObserver observer) { + if (this.closing) { + throw Status.UNAVAILABLE.withDescription("Store queries are stopping").asRuntimeException(); + } + AggregativeQueryObserver query = newObserver(observer); + this.queries.add(query); + if (observer instanceof ServerCallStreamObserver) { + ((ServerCallStreamObserver) observer).setOnCancelHandler( + query::onTransportCancel); + } + return query; } @Override public void query0(QueryRequest request, StreamObserver observer) { - - var itr = QueryUtil.getIterator(request); - var builder = QueryResponse.newBuilder(); - var kvBuilder = Kv.newBuilder(); - - try { - while (itr.hasNext()) { - var column = (RocksDBSession.BackendColumn) itr.next(); - if (column != null) { - builder.addData(kvBuilder.setKey(ByteString.copyFrom(column.name)) - .setValue(column.value == null ? ByteString.EMPTY : - ByteString.copyFrom(column.value)) - .build()); - } - } - builder.setQueryId(request.getQueryId()); - builder.setIsOk(true); - builder.setIsFinished(true); - observer.onNext(builder.build()); - } catch (Exception e) { - observer.onNext(errorResponse(builder, request.getQueryId(), e)); - } - observer.onCompleted(); + ((AggregativeQueryObserver) query(observer)).runUnary(request, false); } - /** - * Query data count - * - * @param request query request object - * @param observer Observer object for receiving query response results - */ @Override public void count(QueryRequest request, StreamObserver observer) { - - log.debug("query id : {}, simple count of table: {}", request.getQueryId(), - request.getTable()); - var builder = QueryResponse.newBuilder(); - var kvBuilder = Kv.newBuilder(); - - try { - - var handler = new QueryUtil().getHandler(); - long start = System.currentTimeMillis(); - long count = handler.count(request.getGraph(), request.getTable()); - log.debug("query id: {}, count of cost: {} ms", request.getQueryId(), - System.currentTimeMillis() - start); - List array = new ArrayList<>(); - for (int i = 0; i < request.getFunctionsList().size(); i++) { - array.add(new AtomicLong(count)); - } - - kvBuilder.setKey(ByteString.copyFrom(KvSerializer.toBytes(List.of()))); - kvBuilder.setValue(ByteString.copyFrom(KvSerializer.toBytes(array))); - builder.addData(kvBuilder.build()); - builder.setQueryId(request.getQueryId()); - builder.setIsOk(true); - builder.setIsFinished(true); - observer.onNext(builder.build()); - } catch (Exception e) { - observer.onNext(errorResponse(builder, request.getQueryId(), e)); - } - observer.onCompleted(); + ((AggregativeQueryObserver) query(observer)).runUnary(request, true); } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/QueryUtil.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/QueryUtil.java index 5d961038e6..c6fbbeed23 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/QueryUtil.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/QueryUtil.java @@ -42,6 +42,7 @@ import org.apache.hugegraph.store.grpc.query.AggregationType; import org.apache.hugegraph.store.grpc.query.DeDupOption; import org.apache.hugegraph.store.grpc.query.QueryRequest; +import org.apache.hugegraph.store.grpc.query.QueryResultFormat; import org.apache.hugegraph.store.grpc.query.ScanType; import org.apache.hugegraph.store.grpc.query.ScanTypeParam; import org.apache.hugegraph.store.node.grpc.EmptyIterator; @@ -100,12 +101,12 @@ public static QueryPlan buildPlan(QueryRequest request) { } else { if (request.getCheckTtl()) { var ttl = QueryStages.ofTtlCheckStage(); - ttl.init(isVertex(request.getTable())); + ttl.init(isVertex(request.getTable()), getGraphSupplier(request.getGraph())); plan.addStage(ttl); } // when to de-serialization ? - if (needDeserialize(request)) { + if (QueryResultFormat.requiresElementDeserialization(request)) { var deserializeStage = QueryStages.ofDeserializationStage(); deserializeStage.init(request.getTable(), getGraphSupplier(request.getGraph())); @@ -196,18 +197,6 @@ private static boolean canOptimiseToTop(QueryRequest request) { request.getLimit() > 0; } - /** - * Determine whether deserialization is needed. - * - * @param request query request object. - * @return true if deserialization is needed, false otherwise. - */ - private static boolean needDeserialize(QueryRequest request) { - return !isEmpty(request.getOrderByList()) || !isEmpty(request.getPropertyList()) - || !request.getCondition().isEmpty() || !isEmpty(request.getFunctionsList()) - && !request.getGroupBySchemaLabel(); - } - /** * Get a scan iterator. * @@ -295,14 +284,14 @@ public static BaseElement parseEntry(HugeGraphSupplier graph, BackendColumn column, boolean isVertex) { if (isVertex) { - return serializer.parseVertex(graph, column, null); + return serializer.parseSchemaVertex(graph, column, null); } else { - return serializer.parseEdge(graph, column, null, true); + return serializer.parseSchemaEdge(graph, column, null, true); } } - public static BaseElement parseOlap(BackendColumn column, BaseVertex vertex) { - return serializer.parseVertexOlap(null, column, vertex); + public static BaseElement parseOlap(HugeGraphSupplier graph, BackendColumn column, BaseVertex vertex) { + return serializer.parseSchemaVertexOlap(graph, column, vertex); } /** @@ -312,9 +301,13 @@ public static BaseElement parseOlap(BackendColumn column, BaseVertex vertex) { * @param olap olap vertex * @return new vertex */ - public static BackendColumn combineColumn(BackendColumn vertexColumn, + public static BackendColumn combineColumn(HugeGraphSupplier graph, BackendColumn vertexColumn, List olap) { - return serializer.mergeCols(vertexColumn, olap.toArray(new BackendColumn[0])); + BaseVertex vertex = serializer.parseSchemaVertex(graph, vertexColumn, null); + for (BackendColumn column : olap) { + serializer.parseSchemaVertexOlap(graph, column, vertex); + } + return BackendColumn.of(vertexColumn.name, serializer.formatSchemaVertexValue(vertex)); } public static AggregationFunction createFunc(AggregationType funcType, String genericType) { diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/model/QueryPlan.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/model/QueryPlan.java index 228850d65c..d3289c2492 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/model/QueryPlan.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/model/QueryPlan.java @@ -118,10 +118,25 @@ public String toString() { } public void clear() { + Throwable failure = null; for (var stage : stages) { - stage.close(); + try { + stage.close(); + } catch (RuntimeException | Error e) { + if (failure == null) { + failure = e; + } else if (failure != e) { + failure.addSuppressed(e); + } + } } this.stages.clear(); + if (failure instanceof Error) { + throw (Error) failure; + } + if (failure != null) { + throw (RuntimeException) failure; + } } public boolean isEmpty() { diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/stages/OlapStage.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/stages/OlapStage.java index a3dc6edd40..f9377d8f3d 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/stages/OlapStage.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/stages/OlapStage.java @@ -22,13 +22,16 @@ import java.util.ArrayList; import java.util.List; +import org.apache.hugegraph.HugeGraphSupplier; import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.pd.common.PartitionUtils; import org.apache.hugegraph.rocksdb.access.RocksDBSession; import org.apache.hugegraph.serializer.BinaryElementSerializer; +import org.apache.hugegraph.serializer.OlapKey; import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.store.business.BusinessHandler; +import org.apache.hugegraph.store.business.BusinessHandlerImpl; import org.apache.hugegraph.store.node.grpc.query.QueryStage; import org.apache.hugegraph.store.node.grpc.query.QueryUtil; import org.apache.hugegraph.store.node.grpc.query.model.PipelineResult; @@ -45,15 +48,25 @@ @Slf4j public class OlapStage implements QueryStage { - private final BusinessHandler handler = new QueryUtil().getHandler(); + private final BusinessHandler handler; private final BinaryElementSerializer serializer = new BinaryElementSerializer(); private String graph; + private HugeGraphSupplier supplier; private String table; private List properties; + public OlapStage() { + this(new QueryUtil().getHandler()); + } + + public OlapStage(BusinessHandler handler) { + this.handler = java.util.Objects.requireNonNull(handler, "handler"); + } + @Override public void init(Object... objects) { this.graph = (String) objects[0]; + this.supplier = BusinessHandlerImpl.getGraphSupplier(this.graph); this.table = (String) objects[1]; this.properties = QueryUtil.fromStringBytes((List) objects[2]); } @@ -70,32 +83,28 @@ public PipelineResult handle(PipelineResult result) { PartitionUtils.calcHashcode(BinaryElementSerializer.ownerId(element).asBytes()); for (Id property : properties) { - // Build key - var key = getOlapKey(property, element.id()); - var values = handler.doGet(this.graph, code, OLAP_TABLE, key); - if (values != null) { - var column = BackendColumn.of(key, values); - QueryUtil.parseOlap(column, (BaseVertex) element); + BackendColumn column = this.readProperty(code, property, element.id()); + if (column != null) { + QueryUtil.parseOlap(this.supplier, column, (BaseVertex) element); } } } else if (result.getResultType() == PipelineResultType.BACKEND_COLUMN) { var column = result.getColumn(); try { var vertexOnlyId = - serializer.parseVertex(null, BackendColumn.of(column.name, null), null); + serializer.parseSchemaVertex(this.supplier, BackendColumn.of(column.name, null), null); var code = PartitionUtils.calcHashcode( BinaryElementSerializer.ownerId(vertexOnlyId).asBytes()); // todo: Wait for structure to change to byte[] operations var list = new ArrayList(); for (Id property : properties) { - var key = getOlapKey(property, vertexOnlyId.id()); - var values = handler.doGet(this.graph, code, OLAP_TABLE, key); - if (values != null) { - list.add(BackendColumn.of(key, values)); + BackendColumn olap = this.readProperty(code, property, vertexOnlyId.id()); + if (olap != null) { + list.add(olap); } } var vertex = - QueryUtil.combineColumn(BackendColumn.of(column.name, column.value), list); + QueryUtil.combineColumn(this.supplier, BackendColumn.of(column.name, column.value), list); result.setColumn(RocksDBSession.BackendColumn.of(vertex.name, vertex.value)); } catch (Exception e) { log.error("parse olap error, graph: {}, table : {}", graph, table, e); @@ -105,11 +114,18 @@ public PipelineResult handle(PipelineResult result) { return result; } - private byte[] getOlapKey(Id propertyId, Id vertexId) { - BytesBuffer bufferName = - BytesBuffer.allocate(1 + propertyId.length() + 1 + vertexId.length()); - bufferName.writeId(propertyId); - return bufferName.writeId(vertexId).bytes(); + private BackendColumn readProperty(int code, Id propertyId, Id vertexId) { + byte[] key = OlapKey.format(propertyId, vertexId); + byte[] value = this.handler.doGet(this.graph, code, OLAP_TABLE, key); + if (value == null || value.length == 0) { + // Previous HStore writes used the standalone vertex-only key in the merged table. + byte[] legacyKey = BytesBuffer.allocate(vertexId.length() + 9).writeId(vertexId).bytes(); + value = this.handler.doGet(this.graph, code, OLAP_TABLE, legacyKey); + } + if (!OlapKey.matchesProperty(value, propertyId)) { + return null; + } + return BackendColumn.of(key, value); } @Override diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/stages/TtlCheckStage.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/stages/TtlCheckStage.java index 315ec2d31d..0185d52b13 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/stages/TtlCheckStage.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/query/stages/TtlCheckStage.java @@ -17,6 +17,7 @@ package org.apache.hugegraph.store.node.grpc.query.stages; +import org.apache.hugegraph.HugeGraphSupplier; import org.apache.hugegraph.serializer.DirectBinarySerializer; import org.apache.hugegraph.store.node.grpc.query.QueryStage; import org.apache.hugegraph.store.node.grpc.query.model.PipelineResult; @@ -31,6 +32,7 @@ public class TtlCheckStage implements QueryStage { private boolean isVertex; + private HugeGraphSupplier graph; private final DirectBinarySerializer serializer = new DirectBinarySerializer(); private long now; @@ -38,6 +40,7 @@ public class TtlCheckStage implements QueryStage { @Override public void init(Object... objects) { this.isVertex = (boolean) objects[0]; + this.graph = (HugeGraphSupplier) objects[1]; now = System.currentTimeMillis(); } @@ -46,8 +49,8 @@ public PipelineResult handle(PipelineResult result) { if (result.getResultType() == PipelineResultType.BACKEND_COLUMN) { var col = result.getColumn(); try { - var element = isVertex ? serializer.parseVertex(col.name, col.value) : - serializer.parseEdge(col.name, col.value); + var element = isVertex ? serializer.parseSchemaVertex(this.graph, col.name, col.value) : + serializer.parseSchemaEdge(this.graph, col.name, col.value); if (element.expiredTime() > 0 && element.expiredTime() < now) { return null; } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/scan/GraphStoreImpl.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/scan/GraphStoreImpl.java index 6583e59745..31bd69c40f 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/scan/GraphStoreImpl.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/scan/GraphStoreImpl.java @@ -77,7 +77,7 @@ public ThreadPoolExecutor getExecutor() { @Override public StreamObserver scanPartition( StreamObserver ro) { - return new ScanResponseObserver(ro, getHandler(), getExecutor()); + return this.storeStream.scanGraphPartition(ro, getHandler()); } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/scan/ScanResponseObserver.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/scan/ScanResponseObserver.java index b4d10f7c43..2bb7ee44f0 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/scan/ScanResponseObserver.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/grpc/scan/ScanResponseObserver.java @@ -18,249 +18,285 @@ package org.apache.hugegraph.store.node.grpc.scan; import java.util.ArrayList; -import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; 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.locks.ReentrantLock; +import java.util.function.Consumer; import org.apache.hugegraph.store.business.BusinessHandler; import org.apache.hugegraph.store.business.GraphStoreIterator; -import org.apache.hugegraph.store.grpc.Graphpb.Error; +import org.apache.hugegraph.store.grpc.Graphpb; import org.apache.hugegraph.store.grpc.Graphpb.ErrorType; import org.apache.hugegraph.store.grpc.Graphpb.ResponseHeader; import org.apache.hugegraph.store.grpc.Graphpb.ScanPartitionRequest; -import org.apache.hugegraph.store.grpc.Graphpb.ScanPartitionRequest.Request; import org.apache.hugegraph.store.grpc.Graphpb.ScanPartitionRequest.ScanType; import org.apache.hugegraph.store.grpc.Graphpb.ScanResponse; import com.google.protobuf.Descriptors; +import io.grpc.Status; import io.grpc.stub.StreamObserver; -import lombok.extern.slf4j.Slf4j; -@Slf4j -public class ScanResponseObserver implements - StreamObserver { +public class ScanResponseObserver implements StreamObserver { private static final int BATCH_SIZE = 100000; - private static final int MAX_PAGE = 8; // - private static final Error ok = Error.newBuilder().setType(ErrorType.OK).build(); - private static final ResponseHeader okHeader = - ResponseHeader.newBuilder().setError(ok).build(); + private static final int MAX_PAGE = 8; + private static final ResponseHeader OK = ResponseHeader.newBuilder() + .setError(Graphpb.Error.newBuilder().setType(ErrorType.OK)).build(); private final BusinessHandler handler; - private final AtomicInteger nextSeqNo = new AtomicInteger(0); - private final AtomicInteger cltSeqNo = new AtomicInteger(0); - private final ThreadPoolExecutor executor; - private final AtomicBoolean readOver = new AtomicBoolean(false); - private final LinkedBlockingQueue packages = - new LinkedBlockingQueue(MAX_PAGE * 2); - private final Descriptors.FieldDescriptor vertexField = - ScanResponse.getDescriptor().findFieldByNumber(3); - private final Descriptors.FieldDescriptor edgeField = - ScanResponse.getDescriptor().findFieldByNumber(4); - private final ReentrantLock readLock = new ReentrantLock(); - private final ReentrantLock sendLock = new ReentrantLock(); - private StreamObserver sender; + private final StreamObserver sender; + private final Consumer execute; + private final Consumer failedCleanup; + private final AtomicInteger nextSeqNo = new AtomicInteger(); + private final AtomicInteger cltSeqNo = new AtomicInteger(); + private final AtomicBoolean readOver = new AtomicBoolean(); + private final AtomicBoolean closed = new AtomicBoolean(); + private final AtomicBoolean halfClosed = new AtomicBoolean(); + private final AtomicBoolean reading = new AtomicBoolean(); + private final AtomicBoolean sending = new AtomicBoolean(); + private final LinkedBlockingQueue packages = new LinkedBlockingQueue<>(MAX_PAGE * 2); + private final Object iteratorLock = new Object(); + private final Object readerLock = new Object(); + private Thread reader; + private GraphStoreIterator iter; private ScanPartitionRequest scanReq; - private GraphStoreIterator iter; - private volatile long leftCount; - private volatile Future sendTask; - private volatile Future readTask; + private boolean iteratorClosed; + private long leftCount; - /* - * November 1, 2022 - * 1. onNext needs to be processed asynchronously to prevent the grpc call from being blocked. - * 2. Do not read iterators or send data do not produce thread waiting. - * 3. Before sending, try to prepare the data to be sent as much as possible. - * */ - - /* - * November 2, 2022 - * 1. Read the thread of rocksdb iterator read - * 2. Perform data conversion and send to the blocking queue thread offer - * 3. Thread for reading data from the blocking queue and sending, including waking up the - * reading and sending threads when no data is read - * */ + public ScanResponseObserver(StreamObserver sender, + BusinessHandler handler, ThreadPoolExecutor executor) { + this(sender, handler, executor::execute, failure -> { }); + } public ScanResponseObserver(StreamObserver sender, BusinessHandler handler, - ThreadPoolExecutor executor) { + Consumer execute, Consumer failedCleanup) { this.sender = sender; this.handler = handler; - this.executor = executor; - } - - private boolean readCondition() { - return packages.remainingCapacity() != 0 && !readOver.get(); - } - - private boolean readTaskCondition() { - return readCondition() && (readTask == null || readTask.isDone()); - } - - private boolean sendCondition() { - return nextSeqNo.get() - cltSeqNo.get() < MAX_PAGE; + this.execute = execute; + this.failedCleanup = failedCleanup; } - private boolean sendTaskCondition() { - return sendCondition() && (sendTask == null || sendTask.isDone()); - } - - private void offer(Iterable data, boolean isVertex) { - ScanResponse.Builder builder = ScanResponse.newBuilder(); - builder.setHeader(okHeader).setSeqNo(nextSeqNo.get()); - if (isVertex) { - builder = builder.setField(vertexField, data); - } else { - builder = builder.setField(edgeField, data); + @Override + public void onNext(ScanPartitionRequest request) { + if (this.closed.get() || this.halfClosed.get()) { + return; } - ScanResponse response = builder.build(); - packages.offer(response); - startSend(); - } - - private void startRead() { - if (readTaskCondition()) { - if (readLock.tryLock()) { - if (readTaskCondition()) { - readTask = executor.submit(rr); + try { + if (request.hasScanRequest() && !request.hasReplyRequest()) { + synchronized (this.iteratorLock) { + if (this.closed.get() || this.halfClosed.get()) { + return; + } + if (this.iter != null) { + throw Status.INVALID_ARGUMENT.withDescription("Scan request was already initialized") + .asRuntimeException(); + } + this.scanReq = request; + long limit = request.getScanRequest().getLimit(); + this.leftCount = limit > 0 ? limit : Long.MAX_VALUE; + this.iter = this.handler.scan(request); } - readLock.unlock(); + startRead(); + } else { + this.cltSeqNo.incrementAndGet(); + startSend(); } + } catch (RuntimeException | Error failure) { + terminate(failure); } } - private void startSend() { - if (sendTaskCondition()) { - if (sendLock.tryLock()) { - if (sendTaskCondition()) { - sendTask = executor.submit(sr); - } - sendLock.unlock(); - } - } + @Override + public void onError(Throwable failure) { + terminate(failure); } @Override - public void onNext(ScanPartitionRequest scanReq) { - if (scanReq.hasScanRequest() && !scanReq.hasReplyRequest()) { - this.scanReq = scanReq; - Request request = scanReq.getScanRequest(); - long rl = request.getLimit(); - leftCount = rl > 0 ? rl : Long.MAX_VALUE; - iter = handler.scan(scanReq); - if (!iter.hasNext()) { - close(); - sender.onCompleted(); - } else { - readTask = executor.submit(rr); - } + public void onCompleted() { + boolean initialized; + synchronized (this.iteratorLock) { + this.halfClosed.set(true); + initialized = this.iter != null; + } + if (!initialized) { + terminate(null); } else { - cltSeqNo.getAndIncrement(); startSend(); } } - @Override - public void onError(Throwable t) { - close(); - log.warn("receive client error:", t); + private boolean readCondition() { + return !this.closed.get() && !this.readOver.get() && this.packages.remainingCapacity() > 0; } - @Override - public void onCompleted() { - close(); + private boolean sendCondition() { + return !this.closed.get() && this.nextSeqNo.get() - this.cltSeqNo.get() < MAX_PAGE; } - private void close() { + private boolean sendWorkAvailable() { + return !this.closed.get() && + (sendCondition() || this.halfClosed.get() || + (this.readOver.get() && this.packages.isEmpty())); + } + + private void startRead() { + if (!readCondition() || !this.reading.compareAndSet(false, true)) { + return; + } try { - nextSeqNo.set(0); - if (sendTask != null) { - sendTask.cancel(true); - } - if (readTask != null) { - readTask.cancel(true); + this.execute.accept(() -> { + try { + synchronized (this.readerLock) { + if (this.closed.get()) { + return; + } + this.reader = Thread.currentThread(); + } + read(); + } catch (RuntimeException | Error failure) { + terminate(failure); + } finally { + synchronized (this.readerLock) { + if (this.reader == Thread.currentThread()) { + this.reader = null; + } + } + this.reading.set(false); + if (readCondition()) { + startRead(); + } + } + }); + } catch (RuntimeException | Error failure) { + this.reading.set(false); + terminate(failure); + } + } + + private void read() { + synchronized (this.iteratorLock) { + while (readCondition()) { + ArrayList data = new ArrayList<>(BATCH_SIZE); + while (!this.closed.get() && this.leftCount > 0 && + data.size() < BATCH_SIZE && this.iter.hasNext()) { + data.add(this.iter.next()); + this.leftCount--; + } + if (this.closed.get()) { + return; + } + boolean ended = this.leftCount == 0 || !this.iter.hasNext(); + if (!data.isEmpty()) { + Descriptors.FieldDescriptor field = ScanResponse.getDescriptor().findFieldByNumber( + this.scanReq.getScanRequest().getScanType() == ScanType.SCAN_VERTEX ? 3 : 4); + this.packages.add(ScanResponse.newBuilder().setHeader(OK) + .setSeqNo(this.nextSeqNo.get()).setField(field, data).build()); + } + if (ended) { + closeIterator(); + this.readOver.set(true); + } + startSend(); } - readOver.set(true); - iter.close(); - } catch (Exception e) { - log.warn("on Complete with error:", e); } } - Runnable rr = new Runnable() { - @Override - public void run() { - try { - if (readCondition()) { - synchronized (iter) { - while (readCondition()) { - Request r = scanReq.getScanRequest(); - ScanType t = r.getScanType(); - boolean isVertex = t.equals(ScanType.SCAN_VERTEX); - ArrayList data = new ArrayList<>(BATCH_SIZE); - int count = 0; - while (iter.hasNext() && leftCount > -1) { - count++; - leftCount--; - T next = (T) iter.next(); - data.add(next); - if (count >= BATCH_SIZE) { - offer(data, isVertex); - // data.clear(); - break; - } - } - if (!(iter.hasNext() && leftCount > -1)) { - if (data.size() > 0 && - data.size() < BATCH_SIZE) { - offer(data, isVertex); - } - readOver.set(true); - data = null; - //log.warn("scan complete , count: {},time: {}", - // sum, System.currentTimeMillis()); - return; + private void startSend() { + if (!sendWorkAvailable() || !this.sending.compareAndSet(false, true)) { + return; + } + try { + this.execute.accept(() -> { + try { + while (!this.closed.get()) { + if (this.readOver.get() && this.packages.isEmpty()) { + terminate(null); + return; + } + if (!sendCondition()) { + // A half-closed request stream cannot grant further page credit. + if (this.halfClosed.get() && !this.packages.isEmpty()) { + terminate(Status.FAILED_PRECONDITION.withDescription( + "Partition scan needs more response credit after client half-close") + .asRuntimeException()); } + return; } + ScanResponse response = this.packages.poll(10, TimeUnit.MILLISECONDS); + if (response != null) { + this.sender.onNext(response.toBuilder().setSeqNo(this.nextSeqNo.get()).build()); + this.nextSeqNo.incrementAndGet(); + startRead(); + } else if (this.readOver.get() && this.packages.isEmpty()) { + terminate(null); + return; + } else { + return; + } + } + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + terminate(failure); + } catch (RuntimeException | Error failure) { + terminate(failure); + } finally { + this.sending.set(false); + if (sendWorkAvailable() && (!this.packages.isEmpty() || this.readOver.get())) { + startSend(); } } - } catch (Exception e) { - log.warn("read data with error: ", e); - sender.onError(e); - } + }); + } catch (RuntimeException | Error failure) { + this.sending.set(false); + terminate(failure); } - }; + } - Runnable sr = () -> { - while (sendCondition()) { - ScanResponse response; + private void closeIterator() { + synchronized (this.iteratorLock) { + if (this.iter == null || this.iteratorClosed) { + return; + } + this.iteratorClosed = true; try { - if (readOver.get()) { - if ((response = packages.poll()) == null) { - sender.onCompleted(); - } else { - sender.onNext(response); - nextSeqNo.incrementAndGet(); - } - } else { - response = packages.poll(10, - TimeUnit.MILLISECONDS); - if (response != null) { - sender.onNext(response); - nextSeqNo.incrementAndGet(); - startRead(); - } else { - break; - } - } + this.iter.close(); + } catch (RuntimeException | Error failure) { + this.failedCleanup.accept(failure); + throw failure; + } + } + } - } catch (InterruptedException e) { - break; + private void terminate(Throwable failure) { + if (!this.closed.compareAndSet(false, true)) { + return; + } + this.readOver.set(true); + this.packages.clear(); + if (failure != null) { + synchronized (this.readerLock) { + // Keep ownership protected until interrupt is issued; the pool may reuse this thread. + if (this.reader != null && this.reader != Thread.currentThread()) { + this.reader.interrupt(); + } + } + } + try { + closeIterator(); + } catch (RuntimeException | Error cleanup) { + if (failure == null) { + failure = cleanup; + } else if (failure != cleanup) { + failure.addSuppressed(cleanup); } } - }; + if (failure == null) { + this.sender.onCompleted(); + } else { + this.sender.onError(failure); + } + } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/ContextClosedListener.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/ContextClosedListener.java index df8d084e22..03b6443f87 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/ContextClosedListener.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/ContextClosedListener.java @@ -17,106 +17,128 @@ package org.apache.hugegraph.store.node.listener; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadPoolExecutor; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; -import org.apache.hugegraph.store.HgStoreEngine; +import org.apache.hugegraph.store.node.grpc.GrpcShutdownBarrier; import org.apache.hugegraph.store.node.grpc.HgStoreStreamImpl; +import org.apache.hugegraph.store.node.grpc.query.AggregativeQueryService; import org.apache.hugegraph.store.node.task.TTLCleaner; +import org.lognet.springboot.grpc.context.GRpcServerInitializedEvent; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import com.alipay.sofa.jraft.Status; -import com.alipay.sofa.jraft.entity.PeerId; - +import io.grpc.Server; import lombok.extern.slf4j.Slf4j; @Slf4j @Service public class ContextClosedListener implements ApplicationListener { + private final List grpcServers = new CopyOnWriteArrayList<>(); + @Autowired HgStoreStreamImpl storeStream; @Autowired + AggregativeQueryService queryService; + @Autowired TTLCleaner cleaner; + @Autowired + GrpcShutdownBarrier grpcBarrier; + + @EventListener + public void onServerInitialized(GRpcServerInitializedEvent event) { + this.grpcServers.add(event.getServer()); + } @Override public void onApplicationEvent(ContextClosedEvent event) { + // Spring invokes HgStoreNodeService.destroy() after this event. Raft and + // its databases must remain available until local workers have stopped. + this.grpcBarrier.stopAcceptingCalls(); + if (storeStream != null) { + storeStream.stopAcceptingScans(); + } + if (queryService != null) { + queryService.stopAcceptingQueries(); + } + this.grpcServers.forEach(Server::shutdownNow); + if (cleaner != null) { + // The scheduler can create the worker pool while a job is starting. + stopAndWait(cleaner.getScheduler(), "TTL scheduler"); + stopAndWait(cleaner.getExecutor(), "TTL workers"); + } + if (storeStream != null) { + // Cancelled queued scans must run their finally blocks to release iterators. + storeStream.shutdownScans(); + awaitWorkers(storeStream.getRealExecutor(), "scan workers"); + } + if (queryService != null) { + queryService.shutdownQueries(); + awaitWorkers(queryService.getThreadPool(), "aggregate query workers"); + } + boolean interrupted = false; try { - try { - transferLeaders(); - - synchronized (ContextClosedListener.class) { - ContextClosedListener.class.wait(60 * 1000); - } - - transferLeaders(); - - synchronized (ContextClosedListener.class) { - ContextClosedListener.class.wait(30 * 1000); - } - } catch (Exception e) { - log.info("shutdown hook: ", e); - } - - log.info("closing scan threads...."); - if (storeStream != null) { - ThreadPoolExecutor executor = storeStream.getRealExecutor(); - if (executor != null) { + for (Server server : this.grpcServers) { + while (!server.isTerminated()) { try { - executor.shutdownNow(); - } catch (Exception e) { + if (!server.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("Still waiting for gRPC callbacks before closing databases"); + } + } catch (InterruptedException e) { + interrupted = true; } } } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + this.grpcBarrier.awaitCallbacks(); + if (storeStream != null) { + storeStream.awaitScanCleanup(); + } + if (cleaner != null) { + cleaner.awaitCleanup(); + } + log.info("closed gRPC callbacks, scan, aggregate query and TTL workers"); + } - if (cleaner != null) { - ThreadPoolExecutor cleanerExecutor = cleaner.getExecutor(); - if (cleanerExecutor != null) { - try { - cleanerExecutor.shutdownNow(); - } catch (Exception e) { - - } - } - ScheduledExecutorService scheduler = cleaner.getScheduler(); - if (scheduler != null) { - try { - scheduler.shutdownNow(); - } catch (Exception e) { + private static void stopAndWait(ExecutorService executor, String name) { + if (executor == null) { + return; + } + executor.shutdownNow(); + awaitWorkers(executor, name); + } + private static void awaitWorkers(ExecutorService executor, String name) { + if (executor == null) { + return; + } + boolean interrupted = false; + try { + while (!executor.isTerminated()) { + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + log.warn("Still waiting for {} to stop before closing databases", name); } + } catch (InterruptedException e) { + // An interrupted shutdown thread must not close databases underneath + // a worker that still owns a native iterator. + interrupted = true; } } - } catch (Exception e) { - log.error("ContextClosedListener: ", e); } finally { - log.info("closed scan threads"); - } - } - - private void transferLeaders() { - try { - HgStoreEngine.getInstance().getLeaderPartition() - .forEach(leader -> { - try { - Status status = - leader.getRaftNode().transferLeadershipTo(PeerId.ANY_PEER); - log.info("partition {} transfer leader status: {}", - leader.getGroupId(), status); - } catch (Exception e) { - log.info("partition {} transfer leader error: ", - leader.getGroupId(), e); - } - }); - HgStoreEngine.getInstance().getPartitionEngines().forEach( - ((integer, partitionEngine) -> partitionEngine.getRaftNode() - .shutdown()) - ); - } catch (Exception e) { - log.error("transfer leader failed: " + e.getMessage()); + if (interrupted) { + Thread.currentThread().interrupt(); + } } } } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/PdConfigureListener.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/PdConfigureListener.java index 1087bb0f2b..070d3c15ee 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/PdConfigureListener.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/listener/PdConfigureListener.java @@ -39,7 +39,9 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertiesPropertySource; +import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; import com.google.common.base.Charsets; @@ -125,7 +127,7 @@ private void updatePdConfig(MutablePropertySources sources, KvClient client, } private Properties getYmlConfig(String yml) { - Yaml yaml = new Yaml(); + Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); Iterable load = yaml.loadAll(yml); Iterator iterator = load.iterator(); Properties properties = new Properties(); diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/metrics/RocksDBMetricsConst.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/metrics/RocksDBMetricsConst.java index 94bdc4c6bc..ceecc43399 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/metrics/RocksDBMetricsConst.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/metrics/RocksDBMetricsConst.java @@ -54,12 +54,12 @@ public final class RocksDBMetricsConst { TickerType.BLOCK_CACHE_INDEX_HIT, // Index cache hits. TickerType.BLOCK_CACHE_INDEX_ADD, // Index blocks added to cache. TickerType.BLOCK_CACHE_INDEX_BYTES_INSERT, // Bytes inserted into index cache. - TickerType.BLOCK_CACHE_INDEX_BYTES_EVICT, // Bytes evicted from index cache. + // BLOCK_CACHE_INDEX_BYTES_EVICT removed in RocksDB 8.x TickerType.BLOCK_CACHE_FILTER_MISS, // Filter cache misses. TickerType.BLOCK_CACHE_FILTER_HIT, // Filter cache hits. TickerType.BLOCK_CACHE_FILTER_ADD, // Filter blocks added to cache. TickerType.BLOCK_CACHE_FILTER_BYTES_INSERT, // Bytes inserted in filter cache. - TickerType.BLOCK_CACHE_FILTER_BYTES_EVICT, // Bytes evicted from filter cache. + // BLOCK_CACHE_FILTER_BYTES_EVICT removed in RocksDB 8.x TickerType.BLOCK_CACHE_DATA_MISS, // Data cache misses. TickerType.BLOCK_CACHE_DATA_HIT, // Data cache hits. TickerType.BLOCK_CACHE_DATA_ADD, // Data blocks added to cache. @@ -95,31 +95,31 @@ public final class RocksDBMetricsConst { TickerType.NUMBER_DB_NEXT_FOUND, // Number of successful next operations. TickerType.NUMBER_DB_PREV_FOUND, // Number of successful previous operations. TickerType.ITER_BYTES_READ, // Bytes read by iterators. - TickerType.NO_FILE_CLOSES, // Number of file close operations. + // NO_FILE_CLOSES removed in RocksDB 8.x TickerType.NO_FILE_OPENS, // Number of file open operations. TickerType.NO_FILE_ERRORS, // Number of file errors. TickerType.STALL_MICROS, // Time spent in a stall micro. TickerType.DB_MUTEX_WAIT_MICROS, // Time spent waiting on a mutex. - TickerType.RATE_LIMIT_DELAY_MILLIS, // Rate limiting delay in milliseconds. - TickerType.NO_ITERATORS, // Number of iterators created. + // RATE_LIMIT_DELAY_MILLIS removed in RocksDB 8.x + // NO_ITERATORS removed in RocksDB 8.x TickerType.NUMBER_MULTIGET_BYTES_READ, // Bytes read by multi-get operations. TickerType.NUMBER_MULTIGET_KEYS_READ, // Keys read in multi-get operations. TickerType.NUMBER_MULTIGET_CALLS, // Number of multi-get operations. - TickerType.NUMBER_FILTERED_DELETES, // Number of deletes filtered. + // NUMBER_FILTERED_DELETES removed in RocksDB 8.x TickerType.NUMBER_MERGE_FAILURES, // Number of merge failures. TickerType.BLOOM_FILTER_PREFIX_CHECKED, // Number of prefix bloom filter checks. TickerType.BLOOM_FILTER_PREFIX_USEFUL, // Number of useful prefix bloom filter checks. TickerType.NUMBER_OF_RESEEKS_IN_ITERATION, // Number of reseeks in iteration. TickerType.GET_UPDATES_SINCE_CALLS, // Number of get updates since calls. - TickerType.BLOCK_CACHE_COMPRESSED_MISS, // Misses in compressed block cache. - TickerType.BLOCK_CACHE_COMPRESSED_HIT, // Hits in compressed block cache. - TickerType.BLOCK_CACHE_COMPRESSED_ADD, // Compressed blocks added to cache. - TickerType.BLOCK_CACHE_COMPRESSED_ADD_FAILURES, // Failures adding compressed blocks. + // BLOCK_CACHE_COMPRESSED_MISS removed in RocksDB 8.x + // BLOCK_CACHE_COMPRESSED_HIT removed in RocksDB 8.x + // BLOCK_CACHE_COMPRESSED_ADD removed in RocksDB 8.x + // BLOCK_CACHE_COMPRESSED_ADD_FAILURES removed in RocksDB 8.x TickerType.WAL_FILE_SYNCED, // Number of synced WAL files. TickerType.WAL_FILE_BYTES, // Bytes written to WAL files. TickerType.WRITE_DONE_BY_SELF, // Writes completed by self. TickerType.WRITE_DONE_BY_OTHER, // Writes completed by others. - TickerType.WRITE_TIMEDOUT, // Number of write timeouts. + // WRITE_TIMEDOUT removed in RocksDB 8.x TickerType.WRITE_WITH_WAL, // Writes involving WAL. TickerType.COMPACT_READ_BYTES, // Bytes read during compaction. TickerType.COMPACT_WRITE_BYTES, // Bytes written during compaction. @@ -167,12 +167,10 @@ public final class RocksDBMetricsConst { // Time spent reading blocks during compaction. HistogramType.READ_BLOCK_GET_MICROS, // Time spent reading blocks during get. HistogramType.WRITE_RAW_BLOCK_MICROS, // Time spent writing raw blocks. - HistogramType.STALL_L0_SLOWDOWN_COUNT, // Count of stalls due to L0 slowdown. - HistogramType.STALL_MEMTABLE_COMPACTION_COUNT, - // Count of stalls due to memtable compaction. - HistogramType.STALL_L0_NUM_FILES_COUNT, // Count of stalls due to number of files at L0. - HistogramType.HARD_RATE_LIMIT_DELAY_COUNT, // Count of delays due to hard rate limits. - HistogramType.SOFT_RATE_LIMIT_DELAY_COUNT, // Count of delays due to soft rate limits. + // STALL_L0_SLOWDOWN_COUNT removed in RocksDB 8.x + // Note: The following constants were removed in RocksDB 8.10.2: + // STALL_MEMTABLE_COMPACTION_COUNT, STALL_L0_NUM_FILES_COUNT, + // HARD_RATE_LIMIT_DELAY_COUNT, SOFT_RATE_LIMIT_DELAY_COUNT HistogramType.NUM_FILES_IN_SINGLE_COMPACTION, // Number of files in a single compaction. HistogramType.DB_SEEK, // Latency of database seek operations. HistogramType.WRITE_STALL, // Time spent in write stalls. diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/task/TTLCleaner.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/task/TTLCleaner.java index 6836fe20b7..ee25a0e1c6 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/task/TTLCleaner.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/task/TTLCleaner.java @@ -33,6 +33,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import org.apache.commons.lang3.ArrayUtils; @@ -93,11 +94,11 @@ public class TTLCleaner implements Runnable { private ThreadPoolExecutor executor; private final Set failedPartitions = Sets.newConcurrentHashSet(); private final ScheduledFuture future; - private final String key = "HUGEGRAPH/hg/EXPIRED"; private final DirectBinarySerializer serializer = new DirectBinarySerializer(); @Autowired private HgStoreNodeService service; private final AtomicBoolean running = new AtomicBoolean(false); + private final AtomicReference cleanupFailure = new AtomicReference<>(); private final AppConfig appConfig; private final AppConfig.JobConfig jobConfig; @@ -127,24 +128,27 @@ public void submit() { scheduler.submit(this); } - public BiFunction getJudge(String table) { + public BiFunction getJudge(String graph, String table) { try { switch (table) { case HugeServerTables.VERTEX_TABLE: return (key, value) -> { - DirectHugeElement el = serializer.parseVertex(key, value); + DirectHugeElement el = serializer.parseSchemaVertex( + BusinessHandlerImpl.getGraphSupplier(graph), key, value); return predicate(el); }; case HugeServerTables.OUT_EDGE_TABLE: case HugeServerTables.IN_EDGE_TABLE: return (key, value) -> { - DirectHugeElement el = serializer.parseEdge(key, value); + DirectHugeElement el = serializer.parseSchemaEdge( + BusinessHandlerImpl.getGraphSupplier(graph), key, value); return predicate(el); }; case HugeServerTables.INDEX_TABLE: return (key, value) -> { - DirectHugeElement el = serializer.parseIndex(key, value); + DirectHugeElement el = serializer.parseSchemaIndex( + BusinessHandlerImpl.getGraphSupplier(graph), key, value); return predicate(el); }; default: @@ -182,7 +186,7 @@ public void run() { config.setAuthority(DefaultPdProvider.name, DefaultPdProvider.authority); client = new KvClient(config); } - KResponse k = client.get(key); + KResponse k = client.get("HUGEGRAPH/" + appConfig.getPdCluster() + "/EXPIRED"); String g = k.getValue(); log.info("cleaner config:{}", jobConfig); @@ -283,14 +287,15 @@ private Runnable getTask( String table = t.getRight(); TaskInfo taskInfo = counter.get(graph); ScanIterator scan = null; + RocksDBSession session = null; try { Map graphCounter = taskInfo.getTableCounter(); TaskSubmitter submitter = taskInfo.getTaskSubmitter(); AtomicLong tableCounter = graphCounter.get(table); - RocksDBSession session = handler.getSession(id); + session = handler.getSession(id); InnerKeyCreator keyCreator = handler.getKeyCreator(); SessionOperator op = session.sessionOp(); - BiFunction judge = getJudge(table); + BiFunction judge = getJudge(graph, table); scan = op.scan(table, keyCreator.getStartKey(id, graph), keyCreator.getEndKey(id, graph), @@ -299,7 +304,7 @@ private Runnable getTask( LinkedList all = new LinkedList<>(); AtomicBoolean state = new AtomicBoolean(true); AtomicLong partitionCounter = pc.get(id); - while (filter.hasNext() && state.get()) { + while (!Thread.currentThread().isInterrupted() && state.get() && filter.hasNext()) { RocksDBSession.BackendColumn current = filter.next(); byte[] realKey = Arrays.copyOfRange(current.name, 0, current.name.length - Short.BYTES); @@ -313,7 +318,7 @@ private Runnable getTask( all = new LinkedList<>(); } } - if (all.size() > 0 && state.get()) { + if (!Thread.currentThread().isInterrupted() && all.size() > 0 && state.get()) { submitter.submitClean(id, graph, table, all, state, tableCounter, partitionCounter); } @@ -324,14 +329,59 @@ private Runnable getTask( String msg = String.format(s, id, graph, table); log.error(msg, e); } finally { - latch.countDown(); - if (scan != null) { - scan.close(); + try { + if (scan != null) { + scan.close(); + } + } catch (RuntimeException | Error failure) { + recordCleanupFailure(failure); + } finally { + try { + if (session != null) { + session.close(); + } + } catch (RuntimeException | Error failure) { + recordCleanupFailure(failure); + } finally { + latch.countDown(); + } } } }; } + private void recordCleanupFailure(Throwable failure) { + this.cleanupFailure.compareAndSet(null, failure); + Throwable first = this.cleanupFailure.get(); + if (first != failure) { + first.addSuppressed(failure); + } + log.error("TTL native cleanup failed; database close stays blocked", failure); + } + + /** Call after workers terminate: a failed native release must remain a shutdown blocker. */ + public void awaitCleanup() { + boolean interrupted = false; + long nextLog = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + synchronized (this.cleanupFailure) { + while (this.cleanupFailure.get() != null) { + try { + this.cleanupFailure.wait(5000L); + } catch (InterruptedException e) { + interrupted = true; + } + if (System.nanoTime() - nextLog >= 0) { + log.warn("TTL scan cleanup failed; database close stays blocked", + this.cleanupFailure.get()); + nextLog = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + } + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + public ScheduledFuture getFuture() { return future; } diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/util/HgChannel.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/util/HgChannel.java index c5b81fe5ca..debca4bea3 100644 --- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/util/HgChannel.java +++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/util/HgChannel.java @@ -37,6 +37,8 @@ @Slf4j public final class HgChannel { + // Recheck close without interrupting a borrowed caller thread. + private static final long CLOSE_POLL_NANOS = TimeUnit.MILLISECONDS.toNanos(100); private final BlockingQueue> queue; private final AtomicBoolean closed = new AtomicBoolean(false); private final long timeoutSeconds; @@ -69,14 +71,27 @@ public boolean send(T t) { if (this.closed.get()) { return false; } - boolean flag; + long timeoutNanos = Math.max(0L, TimeUnit.SECONDS.toNanos(this.timeoutSeconds)); + long started = System.nanoTime(); + long remaining = timeoutNanos; + Supplier supplier = () -> t; try { - flag = this.queue.offer(() -> t, timeoutSeconds, TimeUnit.SECONDS); + do { + if (this.closed.get()) { + return false; + } + if (this.queue.offer(supplier, Math.max(0L, Math.min(remaining, CLOSE_POLL_NANOS)), + TimeUnit.NANOSECONDS)) { + return true; + } + remaining = timeoutNanos - (System.nanoTime() - started); + } while (remaining > 0L); + return false; } catch (InterruptedException e) { + Thread.currentThread().interrupt(); log.error("failed to send a item to chan. cause by: ", t); throw new RuntimeException(e); } - return flag; } } @@ -105,11 +120,29 @@ public T receive(Consumer timeoutCallBack) { if (this.closed.get()) { s = this.queue.poll(); } else { + long timeoutNanos = Math.max(0L, TimeUnit.SECONDS.toNanos(this.timeoutSeconds)); + long started = System.nanoTime(); + long remaining = timeoutNanos; + s = null; try { - s = this.queue.poll(timeoutSeconds, TimeUnit.SECONDS); - } catch (Throwable t) { - log.error("Failed to receive a item from chan. cause by: ", t); - throw new RuntimeException(t); + do { + if (this.closed.get()) { + return null; + } + s = this.queue.poll(Math.max(0L, Math.min(remaining, CLOSE_POLL_NANOS)), + TimeUnit.NANOSECONDS); + if (s != null) { + break; + } + remaining = timeoutNanos - (System.nanoTime() - started); + } while (remaining > 0L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Failed to receive a item from chan. cause by: ", e); + throw new RuntimeException(e); + } + if (s == null && this.closed.get()) { + return null; } if (s == null) { if (timeoutCallBack == null) { @@ -136,14 +169,7 @@ public boolean isClosed() { * @throws RuntimeException when fail to close the chan */ public void close() { - if (this.closed.get()) { - return; - } this.closed.set(true); - this.queue.offer(() -> null); - Thread.yield(); - this.queue.poll(); - } } diff --git a/hugegraph-store/hg-store-node/src/main/resources/application.yml b/hugegraph-store/hg-store-node/src/main/resources/application.yml index 0b65270608..9b70aa2c6f 100644 --- a/hugegraph-store/hg-store-node/src/main/resources/application.yml +++ b/hugegraph-store/hg-store-node/src/main/resources/application.yml @@ -49,3 +49,6 @@ logging: config: classpath:log4j2-dev.xml level: root: info + + +# HTTP Arthas activation is disabled. Use bin/arthas.sh locally without restarting Store. diff --git a/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/business/StoredRowIngressTest.java b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/business/StoredRowIngressTest.java new file mode 100644 index 0000000000..a8eb6c8761 --- /dev/null +++ b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/business/StoredRowIngressTest.java @@ -0,0 +1,614 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.store.business; + +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.HashMap; +import java.nio.ByteBuffer; +import java.util.Properties; +import java.util.Set; + +import org.apache.hugegraph.HugeGraphSupplier; +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.pd.grpc.Metapb; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.rocksdb.access.RocksDBSession; +import org.apache.hugegraph.rocksdb.access.ScanIterator; +import org.apache.hugegraph.serializer.BinaryElementSerializer; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.DirectBinarySerializer; +import org.apache.hugegraph.serializer.OlapKey; +import org.apache.hugegraph.store.client.query.QueryExecutor; +import org.apache.hugegraph.store.cmd.HgCmdClient; +import org.apache.hugegraph.store.cmd.HgCmdProcessor; +import org.apache.hugegraph.store.cmd.request.BatchPutRequest; +import org.apache.hugegraph.store.cmd.response.BatchPutResponse; +import org.apache.hugegraph.store.grpc.common.Kv; +import org.apache.hugegraph.store.grpc.query.QueryRequest; +import org.apache.hugegraph.store.grpc.query.QueryResultFormat; +import org.apache.hugegraph.store.grpc.query.ScanType; +import org.apache.hugegraph.store.HgStoreEngine; +import org.apache.hugegraph.store.node.grpc.query.QueryUtil; +import org.apache.hugegraph.store.node.grpc.query.model.PipelineResult; +import org.apache.hugegraph.store.node.grpc.query.stages.TtlCheckStage; +import org.apache.hugegraph.store.node.grpc.query.stages.OlapStage; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.store.util.HgStoreException; +import org.apache.hugegraph.structure.BaseElement; +import org.apache.hugegraph.structure.BaseVertex; +import org.apache.hugegraph.structure.BaseEdge; +import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.structure.builder.IndexBuilder; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.EdgeLabelType; +import org.apache.hugegraph.type.define.Frequency; +import org.apache.hugegraph.type.define.IndexType; +import org.apache.hugegraph.type.define.WriteType; +import org.apache.hugegraph.util.Bytes; +import org.junit.Assert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.google.protobuf.ByteString; + +public class StoredRowIngressTest { + + private static final String FIXTURE = "compatibility/2f827d6e8/core-rows.properties"; + private final BinaryElementSerializer serializer = new BinaryElementSerializer(); + private final DirectBinarySerializer directSerializer = new DirectBinarySerializer(); + private Properties rows; + private HugeGraphSupplier graph; + + @BeforeEach + public void setUp() throws Exception { + this.rows = new Properties(); + try (InputStream input = getClass().getClassLoader().getResourceAsStream(FIXTURE)) { + Assert.assertNotNull("Frozen baseline BinarySerializer rows must be present", input); + this.rows.load(input); + } + this.graph = Mockito.mock(HugeGraphSupplier.class); + Mockito.when(this.graph.configuration()).thenReturn(new HugeConfig(Map.of())); + property(1, Cardinality.SINGLE, WriteType.OLTP); + property(2, Cardinality.LIST, WriteType.OLTP); + property(3, Cardinality.SET, WriteType.OLTP); + property(9, Cardinality.SINGLE, WriteType.OLAP_COMMON); + property(16, DataType.LONG, Cardinality.SINGLE, WriteType.OLTP); + property(17, DataType.TEXT, Cardinality.SINGLE, WriteType.OLTP); + VertexLabel vertex = new VertexLabel(this.graph, id(4), "person"); + vertex.properties(Set.of(id(1), id(2), id(3))); + vertex.ttl(60_000L); + Mockito.when(this.graph.vertexLabelOrNone(id(4))).thenReturn(vertex); + Mockito.when(this.graph.vertexLabel(id(4))).thenReturn(vertex); + EdgeLabel edge = new EdgeLabel(this.graph, id(5), "knows"); + edge.properties(Set.of(id(1), id(2), id(3))); + edge.sourceLabel(id(4)); + edge.targetLabel(id(4)); + edge.frequency(Frequency.MULTIPLE); + edge.ttl(60_000L); + Mockito.when(this.graph.edgeLabelOrNone(id(5))).thenReturn(edge); + Mockito.when(this.graph.edgeLabel(id(5))).thenReturn(edge); + index(6, IndexType.SECONDARY); + index(7, IndexType.RANGE_INT); + index(8, IndexType.RANGE_LONG); + index(15, IndexType.SECONDARY); + } + + @Test + public void testFilterIteratorReadsFrozenServerVertexAndEdges() { + for (String row : List.of("vertex", "edge.out", "edge.in")) { + BackendColumn column = column(row); + RocksDBSession.BackendColumn stored = RocksDBSession.BackendColumn.of(column.name, column.value); + ConditionQuery condition = new ConditionQuery(row.equals("vertex") ? HugeType.VERTEX : HugeType.EDGE); + condition.query(Condition.eq(id(1), 7)); + FilterIterator iterator = new FilterIterator<>(iterator(stored), condition, this.graph); + Assert.assertTrue(row, iterator.hasNext()); + Assert.assertSame(stored, iterator.next()); + Assert.assertFalse(iterator.hasNext()); + iterator.close(); + + ConditionQuery noMatch = new ConditionQuery(condition.resultType()); + noMatch.query(Condition.eq(id(1), 8)); + FilterIterator rejected = new FilterIterator<>(iterator(stored), noMatch, this.graph); + Assert.assertFalse(row, rejected.hasNext()); + rejected.close(); + } + } + + @Test + public void testSchemaLookupFailureClosesAlreadyOpenedFilterIterator() { + ScanIterator source = Mockito.mock(ScanIterator.class); + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.query(Condition.eq(id(1), 7)); + HgStoreEngine engine = HgStoreEngine.getInstance(); + boolean previousClosing = engine.isClosing().getAndSet(false); + try { + FilterIterator.of(source, query.bytes(), "invalid-namespace"); + Assert.fail("Malformed metadata namespace must fail initialization"); + } catch (HgStoreException expected) { + Assert.assertTrue(expected.getMessage().contains("Graph must include")); + Mockito.verify(source).close(); + } finally { + engine.isClosing().set(previousClosing); + } + } + + @Test + public void testSelectionCountsActualMatchesAndPreservesCollectionsAndTtl() { + for (String row : List.of("vertex", "edge.out", "edge.in")) { + BackendColumn column = column(row); + RocksDBSession.BackendColumn stored = RocksDBSession.BackendColumn.of(column.name, column.value); + boolean isVertex = row.equals("vertex"); + // Requested count equals stored count, but one requested ID is absent. + SelectIterator iterator = new SelectIterator(iterator(stored), List.of(2, 3, 99), + this.graph, isVertex); + RocksDBSession.BackendColumn selected = iterator.next(); + Assert.assertArrayEquals(stored.name, selected.name); + BaseElement element = QueryUtil.parseEntry(this.graph, + BackendColumn.of(selected.name, selected.value), isVertex); + Assert.assertEquals(2, element.sizeOfProperties()); + Assert.assertEquals(List.of(7, -1, 7), element.getPropertyValue(id(2))); + Assert.assertEquals(Set.of(-1, 7), element.getPropertyValue(id(3))); + Assert.assertNull(element.getPropertyValue(id(1))); + Assert.assertEquals(expiry(), element.expiredTime()); + iterator.close(); + } + } + + @Test + public void testSelectionOfMissingPropertiesWritesZeroCountAndKeepsTtl() { + BackendColumn column = column("vertex"); + RocksDBSession.BackendColumn stored = RocksDBSession.BackendColumn.of(column.name, column.value); + SelectIterator iterator = new SelectIterator(iterator(stored), List.of(99), this.graph, true); + RocksDBSession.BackendColumn selected = iterator.next(); + BaseVertex vertex = this.serializer.parseSchemaVertex(this.graph, + BackendColumn.of(selected.name, selected.value), null); + Assert.assertEquals(0, vertex.sizeOfProperties()); + Assert.assertEquals(expiry(), vertex.expiredTime()); + iterator.close(); + } + + @Test + public void testOlapMergeKeepsPersistentSchemaFormatAndTtl() { + BackendColumn merged = QueryUtil.combineColumn(this.graph, column("vertex"), List.of(column("olap"))); + BaseVertex vertex = this.serializer.parseSchemaVertex(this.graph, merged, null); + Assert.assertEquals(4, vertex.sizeOfProperties()); + Assert.assertEquals(Integer.valueOf(42), vertex.getPropertyValue(id(9))); + Assert.assertEquals(expiry(), vertex.expiredTime()); + } + + @Test + public void testDirectTtlReaderUsesFrozenRowSchemaAndIndexNameSuffix() { + BackendColumn vertex = column("vertex"); + Assert.assertEquals(expiry(), this.directSerializer.parseSchemaVertex( + this.graph, vertex.name, vertex.value).expiredTime()); + for (String row : List.of("edge.out", "edge.in")) { + BackendColumn edge = column(row); + Assert.assertEquals(expiry(), this.directSerializer.parseSchemaEdge( + this.graph, edge.name, edge.value).expiredTime()); + } + for (String row : List.of("index.secondary", "index.int", "index.long")) { + BackendColumn index = column(row); + Assert.assertEquals(expiry(), this.directSerializer.parseSchemaIndex( + this.graph, index.name, index.value).expiredTime()); + Assert.assertEquals(expiry(), this.serializer.parseSchemaIndex(this.graph, index).expiredTime()); + } + } + + @Test + public void testTtlPipelineAcceptsLiveFixtureAndRejectsExpiredFixture() throws Exception { + Field now = TtlCheckStage.class.getDeclaredField("now"); + now.setAccessible(true); + for (String row : List.of("vertex", "edge.out", "edge.in")) { + TtlCheckStage stage = new TtlCheckStage(); + stage.init(row.equals("vertex"), this.graph); + BackendColumn column = column(row); + PipelineResult input = new PipelineResult(RocksDBSession.BackendColumn.of(column.name, column.value)); + now.setLong(stage, Long.parseLong(this.rows.getProperty("now"))); + Assert.assertSame(row, input, stage.handle(input)); + now.setLong(stage, expiry() + 1); + Assert.assertNull(row, stage.handle(input)); + } + } + + @Test + public void testClientDecodesPersistentRowsAndTaggedElementResponsesExplicitly() throws Exception { + QueryExecutor executor = new QueryExecutor(null, this.graph, 60_000L); + Method decode = QueryExecutor.class.getDeclaredMethod("fromKv", String.class, Kv.class, + boolean.class, boolean.class); + decode.setAccessible(true); + BackendColumn stored = column("vertex"); + BaseVertex persistent = (BaseVertex) decode.invoke(executor, "g+v", kv(stored), false, true); + Assert.assertNotNull(persistent); + Assert.assertEquals(List.of(7, -1, 7), persistent.getPropertyValue(id(2))); + BackendColumn tagged = this.serializer.writeVertex(persistent); + BaseVertex response = (BaseVertex) decode.invoke(executor, "g+v", kv(tagged), false, false); + Assert.assertNotNull(response); + Set expectedGroups = persistent.getPropertyValue(id(3)); + Set actualGroups = response.getPropertyValue(id(3)); + Assert.assertEquals(expectedGroups, actualGroups); + Assert.assertEquals(expiry(), response.expiredTime()); + } + + @Test + public void testUntimedAndSystemRowsDoNotRequireTrailingTtl() { + VertexLabel untimed = new VertexLabel(this.graph, id(10), "untimed"); + untimed.properties(Set.of(id(1), id(2), id(3))); + Mockito.when(this.graph.vertexLabel(id(10))).thenReturn(untimed); + Mockito.when(this.graph.vertexLabelOrNone(id(10))).thenReturn(untimed); + EdgeLabel edge = new EdgeLabel(this.graph, id(11), "untimedEdge"); + edge.properties(Set.of(id(1), id(2), id(3))); + edge.sourceLabel(id(4)); + edge.targetLabel(id(4)); + edge.frequency(Frequency.MULTIPLE); + Mockito.when(this.graph.edgeLabel(id(11))).thenReturn(edge); + Mockito.when(this.graph.edgeLabelOrNone(id(11))).thenReturn(edge); + for (int number : List.of(12, 13, 14)) { + IndexLabel label = new IndexLabel(this.graph, id(number), "untimedIndex" + number); + label.baseType(HugeType.VERTEX_LABEL); + label.baseValue(id(10)); + label.indexType(number == 12 ? IndexType.SECONDARY : + number == 13 ? IndexType.RANGE_INT : IndexType.RANGE_LONG); + label.indexFields(id(number == 12 ? 17 : number == 13 ? 1 : 16)); + Mockito.when(this.graph.indexLabel(id(number))).thenReturn(label); + } + BackendColumn vertex = column("vertex.untimed"); + Assert.assertEquals(0L, this.serializer.parseSchemaVertex(this.graph, vertex, null).expiredTime()); + BackendColumn untimedEdge = column("edge.untimed"); + Assert.assertEquals(0L, this.serializer.parseSchemaEdge(this.graph, untimedEdge, null, true).expiredTime()); + for (String row : List.of("index.secondary.untimed", "index.int.untimed", "index.long.untimed", + "index.system")) { + BackendColumn column = column(row); + Assert.assertEquals(row, 0L, this.serializer.parseSchemaIndex(this.graph, column).expiredTime()); + Assert.assertEquals(row, 0L, this.directSerializer.parseSchemaIndex( + this.graph, column.name, column.value).expiredTime()); + } + } + + @Test + public void testRebuiltIndexWriterMatchesFrozenServerColumnsAndStoreReader() { + BaseVertex vertex = this.serializer.parseSchemaVertex(this.graph, column("vertex"), null); + vertex.addProperty(this.graph.propertyKey(id(16)), 7L); + vertex.addProperty(this.graph.propertyKey(id(17)), "alice"); + IndexBuilder builder = new IndexBuilder(this.graph); + Map fixtures = Map.of(6, "index.secondary", 7, "index.int", 8, "index.long"); + for (Map.Entry fixture : fixtures.entrySet()) { + List indexes = builder.buildIndex(vertex, this.graph.indexLabel(id(fixture.getKey()))); + Assert.assertEquals(1, indexes.size()); + assertPersistentIndex(indexes.get(0), fixture.getValue()); + } + vertex.addProperty(this.graph.propertyKey(id(17)), "long-field-".repeat(20)); + List longString = builder.buildIndex(vertex, this.graph.indexLabel(id(15))); + Assert.assertEquals(1, longString.size()); + assertPersistentIndex(longString.get(0), "index.hashed"); + } + + @Test + public void testSchemaIndexWriterHashingMatchesFrozenServerColumn() { + // Also verify hashing at the serializer boundary without depending on the builder. + // The frozen Server producer supplies the same complete field value. + Index index = new Index(this.graph, this.graph.indexLabel(id(15)), true); + index.fieldValues("long-field-".repeat(20)); + index.elementIds(id(1), expiry()); + assertPersistentIndex(index, "index.hashed"); + } + + @Test + public void testStringIndexBranchesPreserveLongValuesAndCanonicalEscapes() { + BaseVertex vertex = this.serializer.parseSchemaVertex(this.graph, column("vertex"), null); + IndexBuilder builder = new IndexBuilder(this.graph); + String longValue = "long-field-".repeat(20); + for (IndexType type : List.of(IndexType.SECONDARY, IndexType.SHARD, IndexType.UNIQUE)) { + IndexLabel label = new IndexLabel(this.graph, id(20), "stringIndex"); + label.baseType(HugeType.VERTEX_LABEL); + label.baseValue(id(4)); + label.indexType(type); + label.indexFields(id(17)); + vertex.addProperty(this.graph.propertyKey(id(17)), longValue); + Assert.assertEquals(type.name(), longValue, builder.buildIndex(vertex, label).get(0).fieldValues()); + vertex.addProperty(this.graph.propertyKey(id(17)), ""); + Assert.assertEquals(type.name(), "\u0002", builder.buildIndex(vertex, label).get(0).fieldValues()); + vertex.addProperty(this.graph.propertyKey(id(17)), "prefix!tail"); + Assert.assertEquals(type.name(), "prefix`!tail", builder.buildIndex(vertex, label).get(0).fieldValues()); + vertex.addProperty(this.graph.propertyKey(id(17)), "\u0001illegal"); + try { + builder.buildIndex(vertex, label); + Assert.fail("Reserved leading index symbols must remain illegal: " + type); + } catch (IllegalArgumentException expected) { + Assert.assertTrue(expected.getMessage().contains("Illegal leading char")); + } + } + } + + @Test + public void testUniqueNullableFieldUsesCanonicalNullSentinel() { + property(18, DataType.TEXT, Cardinality.SINGLE, WriteType.OLTP); + this.graph.vertexLabel(id(4)).nullableKeys(id(18)); + BaseVertex vertex = this.serializer.parseSchemaVertex(this.graph, column("vertex"), null); + vertex.addProperty(this.graph.propertyKey(id(17)), "prefix"); + IndexLabel label = new IndexLabel(this.graph, id(21), "nullableUnique"); + label.baseType(HugeType.VERTEX_LABEL); + label.baseValue(id(4)); + label.indexType(IndexType.UNIQUE); + label.indexFields(id(17), id(18)); + List indexes = new IndexBuilder(this.graph).buildIndex(vertex, label); + Assert.assertEquals(1, indexes.size()); + Assert.assertEquals("prefix!\u0001", indexes.get(0).fieldValues()); + } + + @Test + public void testFrozenLegacyStoreTtlEnvelopeRemainsReadable() throws Exception { + Properties legacy = new Properties(); + try (InputStream input = getClass().getClassLoader().getResourceAsStream( + "compatibility/2f827d6e8/store-index-rows.properties")) { + Assert.assertNotNull("Frozen baseline Store index writer output must be present", input); + legacy.load(input); + } + for (String row : List.of("secondary", "int", "long", "system")) { + BackendColumn column = BackendColumn.of(Bytes.fromHex(legacy.getProperty(row + ".name")), + Bytes.fromHex(legacy.getProperty(row + ".value"))); + long expectedExpiry = Long.parseLong(legacy.getProperty(row + ".expiry")); + Assert.assertEquals(row, expectedExpiry, + this.serializer.parseSchemaIndex(this.graph, column).expiredTime()); + Assert.assertEquals(row, expectedExpiry, this.directSerializer.parseSchemaIndex( + this.graph, column.name, column.value).expiredTime()); + } + } + + @Test + public void testOlapStageReadsFrozenLegacyRowsWithoutCrossPropertyLeakage() throws Exception { + property(18, Cardinality.SINGLE, WriteType.OLAP_COMMON); + BackendColumn legacy = column("olap"); + Map rows = new HashMap<>(); + rows.put(ByteBuffer.wrap(legacy.name), legacy.value); + for (boolean tagged : List.of(false, true)) { + BaseVertex merged = runOlapStage(rows, tagged, id(9), id(18)); + Assert.assertEquals(42, (Object) merged.getProperty(id(9)).value()); + Assert.assertNull(merged.getProperty(id(18))); + Assert.assertArrayEquals(legacy.value, rows.get(ByteBuffer.wrap(legacy.name))); + Assert.assertEquals(1, rows.size()); + } + } + + @Test + public void testOlapStageMergesMultiplePropertiesAndPrefersNamespacedValues() throws Exception { + property(18, Cardinality.SINGLE, WriteType.OLAP_COMMON); + BackendColumn legacy = column("olap"); + Map rows = new HashMap<>(); + rows.put(ByteBuffer.wrap(legacy.name), legacy.value); + rows.put(ByteBuffer.wrap(OlapKey.format(id(9), id(1))), schemaOlapValue(id(9), 99)); + rows.put(ByteBuffer.wrap(OlapKey.format(id(18), id(1))), schemaOlapValue(id(18), 123)); + for (boolean tagged : List.of(false, true)) { + BaseVertex merged = runOlapStage(rows, tagged, id(9), id(18)); + Assert.assertEquals(99, (Object) merged.getProperty(id(9)).value()); + Assert.assertEquals(123, (Object) merged.getProperty(id(18)).value()); + Assert.assertArrayEquals(legacy.value, rows.get(ByteBuffer.wrap(legacy.name))); + } + } + + private byte[] schemaOlapValue(Id property, int value) { + BaseVertex vertex = new BaseVertex(id(1), VertexLabel.OLAP_VL); + vertex.addProperty(this.graph.propertyKey(property), value); + BytesBuffer buffer = BytesBuffer.allocate(16); + this.serializer.formatSchemaProperty(vertex.getProperty(property), buffer); + return buffer.bytes(); + } + + private BaseVertex runOlapStage(Map rows, boolean tagged, Id... properties) throws Exception { + String graphName = "CODEC_TEST/olap/g"; + BusinessHandler handler = Mockito.mock(BusinessHandler.class); + Mockito.when(handler.doGet(Mockito.eq(graphName), Mockito.anyInt(), Mockito.eq("g+olap"), + Mockito.any(byte[].class))).thenAnswer(call -> + rows.get(ByteBuffer.wrap(call.getArgument(3)))); + Field mock = BusinessHandlerImpl.class.getDeclaredField("mockGraphSupplier"); + mock.setAccessible(true); + HugeGraphSupplier previous = (HugeGraphSupplier) mock.get(null); + BusinessHandlerImpl.setMockGraphSupplier(this.graph); + OlapStage stage = new OlapStage(handler); + try { + List requested = new ArrayList<>(); + for (Id property : properties) { + requested.add(ByteString.copyFrom(BytesBuffer.allocate(16).writeId(property).bytes())); + } + stage.init(graphName, "g+v", requested); + BackendColumn vertex = column("vertex"); + BaseVertex decoded = this.serializer.parseSchemaVertex(this.graph, vertex, null); + PipelineResult input = tagged ? new PipelineResult(decoded) : + new PipelineResult(RocksDBSession.BackendColumn.of(vertex.name, vertex.value)); + Assert.assertSame(input, stage.handle(input)); + if (tagged) { + return (BaseVertex) input.getElement(); + } + return this.serializer.parseSchemaVertex(this.graph, + BackendColumn.of(input.getColumn().name, input.getColumn().value), null); + } finally { + stage.close(); + BusinessHandlerImpl.setMockGraphSupplier(previous); + } + } + + private void assertPersistentIndex(Index rebuilt, String fixture) { + BackendColumn expected = column(fixture); + BackendColumn written = this.serializer.writeSchemaIndex(rebuilt); + // DataManager's persisted batch normalizes an absent payload to zero bytes. + byte[] value = written.value == null ? new byte[0] : written.value; + Assert.assertArrayEquals(fixture, expected.name, written.name); + Assert.assertArrayEquals(fixture, expected.value, value); + Index decoded = this.serializer.parseSchemaIndex(this.graph, BackendColumn.of(written.name, value)); + Assert.assertEquals(rebuilt.elementId(), decoded.elementId()); + Assert.assertEquals(rebuilt.fieldValues(), decoded.fieldValues()); + Assert.assertEquals(expiry(), decoded.expiredTime()); + } + + @Test + public void testParentRequestedRebuildIndexesChildRowsAndExcludesUnrelatedLabels() throws Exception { + EdgeLabel parent = new EdgeLabel(this.graph, id(100), "parent"); + EdgeLabel child = new EdgeLabel(this.graph, id(101), "child"); + child.edgeLabelType(EdgeLabelType.SUB); + child.fatherId(parent.id()); + EdgeLabel unrelated = new EdgeLabel(this.graph, id(102), "unrelated"); + for (EdgeLabel label : List.of(parent, child, unrelated)) { + label.properties(Set.of(id(17))); + // Real sublabels persist their own endpoint links; they do not inherit them on read. + label.sourceLabel(id(4)); + label.targetLabel(id(4)); + Assert.assertEquals(id(4), label.sourceLabel()); + Assert.assertEquals(id(4), label.targetLabel()); + Mockito.when(this.graph.edgeLabel(label.id())).thenReturn(label); + Mockito.when(this.graph.edgeLabelOrNone(label.id())).thenReturn(label); + } + IndexLabel index = new IndexLabel(this.graph, id(103), "parentIndex"); + index.baseType(HugeType.EDGE_LABEL); + index.baseValue(parent.id()); + index.indexType(IndexType.SECONDARY); + index.indexFields(id(17)); + Mockito.when(this.graph.indexLabel(index.id())).thenReturn(index); + BaseEdge childEdge = new BaseEdge(new EdgeId(id(1), Directions.OUT, parent.id(), child.id(), "", id(2)), child); + childEdge.isOutEdge(true); + childEdge.addProperty(this.graph.propertyKey(id(17)), "child record"); + BaseEdge otherEdge = new BaseEdge(new EdgeId(id(3), Directions.OUT, unrelated.id(), unrelated.id(), "", id(4)), + unrelated); + otherEdge.isOutEdge(true); + otherEdge.addProperty(this.graph.propertyKey(id(17)), "unrelated record"); + ScanIterator source = Mockito.mock(ScanIterator.class); + Mockito.when(source.hasNext()).thenReturn(true, true, false); + Mockito.when(source.next()).thenReturn(rebuildInput(childEdge), rebuildInput(otherEdge)); + BusinessHandler handler = Mockito.mock(BusinessHandler.class); + String graphName = "CODEC_TEST/rebuild/g"; + Mockito.when(handler.scan(graphName, "g+oe", 0, 65536)).thenReturn(source); + List written = new ArrayList<>(); + HgCmdClient client = Mockito.mock(HgCmdClient.class); + Mockito.when(client.batchPut(Mockito.any(BatchPutRequest.class))).thenAnswer(call -> { + BatchPutRequest request = call.getArgument(0); + written.addAll(request.getEntries()); + BatchPutResponse response = new BatchPutResponse(); + response.setStatus(HgCmdProcessor.Status.OK); + return response; + }); + DataManagerImpl manager = new DataManagerImpl(); + manager.setBusinessHandler(handler); + manager.setCmdClient(client); + Metapb.BuildIndexParam request = Metapb.BuildIndexParam.newBuilder().setGraph(graphName) + .setLabelId(ByteString.copyFrom(BytesBuffer.allocate(8).writeId(parent.id()).bytes())) + .setIsVertexLabel(false) + .setIndexLabel(ByteString.copyFrom(BytesBuffer.allocate(8).writeId(index.id()).bytes())).build(); + Metapb.Partition partition = Metapb.Partition.newBuilder().setId(9999).setStartKey(0).setEndKey(65536).build(); + Field mock = BusinessHandlerImpl.class.getDeclaredField("mockGraphSupplier"); + mock.setAccessible(true); + HugeGraphSupplier previous = (HugeGraphSupplier) mock.get(null); + BusinessHandlerImpl.setMockGraphSupplier(this.graph); + try { + Assert.assertTrue(manager.doBuildIndex(request, partition).isOk()); + Assert.assertEquals(1, written.size()); + BatchPutRequest.KV row = written.get(0); + Index decoded = this.serializer.parseSchemaIndex(this.graph, + BackendColumn.of(row.getKey(), row.getValue())); + Assert.assertEquals(parent.id(), decoded.indexLabel().baseValue()); + Assert.assertEquals(childEdge.id(), decoded.elementId()); + Assert.assertEquals("child record", decoded.fieldValues()); + Mockito.verify(source).close(); + } finally { + BusinessHandlerImpl.setMockGraphSupplier(previous); + } + } + + private RocksDBSession.BackendColumn rebuildInput(BaseEdge edge) { + byte[] name = this.serializer.formatEdgeName(edge); + // Range scans remove the graph prefix but retain the physical hash-code suffix. + return RocksDBSession.BackendColumn.of(Arrays.copyOf(name, name.length + Short.BYTES), + this.serializer.formatSchemaEdgeValue(edge)); + } + + @Test + public void testResponseFormatMatchesDeserializationPlanning() { + QueryRequest.Builder request = QueryRequest.newBuilder().setScanType(ScanType.TABLE_SCAN); + Assert.assertFalse(QueryResultFormat.returnsTaggedElements(request.build())); + request.setCheckTtl(true); + Assert.assertFalse(QueryResultFormat.returnsTaggedElements(request.build())); + request.addProperty(ByteString.copyFromUtf8("property")); + Assert.assertTrue(QueryResultFormat.requiresElementDeserialization(request.build())); + Assert.assertTrue(QueryResultFormat.returnsTaggedElements(request.build())); + request.clearProperty().addOrderBy(ByteString.copyFromUtf8("order")); + Assert.assertTrue(QueryResultFormat.returnsTaggedElements(request.build())); + request.clearOrderBy().setCondition(ByteString.copyFromUtf8("condition")); + Assert.assertTrue(QueryResultFormat.returnsTaggedElements(request.build())); + } + + private void property(int value, Cardinality cardinality, WriteType writeType) { + property(value, DataType.INT, cardinality, writeType); + } + + private void property(int value, DataType type, Cardinality cardinality, WriteType writeType) { + PropertyKey key = new PropertyKey(this.graph, id(value), "property" + value); + key.dataType(type); + key.cardinality(cardinality); + key.writeType(writeType); + Mockito.when(this.graph.propertyKey(id(value))).thenReturn(key); + } + + private void index(int value, IndexType type) { + IndexLabel label = new IndexLabel(this.graph, id(value), "index" + value); + label.baseType(HugeType.VERTEX_LABEL); + label.baseValue(id(4)); + label.indexType(type); + label.indexFields(id(type == IndexType.RANGE_LONG ? 16 : type == IndexType.SECONDARY ? 17 : 1)); + Mockito.when(this.graph.indexLabel(id(value))).thenReturn(label); + } + + private BackendColumn column(String row) { + return BackendColumn.of(Bytes.fromHex(this.rows.getProperty(row + ".column.0.name")), + Bytes.fromHex(this.rows.getProperty(row + ".column.0.value"))); + } + + private long expiry() { + return Long.parseLong(this.rows.getProperty("expiry")); + } + + private static Id id(int value) { + return IdGenerator.of(value); + } + + private static Kv kv(BackendColumn column) { + return Kv.newBuilder().setKey(ByteString.copyFrom(column.name)) + .setValue(ByteString.copyFrom(column.value)).build(); + } + + private static ScanIterator iterator(RocksDBSession.BackendColumn column) { + ScanIterator iterator = Mockito.mock(ScanIterator.class); + Mockito.when(iterator.hasNext()).thenReturn(true, false); + Mockito.when(iterator.next()).thenReturn(column); + return iterator; + } +} diff --git a/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/ArthasSecurityTest.java b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/ArthasSecurityTest.java new file mode 100644 index 0000000000..057ac1f709 --- /dev/null +++ b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/ArthasSecurityTest.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.hugegraph.store.node.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +public class ArthasSecurityTest { + + private MockMvc mvc; + + @BeforeEach + public void setup() { + // No Store engine or Arthas configuration is needed to reject activation. + this.mvc = MockMvcBuilders.standaloneSetup(new PartitionAPI()).build(); + } + + @Test + public void testRemoteActivationIsForbidden() throws Exception { + assertForbidden(requestFrom("192.0.2.1")); + } + + @Test + public void testLoopbackActivationIsForbidden() throws Exception { + assertForbidden(requestFrom("127.0.0.1")); + assertForbidden(requestFrom("::1")); + } + + @Test + public void testSameHostProxyCannotActivateArthas() throws Exception { + assertForbidden(requestFrom("127.0.0.1") + .header("Forwarded", "for=192.0.2.1;host=store.example") + .header("X-Forwarded-For", "192.0.2.1")); + } + + @Test + public void testSpoofedLoopbackHeadersCannotActivateArthas() throws Exception { + assertForbidden(requestFrom("192.0.2.1") + .header("Forwarded", "for=127.0.0.1") + .header("X-Forwarded-For", "127.0.0.1")); + } + + @Test + public void testAuthorizationHeaderDoesNotEnableStoreActivation() throws Exception { + assertForbidden(requestFrom("127.0.0.1") + .header(HttpHeaders.AUTHORIZATION, "Bearer admin")); + } + + private static MockHttpServletRequestBuilder requestFrom(String address) { + return get("/v1/arthasstart").with(request -> { + request.setRemoteAddr(address); + return request; + }); + } + + private void assertForbidden(MockHttpServletRequestBuilder request) throws Exception { + this.mvc.perform(request) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.message").value( + "HTTP Arthas activation is disabled; run bin/arthas.sh on the Store host")); + } +} diff --git a/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/FixGraphIdControllerTest.java b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/FixGraphIdControllerTest.java new file mode 100644 index 0000000000..2a14fe31ac --- /dev/null +++ b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/FixGraphIdControllerTest.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.hugegraph.store.node.controller; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.hugegraph.store.util.HgStoreException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class FixGraphIdControllerTest { + + @Test + public void testUpdateGraphIdRejectsIdsOutsideAllocatableDomain() { + FixGraphIdController controller = new FixGraphIdController(); + long[] invalidGraphIds = {-1L, 65534L, 65536L}; + + for (long graphId : invalidGraphIds) { + String graphName = "invalid-" + graphId; + Map graphIds = + Collections.singletonMap(graphName, graphId); + + HgStoreException exception = Assertions.assertThrows( + HgStoreException.class, + () -> controller.updateGraphId(0, graphIds)); + Assertions.assertTrue(exception.getMessage().contains("Invalid graph ID")); + Assertions.assertTrue(exception.getMessage().contains(String.valueOf(graphId))); + Assertions.assertTrue(exception.getMessage().contains(graphName)); + } + } + + @Test + public void testUpdateGraphIdRejectsDuplicateIds() { + FixGraphIdController controller = new FixGraphIdController(); + Map graphIds = new LinkedHashMap<>(); + graphIds.put("graph-a", 5L); + graphIds.put("graph-b", 5L); + + HgStoreException exception = Assertions.assertThrows( + HgStoreException.class, + () -> controller.updateGraphId(0, graphIds)); + Assertions.assertTrue(exception.getMessage().contains("graph-a")); + Assertions.assertTrue(exception.getMessage().contains("graph-b")); + Assertions.assertTrue(exception.getMessage().contains("5")); + } +} diff --git a/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/listener/PdConfigureListenerYamlTest.java b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/listener/PdConfigureListenerYamlTest.java new file mode 100644 index 0000000000..b54ce9efc0 --- /dev/null +++ b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/listener/PdConfigureListenerYamlTest.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.hugegraph.store.node.listener; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.env.YamlPropertySourceLoader; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.FileSystemResource; +import org.yaml.snakeyaml.error.YAMLException; + +public class PdConfigureListenerYamlTest { + + @Test + public void testFlattenMapsAndMultipleDocuments() throws Exception { + Properties properties = parse("rocksdb:\n max_background_jobs: 8\n" + + "store:\n enabled: true\n peers: [one, two]\n" + + "---\nserver:\n port: 8500\n"); + Assertions.assertEquals(8, properties.get("rocksdb.max_background_jobs")); + Assertions.assertEquals(true, properties.get("store.enabled")); + Assertions.assertEquals(Arrays.asList("one", "two"), properties.get("store.peers")); + Assertions.assertEquals(8500, properties.get("server.port")); + } + + @Test + public void testRejectJavaTypeBeforeConstruction() { + UnexpectedType.constructions = 0; + String tagged = "store: !!" + UnexpectedType.class.getName() + " {}\n"; + InvocationTargetException exception = Assertions.assertThrows( + InvocationTargetException.class, () -> parse(tagged)); + Assertions.assertTrue(exception.getCause() instanceof YAMLException); + Assertions.assertEquals(0, UnexpectedType.constructions); + } + + @Test + public void testBootLoadsShippedApplicationYaml() throws Exception { + List> sources = new YamlPropertySourceLoader().load( + "store", new FileSystemResource( + "../hg-store-dist/src/assembly/static/conf/application.yml")); + Assertions.assertFalse(sources.isEmpty()); + Assertions.assertNotNull(sources.get(0).getProperty("pdserver.address")); + } + + @Test + public void testBootKeepsDateLikeValuesAsStrings() throws Exception { + List> sources = new YamlPropertySourceLoader().load( + "config", new ByteArrayResource("date: 2026-10-05\nport: 8500\n".getBytes( + java.nio.charset.StandardCharsets.UTF_8))); + Assertions.assertEquals("2026-10-05", sources.get(0).getProperty("date")); + Assertions.assertEquals(8500, sources.get(0).getProperty("port")); + } + + @Test + public void testBootStartsWithShippedYamlConfiguration() { + SpringApplication application = new SpringApplication(YamlApplication.class); + application.setWebApplicationType(WebApplicationType.NONE); + application.setRegisterShutdownHook(false); + try (ConfigurableApplicationContext context = application.run( + "--spring.config.location=../hg-store-dist/src/assembly/static/conf/application.yml", + "--logging.config=")) { + Assertions.assertNotNull(context.getEnvironment().getProperty("pdserver.address")); + } + } + + @Configuration(proxyBeanMethods = false) + public static class YamlApplication { + } + + private static Properties parse(String yaml) throws Exception { + Method method = PdConfigureListener.class.getDeclaredMethod("getYmlConfig", String.class); + method.setAccessible(true); + return (Properties) method.invoke(new PdConfigureListener(), yaml); + } + + public static class UnexpectedType { + + private static int constructions; + + public UnexpectedType() { + constructions++; + } + } +} diff --git a/hugegraph-store/hg-store-node/src/test/resources/compatibility/2f827d6e8/README.md b/hugegraph-store/hg-store-node/src/test/resources/compatibility/2f827d6e8/README.md new file mode 100644 index 0000000000..a195c50a35 --- /dev/null +++ b/hugegraph-store/hg-store-node/src/test/resources/compatibility/2f827d6e8/README.md @@ -0,0 +1,22 @@ + + +# Historical fixtures for Store ingress tests + +`StoredRowIngressTest` loads `core-rows.properties` for complete legacy Server rows (properties, TTL, OLAP and indexes), and `store-index-rows.properties` for original Store index-expiry envelopes. + +These byte-identical copies allow tests to load fixtures from this module's own resource classpath. Keep them synchronized with the shared resources. The [canonical fixture guide](../../../../../../../hugegraph-struct/src/test/resources/compatibility/2f827d6e8/README.md) contains the pinned source revision, historical producers, regeneration instructions, compatibility limits and SHA-256 checksums. Regenerate against those historical writers only, never the current writer. diff --git a/hugegraph-store/hg-store-node/src/test/resources/compatibility/2f827d6e8/core-rows.properties b/hugegraph-store/hg-store-node/src/test/resources/compatibility/2f827d6e8/core-rows.properties new file mode 100644 index 0000000000..fd489e58c1 --- /dev/null +++ b/hugegraph-store/hg-store-node/src/test/resources/compatibility/2f827d6e8/core-rows.properties @@ -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. + # +now=1609459200000 +expiry=1609459260000 +vertex.type=VERTEX +vertex.id=0801 +vertex.ttl=60000 +vertex.columns=1 +vertex.column.0.name=0801 +vertex.column.0.value=08040301070203078fffffff7f0703028fffffff7f07aeebd9fdb460 +edge.out.type=EDGE_OUT +edge.out.id=08018208050805323032362178000802 +edge.out.ttl=60000 +edge.out.columns=1 +edge.out.column.0.name=08018208050805323032362178000802 +edge.out.column.0.value=0301070203078fffffff7f0703028fffffff7f07aeebd9fdb460 +edge.in.type=EDGE_IN +edge.in.id=08028c08050805323032362178000801 +edge.in.ttl=60000 +edge.in.columns=1 +edge.in.column.0.name=08028c08050805323032362178000801 +edge.in.column.0.value=0301070203078fffffff7f0703028fffffff7f07aeebd9fdb460 +olap.type=OLAP +olap.id=0801 +olap.ttl=0 +olap.columns=1 +olap.column.0.name=0801 +olap.column.0.value=092a +index.secondary.type=SECONDARY_INDEX +index.secondary.id=53493a363a616c69636500 +index.secondary.ttl=60000 +index.secondary.columns=1 +index.secondary.column.0.name=53493a363a616c696365000801aeebd9fdb460 +index.secondary.column.0.value= +index.int.type=RANGE_INT_INDEX +index.int.id=a00000000780000007 +index.int.ttl=60000 +index.int.columns=1 +index.int.column.0.name=a000000007800000070801aeebd9fdb460 +index.int.column.0.value= +index.long.type=RANGE_LONG_INDEX +index.long.id=a2000000088000000000000007 +index.long.ttl=60000 +index.long.columns=1 +index.long.column.0.name=a20000000880000000000000070801aeebd9fdb460 +index.long.column.0.value= +vertex.untimed.type=VERTEX +vertex.untimed.id=0803 +vertex.untimed.ttl=0 +vertex.untimed.columns=1 +vertex.untimed.column.0.name=0803 +vertex.untimed.column.0.value=080a0301070203078fffffff7f0703028fffffff7f07 +edge.untimed.type=EDGE_OUT +edge.untimed.id=080182080b080b000802 +edge.untimed.ttl=0 +edge.untimed.columns=1 +edge.untimed.column.0.name=080182080b080b000802 +edge.untimed.column.0.value=0301070203078fffffff7f0703028fffffff7f07 +index.secondary.untimed.type=SECONDARY_INDEX +index.secondary.untimed.id=53493a433a616c69636500 +index.secondary.untimed.ttl=0 +index.secondary.untimed.columns=1 +index.secondary.untimed.column.0.name=53493a433a616c696365000801 +index.secondary.untimed.column.0.value= +index.int.untimed.type=RANGE_INT_INDEX +index.int.untimed.id=a00000000d80000007 +index.int.untimed.ttl=0 +index.int.untimed.columns=1 +index.int.untimed.column.0.name=a00000000d800000070801 +index.int.untimed.column.0.value= +index.long.untimed.type=RANGE_LONG_INDEX +index.long.untimed.id=a20000000e8000000000000007 +index.long.untimed.ttl=0 +index.long.untimed.columns=1 +index.long.untimed.column.0.name=a20000000e80000000000000070801 +index.long.untimed.column.0.value= +index.hashed.type=SECONDARY_INDEX +index.hashed.id=53493a463a323234303534613200 +index.hashed.ttl=60000 +index.hashed.columns=1 +index.hashed.column.0.name=53493a463a3232343035346132000801aeebd9fdb460 +index.hashed.column.0.value=6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d +index.system.type=VERTEX_LABEL_INDEX +index.system.id=56493a2d313a3400 +index.system.ttl=0 +index.system.columns=1 +index.system.column.0.name=56493a2d313a34000801 +index.system.column.0.value= +index.system.edge.type=EDGE_LABEL_INDEX +index.system.edge.id=45493a2d323a3500 +index.system.edge.ttl=0 +index.system.edge.columns=1 +index.system.edge.column.0.name=45493a2d323a35007e08018208050805323032362178000802 +index.system.edge.column.0.value= diff --git a/hugegraph-store/hg-store-node/src/test/resources/compatibility/2f827d6e8/store-index-rows.properties b/hugegraph-store/hg-store-node/src/test/resources/compatibility/2f827d6e8/store-index-rows.properties new file mode 100644 index 0000000000..7f5a599ed4 --- /dev/null +++ b/hugegraph-store/hg-store-node/src/test/resources/compatibility/2f827d6e8/store-index-rows.properties @@ -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. + # +secondary.name=53493a363a616c696365000801 +secondary.value=00414141426472732f576d413d +secondary.expiry=1609459260000 +int.name=a000000007800000070801 +int.value=00414141426472732f576d413d +int.expiry=1609459260000 +long.name=a20000000880000000000000070801 +long.value=00414141426472732f576d413d +long.expiry=1609459260000 +system.name=56493a2d313a34000801 +system.value=00414141426472732f576d413d +system.expiry=1609459260000 +system.edge.name=45493a2d323a35007e08018208050805323032362178000802 +system.edge.value=00414141426472732f576d413d +system.edge.expiry=1609459260000 diff --git a/hugegraph-store/hg-store-rocksdb/pom.xml b/hugegraph-store/hg-store-rocksdb/pom.xml index cd9cf28c6d..f8eb4ba58f 100644 --- a/hugegraph-store/hg-store-rocksdb/pom.xml +++ b/hugegraph-store/hg-store-rocksdb/pom.xml @@ -57,7 +57,7 @@ org.rocksdb rocksdbjni - 7.7.3 + ${rocksdb.version} org.projectlombok @@ -66,12 +66,12 @@ org.apache.logging.log4j log4j-slf4j-impl - 2.15.0 + ${log4j2.version} com.alibaba fastjson - 1.2.83 + 1.2.84 diff --git a/hugegraph-store/hg-store-rocksdb/src/main/java/org/apache/hugegraph/rocksdb/access/RocksDBScanIterator.java b/hugegraph-store/hg-store-rocksdb/src/main/java/org/apache/hugegraph/rocksdb/access/RocksDBScanIterator.java index dca8179308..7e23370cf6 100644 --- a/hugegraph-store/hg-store-rocksdb/src/main/java/org/apache/hugegraph/rocksdb/access/RocksDBScanIterator.java +++ b/hugegraph-store/hg-store-rocksdb/src/main/java/org/apache/hugegraph/rocksdb/access/RocksDBScanIterator.java @@ -41,6 +41,7 @@ public class RocksDBScanIterator implements ScanIterator { private final AtomicBoolean closed = new AtomicBoolean(false); private final RocksDBSession.RefCounter iterReference; private final Consumer closeOp; + private Throwable closeFailure; private byte[] key; private boolean matched; @@ -225,13 +226,26 @@ private boolean filter(byte[] v) { } @Override - public void close() { + public synchronized void close() { if (!this.closed.getAndSet(true)) { - if (this.rawIt.isOwningHandle()) { - this.rawIt.close(); + try { + if (this.rawIt.isOwningHandle()) { + this.rawIt.close(); + } + this.closeOp.accept(true); + this.iterReference.release(); + } catch (RuntimeException | Error failure) { + this.closeFailure = failure; + throw failure; } - this.closeOp.accept(true); - this.iterReference.release(); + } + // Automatic exhaustion can initiate close before its owner enters finally. + // A repeated close must wait for that release and retain any failure, without retrying JNI. + if (this.closeFailure instanceof RuntimeException) { + throw (RuntimeException) this.closeFailure; + } + if (this.closeFailure instanceof Error) { + throw (Error) this.closeFailure; } } diff --git a/hugegraph-store/hg-store-test/pom.xml b/hugegraph-store/hg-store-test/pom.xml index a284c1c5c4..28ca7102bf 100644 --- a/hugegraph-store/hg-store-test/pom.xml +++ b/hugegraph-store/hg-store-test/pom.xml @@ -51,7 +51,6 @@ org.jacoco jacoco-maven-plugin - 0.8.8 @@ -121,7 +120,6 @@ com.fasterxml.jackson.core jackson-databind - 2.13.0 @@ -132,34 +130,32 @@ com.google.protobuf protobuf-java-util - 3.17.2 + ${protobuf.runtime.version} compile org.springframework spring-context-support - 5.3.20 + ${spring-framework.version} org.springframework spring-test - 5.3.20 + ${spring-framework.version} test com.fasterxml.jackson.core jackson-core - 2.13.0 com.fasterxml.jackson.core jackson-annotations - 2.13.0 org.springframework.boot spring-boot-starter-test - 2.5.14 + ${spring-boot.version} org.springframework.boot @@ -167,33 +163,6 @@ - - org.powermock - powermock-classloading-xstream - 2.0.0-RC.3 - - - org.powermock - powermock-module-junit4-rule - 2.0.0-RC.3 - - - org.powermock - powermock-api-support - 2.0.0-RC.3 - - - org.powermock - powermock-module-junit4 - 2.0.0-RC.3 - compile - - - org.powermock - powermock-api-mockito2 - 2.0.0-RC.3 - compile - org.apache.hugegraph hg-store-client @@ -201,7 +170,7 @@ com.alipay.sofa jraft-core - ${pd-store-jraft.version} + ${jraft.version} org.rocksdb @@ -214,6 +183,16 @@ hg-store-node ${revision} compile + + + org.springframework + spring-web + + + org.springframework + spring-webmvc + + @@ -221,11 +200,24 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 + + + @{argLine} + "@${project.basedir}/../hg-store-dist/src/assembly/static/bin/jvm-module.options" + + + + + org.apache.maven.surefire + surefire-junit4 + ${maven.surefire.plugin.version} + + store-client-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -238,6 +230,7 @@ store-core-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -250,15 +243,19 @@ false **/CoreSuiteTest.java + **/IpUtilTest.java **/PartitionStateMachineTest.java **/PartitionEngineErrorTest.java **/BatchGraphIsolationTest.java + **/GraphIDManagerTest.java + **/SchemaDriverTest.java store-common-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -271,6 +268,7 @@ store-rocksdb-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -283,6 +281,7 @@ store-server-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -298,6 +297,7 @@ store-raftcore-test + true ${basedir}/src/main/java/ ${basedir}/target/classes/ @@ -312,7 +312,6 @@ org.jacoco jacoco-maven-plugin - 0.8.8 **/grpc/**/* diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/rocksdb/access/RocksDBScanIteratorCloseTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/rocksdb/access/RocksDBScanIteratorCloseTest.java new file mode 100644 index 0000000000..c66c8532b4 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/rocksdb/access/RocksDBScanIteratorCloseTest.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.hugegraph.rocksdb.access; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +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; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.LockSupport; +import java.util.function.Consumer; + +import org.apache.hugegraph.rocksdb.access.RocksDBSession.BackendColumn; +import org.apache.hugegraph.store.business.InnerKeyFilter; +import org.apache.hugegraph.store.business.MultiPartitionIterator; +import org.apache.hugegraph.store.term.Bits; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.InOrder; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksIterator; + +public class RocksDBScanIteratorCloseTest { + + @Rule + public TemporaryFolder directory = new TemporaryFolder(); + + @Test + @SuppressWarnings("unchecked") + public void testRealNativeEmptyIteratorRetainsOneTimeCallbackFailure() throws Exception { + RocksDB.loadLibrary(); + try (Options options = new Options().setCreateIfMissing(true); + RocksDB database = RocksDB.open(options, this.directory.newFolder("empty").getAbsolutePath())) { + RocksIterator raw = database.newIterator(); + RocksDBSession.RefCounter reference = mock(RocksDBSession.RefCounter.class); + Consumer closeOp = mock(Consumer.class); + IllegalStateException failure = new IllegalStateException("close callback failed once"); + doThrow(failure).doNothing().when(closeOp).accept(true); + RocksDBScanIterator iterator = new RocksDBScanIterator<>(raw, null, null, + ScanIterator.Trait.SCAN_ANY, reference, closeOp); + + assertSame(failure, assertThrows(IllegalStateException.class, iterator::hasNext)); + assertFalse("JNI must release the native iterator before callback failure", raw.isOwningHandle()); + assertSame(failure, assertThrows(IllegalStateException.class, iterator::close)); + assertSame(failure, assertThrows(IllegalStateException.class, iterator::close)); + verify(closeOp).accept(true); + verify(reference, never()).release(); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testRealNativeExhaustedIteratorRetainsOneTimeReferenceReleaseError() throws Exception { + RocksDB.loadLibrary(); + try (Options options = new Options().setCreateIfMissing(true); + RocksDB database = RocksDB.open(options, this.directory.newFolder("exhausted").getAbsolutePath())) { + database.put(new byte[]{1}, new byte[]{2}); + RocksIterator raw = database.newIterator(); + RocksDBSession.RefCounter reference = mock(RocksDBSession.RefCounter.class); + Consumer closeOp = mock(Consumer.class); + AssertionError failure = new AssertionError("reference release failed once"); + doThrow(failure).doNothing().when(reference).release(); + RocksDBScanIterator iterator = new RocksDBScanIterator<>(raw, null, null, + ScanIterator.Trait.SCAN_ANY, reference, closeOp); + + assertTrue(iterator.hasNext()); + iterator.next(); + assertSame(failure, assertThrows(AssertionError.class, iterator::hasNext)); + assertFalse("JNI must release the native iterator before reference failure", raw.isOwningHandle()); + assertSame(failure, assertThrows(AssertionError.class, iterator::close)); + assertSame(failure, assertThrows(AssertionError.class, iterator::close)); + InOrder order = inOrder(closeOp, reference); + order.verify(closeOp).accept(true); + order.verify(reference).release(); + } + } + + @Test + public void testEmptyIteratorRetainsOneTimeNativeCloseFailure() { + Fixture fixture = new Fixture(); + IllegalStateException failure = new IllegalStateException("native close failed once"); + doThrow(failure).doNothing().when(fixture.raw).close(); + + assertAutomaticFailureIsRetained(fixture, failure); + verify(fixture.closeOp, never()).accept(true); + verify(fixture.reference, never()).release(); + } + + @Test + public void testExhaustedIteratorRetainsOneTimeNativeCloseError() { + Fixture fixture = new Fixture(); + when(fixture.raw.isValid()).thenReturn(true, false); + when(fixture.raw.key()).thenReturn(new byte[]{1}); + when(fixture.raw.value()).thenReturn(new byte[]{2}); + AssertionError failure = new AssertionError("native close failed once"); + doThrow(failure).doNothing().when(fixture.raw).close(); + + assertTrue(fixture.iterator.hasNext()); + fixture.iterator.next(); + assertAutomaticFailureIsRetained(fixture, failure); + verify(fixture.closeOp, never()).accept(true); + verify(fixture.reference, never()).release(); + } + + @Test + public void testAutomaticCloseRetainsOneTimeCallbackFailure() { + Fixture fixture = new Fixture(); + IllegalStateException failure = new IllegalStateException("close callback failed once"); + doThrow(failure).doNothing().when(fixture.closeOp).accept(true); + + assertAutomaticFailureIsRetained(fixture, failure); + InOrder order = inOrder(fixture.raw, fixture.closeOp); + order.verify(fixture.raw).close(); + order.verify(fixture.closeOp).accept(true); + verify(fixture.reference, never()).release(); + } + + @Test + public void testAutomaticCloseRetainsOneTimeReferenceReleaseError() { + Fixture fixture = new Fixture(); + AssertionError failure = new AssertionError("reference release failed once"); + doThrow(failure).doNothing().when(fixture.reference).release(); + + assertAutomaticFailureIsRetained(fixture, failure); + InOrder order = inOrder(fixture.raw, fixture.closeOp, fixture.reference); + order.verify(fixture.raw).close(); + order.verify(fixture.closeOp).accept(true); + order.verify(fixture.reference).release(); + } + + @Test + public void testSuccessfulCloseIsIdempotentAndPreservesReleaseOrder() { + Fixture fixture = new Fixture(); + + fixture.iterator.close(); + fixture.iterator.close(); + assertFalse(fixture.iterator.hasNext()); + InOrder order = inOrder(fixture.raw, fixture.closeOp, fixture.reference); + order.verify(fixture.raw).close(); + order.verify(fixture.closeOp).accept(true); + order.verify(fixture.reference).release(); + verify(fixture.raw).close(); + verify(fixture.closeOp).accept(true); + verify(fixture.reference).release(); + } + + @Test(timeout = 5000) + public void testConcurrentCloseWaitsForSuccessfulRelease() throws Exception { + assertConcurrentCloseWaits(null); + } + + @Test(timeout = 5000) + public void testConcurrentCloseWaitsAndReportsTheSameFailure() throws Exception { + assertConcurrentCloseWaits(new IllegalStateException("close callback failed once")); + } + + @Test + public void testAllInnerKeyConstructorsDeferReadingUntilOwnershipTransfer() { + for (int constructor = 0; constructor < 3; constructor++) { + ScanIterator raw = mock(ScanIterator.class); + when(raw.hasNext()).thenReturn(true); + when(raw.next()).thenReturn(column(1, 15)); + InnerKeyFilter filter = filter(raw, constructor); + + verify(raw, never()).hasNext(); + verify(raw, never()).next(); + assertTrue(filter.hasNext()); + verify(raw).hasNext(); + verify(raw).next(); + filter.close(); + verify(raw).close(); + } + } + + @Test + public void testPlainInnerKeyFilterStripsGraphAndCodeAndPreservesPrefetch() { + ScanIterator raw = mock(ScanIterator.class); + when(raw.hasNext()).thenReturn(true, true, false); + when(raw.next()).thenReturn(column(1, 5), column(2, 19)); + InnerKeyFilter filter = new InnerKeyFilter<>(raw); + + assertTrue(filter.hasNext()); + assertTrue(filter.hasNext()); + assertArrayEquals(new byte[]{1}, filter.next().name); + assertTrue(filter.hasNext()); + assertArrayEquals(new byte[]{2}, filter.next().name); + assertFalse(filter.hasNext()); + verify(raw, times(2)).next(); + verify(raw, times(3)).hasNext(); + filter.close(); + } + + @Test + public void testCodeFilterKeepsHashSuffix() { + ScanIterator raw = mock(ScanIterator.class); + when(raw.hasNext()).thenReturn(true, false); + when(raw.next()).thenReturn(column(3, 15)); + InnerKeyFilter filter = new InnerKeyFilter<>(raw, true); + + assertArrayEquals(new byte[]{3, 0, 15}, filter.next().name); + assertFalse(filter.hasNext()); + filter.close(); + } + + @Test + public void testCodeRangeFilterPreservesInclusiveLowerAndExclusiveUpperBounds() { + ScanIterator raw = mock(ScanIterator.class); + when(raw.hasNext()).thenReturn(true, true, true, true, false); + when(raw.next()).thenReturn(column(1, 5), column(2, 10), column(3, 20), column(4, 19)); + InnerKeyFilter filter = new InnerKeyFilter<>(raw, 10, 20); + + assertTrue(filter.hasNext()); + assertArrayEquals(new byte[]{2, 0, 10}, filter.next().name); + assertTrue(filter.hasNext()); + assertArrayEquals(new byte[]{4, 0, 19}, filter.next().name); + assertFalse(filter.hasNext()); + verify(raw, times(4)).next(); + filter.close(); + } + + @Test + public void testInnerKeyFilterNextBeforeHasNextPreservesTheFirstColumn() { + ScanIterator raw = mock(ScanIterator.class); + when(raw.hasNext()).thenReturn(true, false); + when(raw.next()).thenReturn(column(7, 15)); + InnerKeyFilter filter = new InnerKeyFilter<>(raw); + + assertArrayEquals(new byte[]{7}, filter.next().name); + assertFalse(filter.hasNext()); + verify(raw).next(); + filter.close(); + } + + @Test + public void testInnerKeyFilterIsValidUsesTheExistingPrefetchedDelegatePosition() { + ScanIterator raw = mock(ScanIterator.class); + when(raw.hasNext()).thenReturn(true); + when(raw.next()).thenReturn(column(1, 15)); + when(raw.isValid()).thenAnswer(invocation -> { + verify(raw).next(); + return false; + }); + InnerKeyFilter filter = new InnerKeyFilter<>(raw); + + assertFalse(filter.isValid()); + assertTrue(filter.hasNext()); + verify(raw).next(); + filter.close(); + } + + @Test + public void testInnerKeyFilterCountKeepsTheExistingPrefetchedDelegatePosition() { + ScanIterator raw = mock(ScanIterator.class); + when(raw.hasNext()).thenReturn(true); + when(raw.next()).thenReturn(column(1, 15)); + when(raw.count()).thenAnswer(invocation -> { + verify(raw).next(); + return 7L; + }); + InnerKeyFilter filter = new InnerKeyFilter<>(raw); + + assertEquals(7L, filter.count()); + verify(raw).next(); + filter.close(); + } + + @Test + public void testInnerKeyFilterCloseBeforeUseDoesNotPrefetch() { + for (int constructor = 0; constructor < 3; constructor++) { + ScanIterator raw = mock(ScanIterator.class); + InnerKeyFilter filter = filter(raw, constructor); + + filter.close(); + assertFalse(filter.hasNext()); + verify(raw, never()).hasNext(); + verify(raw, never()).next(); + verify(raw).close(); + } + } + + @Test + public void testInnerKeyFilterPreservesPartitionResumeKey() { + ScanIterator raw = mock(ScanIterator.class); + when(raw.hasNext()).thenReturn(true, false); + when(raw.next()).thenReturn(column(9, 15)); + AtomicInteger supplied = new AtomicInteger(); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1, 2), (id, position) -> { + assertEquals(Integer.valueOf(2), id); + assertArrayEquals(new byte[]{9}, position); + supplied.incrementAndGet(); + return new InnerKeyFilter<>(raw); + }); + iterator.seek(ByteBuffer.allocate(Integer.BYTES + 1).putInt(2).put((byte) 9).array()); + + assertTrue(iterator.hasNext()); + BackendColumn result = iterator.next(); + assertArrayEquals(new byte[]{9}, result.name); + assertArrayEquals(ByteBuffer.allocate(Integer.BYTES).putInt(2).array(), iterator.position()); + assertFalse(iterator.hasNext()); + assertEquals(1, supplied.get()); + verify(raw).close(); + } + + private static InnerKeyFilter filter(ScanIterator raw, int constructor) { + if (constructor == 0) { + return new InnerKeyFilter<>(raw); + } + if (constructor == 1) { + return new InnerKeyFilter<>(raw, true); + } + return new InnerKeyFilter<>(raw, 10, 20); + } + + private static BackendColumn column(int key, int code) { + byte[] name = new byte[Short.BYTES + 1 + Short.BYTES]; + Bits.putShort(name, 0, 1); + name[Short.BYTES] = (byte) key; + Bits.putShort(name, name.length - Short.BYTES, code); + return BackendColumn.of(name, new byte[]{(byte) key}); + } + + private static void assertAutomaticFailureIsRetained(Fixture fixture, Throwable failure) { + assertSame(failure, assertThrows(Throwable.class, fixture.iterator::hasNext)); + assertSame(failure, assertThrows(Throwable.class, fixture.iterator::close)); + assertSame(failure, assertThrows(Throwable.class, fixture.iterator::close)); + verify(fixture.raw).close(); + } + + private static void assertConcurrentCloseWaits(Throwable failure) throws Exception { + Fixture fixture = new Fixture(); + CountDownLatch nativeCloseStarted = new CountDownLatch(1); + CountDownLatch allowNativeClose = new CountDownLatch(1); + doAnswer(invocation -> { + nativeCloseStarted.countDown(); + allowNativeClose.await(); + return null; + }).when(fixture.raw).close(); + if (failure != null) { + doThrow(failure).doNothing().when(fixture.closeOp).accept(true); + } + FutureTask first = new FutureTask<>(() -> { + fixture.iterator.close(); + return null; + }); + FutureTask second = new FutureTask<>(() -> { + fixture.iterator.close(); + return null; + }); + Thread firstThread = new Thread(first, "test-first-native-close"); + Thread secondThread = new Thread(second, "test-second-native-close"); + firstThread.setDaemon(true); + secondThread.setDaemon(true); + try { + firstThread.start(); + assertTrue(nativeCloseStarted.await(1, TimeUnit.SECONDS)); + secondThread.start(); + awaitBlockedClose(secondThread); + assertFalse("second close cannot claim release before native close finishes", second.isDone()); + verify(fixture.reference, never()).release(); + allowNativeClose.countDown(); + if (failure == null) { + first.get(2, TimeUnit.SECONDS); + second.get(2, TimeUnit.SECONDS); + verify(fixture.reference).release(); + } else { + assertSame(failure, assertThrows(ExecutionException.class, + () -> first.get(2, TimeUnit.SECONDS)).getCause()); + assertSame(failure, assertThrows(ExecutionException.class, + () -> second.get(2, TimeUnit.SECONDS)).getCause()); + verify(fixture.reference, never()).release(); + } + verify(fixture.raw).close(); + verify(fixture.closeOp).accept(true); + } finally { + allowNativeClose.countDown(); + firstThread.join(1000); + secondThread.join(1000); + } + } + + private static void awaitBlockedClose(Thread thread) { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + while (System.nanoTime() < deadline) { + if (thread.getState() == Thread.State.BLOCKED) { + return; + } + if (thread.getState() == Thread.State.TERMINATED) { + break; + } + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1)); + } + fail("concurrent close must wait for the first resource release"); + } + + private static final class Fixture { + + private final RocksIterator raw = mock(RocksIterator.class); + private final RocksDBSession.RefCounter reference = mock(RocksDBSession.RefCounter.class); + @SuppressWarnings("unchecked") + private final Consumer closeOp = mock(Consumer.class); + private final RocksDBScanIterator iterator; + + private Fixture() { + when(this.raw.isOwningHandle()).thenReturn(true); + this.iterator = new RocksDBScanIterator<>(this.raw, null, null, + ScanIterator.Trait.SCAN_ANY, this.reference, this.closeOp); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/HeartbeatShutdownTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/HeartbeatShutdownTest.java new file mode 100644 index 0000000000..7a89ddf241 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/HeartbeatShutdownTest.java @@ -0,0 +1,488 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.store; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hugegraph.pd.common.PDException; +import org.apache.hugegraph.pd.grpc.Metapb; +import org.apache.hugegraph.pd.grpc.Pdpb.ErrorType; +import org.apache.hugegraph.store.business.BusinessHandler; +import org.apache.hugegraph.store.meta.Partition; +import org.apache.hugegraph.store.meta.PartitionManager; +import org.apache.hugegraph.store.meta.Store; +import org.apache.hugegraph.store.meta.StoreMetadata; +import org.apache.hugegraph.store.metric.HgMetricService; +import org.apache.hugegraph.store.options.HgStoreEngineOptions; +import org.apache.hugegraph.store.pd.PdProvider; +import org.apache.hugegraph.store.util.PartitionMetaStoreWrapper; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; + +import com.alipay.sofa.jraft.rpc.RpcServer; + +public class HeartbeatShutdownTest { + + @Test + public void testInvalidRecoveredShardExitsAfterNodeDestroy() throws Exception { + assertFatalExit("recovery", 0); + } + + @Test + public void testFatalRegistrationExitsAfterShutdownHooks() throws Exception { + for (int code : new int[]{ErrorType.STORE_ID_NOT_EXIST_VALUE, + ErrorType.STORE_HAS_BEEN_REMOVED_VALUE, + ErrorType.STORE_PROHIBIT_DUPLICATE_VALUE}) { + assertFatalExit("register", code); + } + } + + @Test + public void testFatalHeartbeatExitsAfterShutdownHooks() throws Exception { + for (int code : new int[]{ErrorType.STORE_ID_NOT_EXIST_VALUE, ErrorType.STORE_HAS_BEEN_REMOVED_VALUE}) { + assertFatalExit("heartbeat", code); + } + } + + @Test + public void testInterruptedEngineShutdownFinishesNativeTeardown() throws Exception { + assertFatalExit("interrupted-shutdown", 0); + } + + private static void assertFatalExit(String phase, int code) throws Exception { + Path directory = Files.createTempDirectory(Path.of("target"), "heartbeat-fatal-"); + Path marker = directory.resolve("hook-complete"); + Path output = directory.resolve("child.log"); + String classpath = System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")); + if ("recovery".equals(phase)) { + // Node's packaged artifact stores classes under Spring Boot's BOOT-INF. + Path nodeClasses = Path.of("../hg-store-node/target/classes").toAbsolutePath(); + Assert.assertTrue("Missing Node reactor classes: " + nodeClasses, + Files.isRegularFile(nodeClasses.resolve( + "org/apache/hugegraph/store/node/grpc/HgStoreNodeService.class"))); + classpath += File.pathSeparator + nodeClasses; + } + Process process = new ProcessBuilder(new File(System.getProperty("java.home"), "bin/java").toString(), + "-cp", classpath, HeartbeatShutdownTest.class.getName(), + phase, Integer.toString(code), marker.toString()) + .redirectErrorStream(true).redirectOutput(output.toFile()).start(); + try { + boolean exited = process.waitFor(20, TimeUnit.SECONDS); + if (!exited) { + new ProcessBuilder(new File(System.getProperty("java.home"), "bin/jstack").toString(), + Long.toString(process.pid())) + .redirectErrorStream(true).redirectOutput(directory.resolve("thread-dump.log").toFile()) + .start().waitFor(5, TimeUnit.SECONDS); + } + Assert.assertTrue("Fatal heartbeat did not exit; child log: " + output + "\n" + Files.readString(output), + exited); + Assert.assertEquals(Files.readString(output), + "recovery".equals(phase) || "interrupted-shutdown".equals(phase) ? 0 : 255, + process.exitValue()); + Assert.assertEquals("joined", Files.readString(marker)); + if ("recovery".equals(phase)) { + String log = Files.readString(output); + Assert.assertTrue(log, log.contains("not in valid shard group")); + Assert.assertTrue(log, log.contains("NODE_DESTROY_COMPLETE")); + } + } finally { + if (process.isAlive()) { + process.destroyForcibly(); + Assert.assertTrue(process.waitFor(5, TimeUnit.SECONDS)); + } + } + } + + public static void main(String[] args) throws Exception { + String phase = args[0]; + int code = Integer.parseInt(args[1]); + Path marker = Path.of(args[2]); + if ("recovery".equals(phase)) { + runInvalidShardRecovery(marker); + return; + } + if ("interrupted-shutdown".equals(phase)) { + try { + runInterruptedEngineShutdown(marker); + } catch (Exception | AssertionError failure) { + failure.printStackTrace(); + System.exit(1); + } + // RocksDBFactory owns a process-wide scheduler; this isolated probe has finished. + System.exit(0); + return; + } + CountDownLatch producersStarted = new CountDownLatch(1); + PdProvider provider = Mockito.mock(PdProvider.class); + Mockito.when(provider.getClusterStats()).thenReturn(Metapb.ClusterStats.newBuilder() + .setState(Metapb.ClusterState.Cluster_OK).build()); + Mockito.when(provider.registerStore(Mockito.any())).thenAnswer(invocation -> { + producersStarted.await(); + if ("register".equals(phase)) { + System.out.println("FATAL_REGISTER " + code); + throw new PDException(code, "fatal registration fixture"); + } + return 1L; + }); + Mockito.when(provider.storeHeartbeat(Mockito.any())).thenAnswer(invocation -> { + System.out.println("FATAL_HEARTBEAT " + code); + throw new PDException(code, "fatal heartbeat fixture"); + }); + HeartbeatService service = new HeartbeatService(null); + service.setStoreMetadata(Mockito.mock(StoreMetadata.class)); + HgStoreEngineOptions options = Mockito.mock(HgStoreEngineOptions.class); + Mockito.when(options.getPdProvider()).thenReturn(provider); + Mockito.when(options.getGrpcAddress()).thenReturn("127.0.0.1:8500"); + Mockito.when(options.getRaftAddress()).thenReturn("127.0.0.1:8510"); + Mockito.when(options.getStoreHBInterval()).thenReturn(1); + Mockito.when(options.getPartitionHBInterval()).thenReturn(60); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("SHUTDOWN_HOOK_JOIN"); + service.shutdown(); + try { + Files.writeString(marker, "joined"); + System.out.println("SHUTDOWN_HOOK_COMPLETE"); + } catch (Exception e) { + throw new AssertionError(e); + } + }, "fatal-fixture-shutdown")); + service.init(options); + producersStarted.countDown(); + } + + private static void runInvalidShardRecovery(Path marker) throws Exception { + Path data = marker.getParent().resolve("data"); + Path raft = marker.getParent().resolve("raft"); + Files.createDirectories(data.resolve(HgStoreEngineOptions.DB_Path_Prefix).resolve("00001")); + Files.createDirectories(raft.resolve(HgStoreEngineOptions.Raft_Path_Prefix).resolve("00001/log")); + HgStoreEngineOptions options = new HgStoreEngineOptions(); + options.setDataPath(data.toString()); + options.setRaftPath(raft.toString()); + PdProvider provider = Mockito.mock(PdProvider.class); + Metapb.Partition partition = Metapb.Partition.newBuilder().setId(1).setGraphName("graph").build(); + Mockito.when(provider.getShardGroup(1)).thenReturn(Metapb.ShardGroup.newBuilder() + .setId(1).addShards(Metapb.Shard.newBuilder().setStoreId(2) + .setRole(Metapb.ShardRole.Leader)).build()); + Mockito.when(provider.getPartitionByID("graph", 1)).thenReturn(new Partition(partition)); + Store remote = new Store(); + remote.setRaftAddress("127.0.0.1:8511"); + Mockito.when(provider.getStoreByID(2L)).thenReturn(remote); + PartitionManager manager = new PartitionManager(provider, options); + StoreMetadata metadata = Mockito.mock(StoreMetadata.class); + Store local = new Store(); + local.setId(1); + Mockito.when(metadata.getStore()).thenReturn(local); + Mockito.when(metadata.getPartitionStore(1)).thenReturn(Metapb.PartitionStore.newBuilder() + .setPartitionId(1).setStoreLocation(data.toString()).build()); + Mockito.when(metadata.getPartitionRaft(1)).thenReturn(Metapb.PartitionRaft.newBuilder() + .setPartitionId(1).setRaftLocation(raft.toString()).build()); + setField(PartitionManager.class, manager, "storeMetadata", metadata); + PartitionMetaStoreWrapper wrapper = Mockito.mock(PartitionMetaStoreWrapper.class); + Mockito.when(wrapper.scan(Mockito.eq(1), Mockito.eq(Metapb.Partition.parser()), Mockito.any())) + .thenReturn(List.of(partition)); + setField(PartitionManager.class, manager, "wrapper", wrapper); + Constructor constructor = HgStoreEngine.class.getDeclaredConstructor(); + constructor.setAccessible(true); + HgStoreEngine engine = constructor.newInstance(); + Fixture service = new Fixture(false); + RpcServer rpc = Mockito.mock(RpcServer.class); + HgMetricService metrics = Mockito.mock(HgMetricService.class); + setField(HgStoreEngine.class, engine, "partitionManager", manager); + setField(HgStoreEngine.class, engine, "heartbeatService", service); + setField(HgStoreEngine.class, engine, "rpcServer", rpc); + setField(HgStoreEngine.class, engine, "metricService", metrics); + Class nodeClass = Class.forName("org.apache.hugegraph.store.node.grpc.HgStoreNodeService"); + Class configClass = Class.forName("org.apache.hugegraph.store.node.AppConfig"); + Object node = nodeClass.getConstructor(configClass).newInstance((Object) null); + setField(nodeClass, node, "storeEngine", engine); + service.addStateListener(engine); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("NODE_DESTROY_ENTERED"); + try { + nodeClass.getMethod("destroy").invoke(node); + Mockito.verify(metrics).shutdown(); + Mockito.verify(rpc).shutdown(); + Files.writeString(marker, "joined"); + System.out.println("NODE_DESTROY_COMPLETE"); + } catch (Exception e) { + throw new AssertionError(e); + } + }, "recovery-fixture-shutdown")); + service.start(); + Assert.assertTrue(service.entered.await(5, TimeUnit.SECONDS)); + service.onStateChanged(Metapb.StoreState.Up); + } + + private static void runInterruptedEngineShutdown(Path marker) throws Exception { + RocksDB.loadLibrary(); + Options nativeOptions = new Options().setCreateIfMissing(true); + RocksDB nativeDatabase = RocksDB.open(nativeOptions, marker.resolveSibling("native-store").toString()); + Constructor constructor = HgStoreEngine.class.getDeclaredConstructor(); + constructor.setAccessible(true); + HgStoreEngine engine = constructor.newInstance(); + Fixture heartbeat = new Fixture(false); + heartbeat.start(); + Assert.assertTrue(heartbeat.entered.await(5, TimeUnit.SECONDS)); + RpcServer rpc = Mockito.mock(RpcServer.class); + BusinessHandler handler = Mockito.mock(BusinessHandler.class); + PartitionEngine partition = Mockito.mock(PartitionEngine.class); + Mockito.when(partition.getGroupId()).thenReturn(1); + Mockito.when(partition.getRaftNode()).thenReturn(Mockito.mock(com.alipay.sofa.jraft.Node.class)); + Mockito.doAnswer(invocation -> { + Assert.assertFalse(Thread.currentThread().isInterrupted()); + Thread.currentThread().interrupt(); + return null; + }).when(partition).shutdown(); + Mockito.doAnswer(invocation -> { + Assert.assertFalse("Raft's restored interrupt must not escape into native cleanup", + Thread.currentThread().isInterrupted()); + Assert.assertFalse(heartbeat.storeThread.isAlive()); + Assert.assertFalse(heartbeat.partitionThread.isAlive()); + nativeDatabase.close(); + return null; + }).when(handler).closeDB(1); + setField(HgStoreEngine.class, engine, "heartbeatService", heartbeat); + setField(HgStoreEngine.class, engine, "metricService", Mockito.mock(HgMetricService.class)); + setField(HgStoreEngine.class, engine, "rpcServer", rpc); + setField(HgStoreEngine.class, engine, "businessHandler", handler); + Field partitionsField = HgStoreEngine.class.getDeclaredField("partitionEngines"); + partitionsField.setAccessible(true); + @SuppressWarnings("unchecked") + Map partitions = (Map) partitionsField.get(engine); + partitions.put(1, partition); + try { + Thread.currentThread().interrupt(); + engine.shutdown(); + Assert.assertTrue("restore the listener's interrupt after all teardown", + Thread.currentThread().isInterrupted()); + Assert.assertTrue(partitions.isEmpty()); + Mockito.verify(handler).closeDB(1); + Assert.assertFalse("engine teardown must reach actual native database close", + nativeDatabase.isOwningHandle()); + Mockito.verify(rpc).shutdown(); + Files.writeString(marker, "joined"); + } finally { + Thread.interrupted(); + heartbeat.shutdown(); + nativeDatabase.close(); + nativeOptions.close(); + } + } + + private static void setField(Class owner, Object object, String name, Object value) throws Exception { + Field field = owner.getDeclaredField(name); + field.setAccessible(true); + field.set(object, value); + } + + @Test + public void testShutdownJoinsBothInFlightProducersBeforeReturning() throws Exception { + Fixture service = new Fixture(true); + ExecutorService closer = Executors.newSingleThreadExecutor(); + try { + service.start(); + Assert.assertTrue(service.entered.await(5, TimeUnit.SECONDS)); + Future shutdown = closer.submit(service::shutdown); + assertStillClosing(shutdown); + service.storeRelease.countDown(); + assertStillClosing(shutdown); + service.partitionRelease.countDown(); + shutdown.get(5, TimeUnit.SECONDS); + Assert.assertFalse(service.storeThread.isAlive()); + Assert.assertFalse(service.partitionThread.isAlive()); + } finally { + service.storeRelease.countDown(); + service.partitionRelease.countDown(); + service.shutdown(); + closer.shutdownNow(); + } + } + + @Test + public void testShutdownWakesBothIdleProducers() throws Exception { + Fixture service = new Fixture(false); + ExecutorService closer = Executors.newSingleThreadExecutor(); + try { + service.start(); + Assert.assertTrue(service.entered.await(5, TimeUnit.SECONDS)); + awaitSleeping(service.storeThread); + awaitSleeping(service.partitionThread); + closer.submit(service::shutdown).get(5, TimeUnit.SECONDS); + Assert.assertFalse(service.storeThread.isAlive()); + Assert.assertFalse(service.partitionThread.isAlive()); + } finally { + service.shutdown(); + closer.shutdownNow(); + } + } + + @Test + public void testShutdownWaitsForDerivedStateCallbackAndRejectsLateUpdates() throws Exception { + Fixture service = new Fixture(false); + ExecutorService closer = Executors.newSingleThreadExecutor(); + CountDownLatch callbackEntered = new CountDownLatch(1); + CountDownLatch callbackRelease = new CountDownLatch(1); + AtomicInteger calls = new AtomicInteger(); + service.addStateListener((store, previous, current) -> { + calls.incrementAndGet(); + callbackEntered.countDown(); + try { + callbackRelease.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } + }); + try { + service.start(); + Assert.assertTrue(service.entered.await(5, TimeUnit.SECONDS)); + service.onStateChanged(Metapb.StoreState.Up); + Assert.assertTrue(callbackEntered.await(5, TimeUnit.SECONDS)); + Future shutdown = closer.submit(service::shutdown); + assertStillClosing(shutdown); + callbackRelease.countDown(); + shutdown.get(5, TimeUnit.SECONDS); + service.onStateChanged(Metapb.StoreState.Offline); + Assert.assertEquals(1, calls.get()); + Assert.assertEquals(Metapb.StoreState.Up, service.getStoreInfo().getState()); + } finally { + callbackRelease.countDown(); + service.shutdown(); + closer.shutdownNow(); + } + } + + @Test + public void testInterruptedShutdownKeepsWaitingForBothProducers() throws Exception { + Fixture service = new Fixture(true); + CountDownLatch closing = new CountDownLatch(1); + AtomicBoolean databaseClosed = new AtomicBoolean(); + AtomicBoolean interruptPreserved = new AtomicBoolean(); + Thread closer = new Thread(() -> { + closing.countDown(); + service.shutdown(); + databaseClosed.set(true); + interruptPreserved.set(Thread.currentThread().isInterrupted()); + }); + try { + service.start(); + Assert.assertTrue(service.entered.await(5, TimeUnit.SECONDS)); + closer.start(); + Assert.assertTrue(closing.await(5, TimeUnit.SECONDS)); + closer.interrupt(); + closer.join(100); + Assert.assertTrue("interruption must not abandon producer drain", closer.isAlive()); + Assert.assertFalse(databaseClosed.get()); + service.storeRelease.countDown(); + closer.join(100); + Assert.assertTrue("both producers must drain", closer.isAlive()); + Assert.assertFalse(databaseClosed.get()); + service.partitionRelease.countDown(); + closer.join(5000); + Assert.assertFalse(closer.isAlive()); + Assert.assertTrue(databaseClosed.get()); + Assert.assertTrue(interruptPreserved.get()); + } finally { + service.storeRelease.countDown(); + service.partitionRelease.countDown(); + service.shutdown(); + closer.join(5000); + } + } + + private static void assertStillClosing(Future shutdown) throws Exception { + try { + shutdown.get(100, TimeUnit.MILLISECONDS); + Assert.fail("Shutdown returned while heartbeat still owns work"); + } catch (TimeoutException expected) { + // A successful return would let HgStoreEngine close the DB too soon. + } + } + + private static void awaitSleeping(Thread thread) throws Exception { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + while (thread.getState() != Thread.State.TIMED_WAITING && System.nanoTime() < deadline) { + Thread.sleep(5); + } + Assert.assertEquals(Thread.State.TIMED_WAITING, thread.getState()); + } + + private static final class Fixture extends HeartbeatService { + + private final CountDownLatch entered = new CountDownLatch(2); + private final CountDownLatch storeRelease; + private final CountDownLatch partitionRelease; + private volatile Thread storeThread; + private volatile Thread partitionThread; + + private Fixture(boolean blocked) { + super(null); + this.storeRelease = new CountDownLatch(blocked ? 1 : 0); + this.partitionRelease = new CountDownLatch(blocked ? 1 : 0); + } + + private void start() throws Exception { + this.setStoreMetadata(Mockito.mock(StoreMetadata.class)); + HgStoreEngineOptions options = Mockito.mock(HgStoreEngineOptions.class); + Mockito.when(options.getPartitionHBInterval()).thenReturn(60); + Field delay = HeartbeatService.class.getDeclaredField("timerNextDelay"); + delay.setAccessible(true); + delay.setInt(this, 60000); + this.init(options); + } + + @Override + protected void registerStore() { + this.storeThread = Thread.currentThread(); + this.awaitWork(this.storeRelease); + } + + @Override + protected void partitionHeartbeat() { + this.partitionThread = Thread.currentThread(); + this.awaitWork(this.partitionRelease); + } + + private void awaitWork(CountDownLatch release) { + this.entered.countDown(); + try { + release.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/ClientSuiteTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/ClientSuiteTest.java index 78445e5934..affda30112 100644 --- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/ClientSuiteTest.java +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/ClientSuiteTest.java @@ -19,6 +19,8 @@ import org.apache.hugegraph.store.client.grpc.AbstractGrpcClientTest; import org.apache.hugegraph.store.client.grpc.KvPageScannerTest; +import org.apache.hugegraph.store.client.query.CommonKvStreamObserverTest; +import org.apache.hugegraph.store.client.query.MultiStreamIteratorTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -32,7 +34,11 @@ NodeTxExecutorTest.class, KvPageScannerTest.class, NodeTxSessionProxyTest.class, - OrderedKvIteratorTest.class + OrderedKvIteratorTest.class, + CommonKvStreamObserverTest.class, + MultiStreamIteratorTest.class, + HgAssertTest.class, + HgBufferProxyTest.class }) public class ClientSuiteTest { } diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/HgBufferProxyTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/HgBufferProxyTest.java new file mode 100644 index 0000000000..2f93bb712e --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/HgBufferProxyTest.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.hugegraph.store.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hugegraph.store.client.type.HgStoreClientException; +import org.apache.hugegraph.store.client.util.HgBufferProxy; +import org.junit.Test; + +/** + * HgBufferProxy is the page buffer behind KvPageScanner: receive() asks the producer task for + * more data when the buffer runs low, and close()/setError() end the stream. + */ +public class HgBufferProxyTest { + + @SuppressWarnings("unchecked") + private static HgBufferProxy proxy(Runnable task) { + return HgBufferProxy.of(task); + } + + @Test + public void testRejectsNullTaskAndNullItems() { + assertThrows(IllegalArgumentException.class, () -> HgBufferProxy.of(null)); + HgBufferProxy buffer = proxy(() -> { }); + assertThrows(IllegalArgumentException.class, () -> buffer.send(null)); + } + + @Test + public void testReceiveRunsTheProducerOnlyWhenTheBufferIsLow() { + AtomicInteger runs = new AtomicInteger(); + AtomicReference> ref = new AtomicReference<>(); + HgBufferProxy buffer = proxy(() -> ref.get().send("page-" + runs.incrementAndGet())); + ref.set(buffer); + + assertEquals("page-1", buffer.receive(1, null)); + assertEquals(1, runs.get()); + + buffer.send("x"); + buffer.send("y"); + assertEquals("x", buffer.receive(1, null)); + assertEquals("a buffered backlog must not trigger the producer", 1, runs.get()); + assertEquals("y", buffer.receive(1, null)); + assertEquals(2, runs.get()); + assertEquals("page-2", buffer.receive(1, null)); + } + + @Test + public void testTimeoutThrowsWithoutCallbackAndNotifiesTheCallback() { + HgBufferProxy buffer = proxy(() -> { }); + + RuntimeException e = assertThrows(RuntimeException.class, () -> buffer.receive(0, null)); + assertEquals("timeout, max time: 0 seconds", e.getMessage()); + + List timeouts = new ArrayList<>(); + assertNull(buffer.receive(0, timeouts::add)); + assertEquals(List.of(0), timeouts); + } + + @Test + public void testCloseDrainsBufferedItemsThenSignalsEnd() { + AtomicInteger runs = new AtomicInteger(); + HgBufferProxy buffer = proxy(runs::incrementAndGet); + buffer.send("a"); + buffer.send("b"); + + buffer.close(); + buffer.close(); + buffer.send("ignored"); + + assertTrue(buffer.isClosed()); + assertEquals("a", buffer.receive(0, null)); + assertEquals("b", buffer.receive(0, null)); + assertNull(buffer.receive(0, null)); + assertNull(buffer.receive(0, null)); + assertEquals("a closed buffer must not ask the producer for more", 0, runs.get()); + } + + @Test + public void testStreamErrorIsRethrownWithItsCause() { + IllegalStateException cause = new IllegalStateException("stream broken"); + + AtomicReference> ref = new AtomicReference<>(); + HgBufferProxy open = proxy(() -> { + ref.get().send("partial"); + ref.get().setError(cause); + }); + ref.set(open); + HgStoreClientException e = assertThrows(HgStoreClientException.class, + () -> open.receive(1, null)); + assertSame(cause, e.getCause()); + + HgBufferProxy closed = proxy(() -> { }); + closed.send("buffered"); + closed.setError(cause); + closed.close(); + e = assertThrows(HgStoreClientException.class, () -> closed.receive(0, null)); + assertSame(cause, e.getCause()); + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/NodeTxExecutorTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/NodeTxExecutorTest.java index 83035184b4..765ebfa096 100644 --- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/NodeTxExecutorTest.java +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/NodeTxExecutorTest.java @@ -18,10 +18,15 @@ package org.apache.hugegraph.store.client; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doThrow; 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; @@ -35,15 +40,72 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import org.apache.hugegraph.store.HgOwnerKey; import org.apache.hugegraph.store.HgStoreSession; import org.apache.hugegraph.store.client.type.HgStoreClientException; +import org.apache.hugegraph.store.term.HgTriple; import org.junit.Test; import io.grpc.Status; public class NodeTxExecutorTest { + @Test + public void testRollbackDiscardsQueuedActions() { + assertRollbackDiscardsQueuedActions(false); + } + + @Test + public void testFailedRollbackDiscardsQueuedActions() { + assertRollbackDiscardsQueuedActions(true); + } + + private static void assertRollbackDiscardsQueuedActions(boolean rollbackFails) { + NodeTxSessionProxy proxy = mock(NodeTxSessionProxy.class); + HgStoreNode node = mock(HgStoreNode.class); + HgStoreNodeSession session = mock(HgStoreNodeSession.class); + when(node.getNodeId()).thenReturn(1L); + when(node.openSession("graph")).thenReturn(session); + when(session.getStoreNode()).thenReturn(node); + when(session.isTx()).thenReturn(true); + when(session.put(anyString(), any(HgOwnerKey.class), any(byte[].class))).thenReturn(true); + when(proxy.doAction(anyString(), any(HgOwnerKey.class), any(HgOwnerKey.class), + any())).thenAnswer(invocation -> { + Function action = invocation.getArgument(3); + return action.apply(null); + }); + NodeTxExecutor executor = NodeTxExecutor.graphOf("graph", proxy); + HgOwnerKey key = HgOwnerKey.of(new byte[]{1}, new byte[]{2}); + byte[] abandonedValue = new byte[]{3}; + byte[] currentValue = new byte[]{4}; + executor.setTx(true); + executor.prepareTx(new HgTriple<>("ap", key, null), + ignored -> executor.openNodeSession(node).put("ap", key, abandonedValue)); + // A read can open a node session before the queued writes are sent. + executor.openNodeSession(node); + if (rollbackFails) { + RuntimeException failure = new RuntimeException("rollback transport failure"); + doThrow(failure).doNothing().when(session).rollback(); + assertSame(failure, assertThrows(RuntimeException.class, executor::rollbackTx)); + } else { + executor.rollbackTx(); + } + assertFalse(executor.isTx()); + verify(session).rollback(); + + executor.setTx(true); + executor.prepareTx(new HgTriple<>("ap", key, null), + ignored -> executor.openNodeSession(node).put("ap", key, currentValue)); + executor.commitTx(); + assertFalse(executor.isTx()); + verify(session, never()).put("ap", key, abandonedValue); + verify(session).put("ap", key, currentValue); + verify(node, times(2)).openSession("graph"); + verify(session).commit(); + } + @Test public void testRetryReplacesSessionFromEvictedNode() { long nodeId = 1L; diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/OrderedKvIteratorTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/OrderedKvIteratorTest.java index 3073c1bfec..77802ca2c3 100644 --- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/OrderedKvIteratorTest.java +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/OrderedKvIteratorTest.java @@ -298,7 +298,8 @@ public void testConcurrentInitializeFailsWithoutWaitingForSlowSource() TestIterator slow = new TestIterator(1); TestIterator failed = new TestIterator(2); slow.blockFirstHasNext(slowStarted, releaseSlow); - failed.failOnHasNextAfter(0); + // Start the slow source before failure can cancel its task. + failed.blockAndFailFirstHasNext(new CountDownLatch(0), slowStarted); Future result = caller.submit(() -> { OrderedKvIterator iterator = new OrderedKvIterator( diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/grpc/KvPageScannerTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/grpc/KvPageScannerTest.java index c2c70e2d50..a99aab9579 100644 --- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/grpc/KvPageScannerTest.java +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/grpc/KvPageScannerTest.java @@ -17,12 +17,35 @@ package org.apache.hugegraph.store.client.grpc; +import static org.mockito.ArgumentMatchers.any; +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; + import java.lang.reflect.Proxy; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import org.apache.hugegraph.store.client.HgStoreNode; +import org.apache.hugegraph.store.client.HgStoreNodeManager; import org.apache.hugegraph.store.client.HgStoreNodeSession; +import org.apache.hugegraph.store.client.HgStoreNotice; +import org.apache.hugegraph.store.client.type.HgNodeStatus; +import org.apache.hugegraph.store.client.type.HgStoreClientException; import org.apache.hugegraph.store.grpc.common.Kv; import org.apache.hugegraph.store.grpc.common.ScanMethod; import org.apache.hugegraph.store.grpc.common.ScanOrderType; @@ -30,13 +53,497 @@ import org.apache.hugegraph.store.grpc.stream.ScanStreamReq; import org.junit.Assert; import org.junit.Test; +import org.mockito.stubbing.Answer; import com.google.protobuf.ByteString; +import io.grpc.ManagedChannel; +import io.grpc.Status; import io.grpc.stub.StreamObserver; public class KvPageScannerTest { + private static final AtomicLong NODE_IDS = new AtomicLong(-9000000); + + @Test + public void testUnavailableScanRetiresPoolAndRebindsNextStub() { + try (FailureFixture fixture = new FailureFixture()) { + var oldStub = fixture.client.getStub(fixture.session); + Assert.assertTrue(Arrays.asList(fixture.channels).contains(oldStub.getChannel())); + fixture.failScan(Status.UNAVAILABLE); + + Assert.assertNull(fixture.manager.getStoreNode(fixture.nodeId)); + Assert.assertFalse(AbstractGrpcClient.channels.containsKey(fixture.target)); + for (ManagedChannel channel : fixture.channels) { + verify(channel).shutdown(); + } + var nextStub = fixture.client.getStub(fixture.session); + Assert.assertFalse(Arrays.asList(fixture.channels).contains(nextStub.getChannel())); + } + } + + @Test + public void testUnavailableScanPreservesErrorWhenRequestCompletionFails() { + try (FailureFixture fixture = new FailureFixture()) { + fixture.failScan(Status.UNAVAILABLE, true); + Assert.assertNull(fixture.manager.getStoreNode(fixture.nodeId)); + Assert.assertFalse(AbstractGrpcClient.channels.containsKey(fixture.target)); + } + } + + @Test + public void testUnavailableScanRetiresPoolBeforePublishingError() throws Exception { + try (FailureFixture fixture = new FailureFixture()) { + CountDownLatch requested = new CountDownLatch(1); + CountDownLatch retirementEntered = new CountDownLatch(1); + CountDownLatch allowRetirement = new CountDownLatch(1); + AtomicReference receiverThread = new AtomicReference<>(); + AtomicReference> response = new AtomicReference<>(); + StreamObserver requestObserver = mock(StreamObserver.class); + doAnswer(invocation -> { + requested.countDown(); + return null; + }).when(requestObserver).onNext(any()); + when(fixture.session.getStoreNode()).thenAnswer(invocation -> { + retirementEntered.countDown(); + Assert.assertTrue("Retirement gate timed out", + allowRetirement.await(5, TimeUnit.SECONDS)); + return fixture.node; + }); + KvPageScanner scanner = new KvPageScanner(fixture.session, + ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL).setTable("table"), + observer -> { + response.set(observer); + return requestObserver; + }); + ExecutorService executor = Executors.newFixedThreadPool(2); + var error = Status.UNAVAILABLE.withDescription("asynchronous scan failure") + .asRuntimeException(); + try { + Future failure = executor.submit(() -> { + receiverThread.set(Thread.currentThread()); + try { + scanner.hasNext(); + Assert.fail("Failed scan must remain visible to the caller"); + return null; + } catch (HgStoreClientException e) { + return e.getCause(); + } + }); + Assert.assertTrue(requested.await(5, TimeUnit.SECONDS)); + awaitBlockedReceiver(receiverThread.get(), failure); + Future notification = executor.submit(() -> response.get().onError(error)); + Assert.assertTrue(retirementEntered.await(5, TimeUnit.SECONDS)); + Assert.assertSame(fixture.channels, AbstractGrpcClient.channels.get(fixture.target)); + try { + failure.get(1, TimeUnit.SECONDS); + Assert.fail("Scan error became visible before failed pool retirement"); + } catch (TimeoutException expected) { + // The caller must remain blocked until the failed pool is retired. + } + allowRetirement.countDown(); + Assert.assertSame(error, failure.get(5, TimeUnit.SECONDS)); + Assert.assertNull(fixture.manager.getStoreNode(fixture.nodeId)); + Assert.assertFalse(AbstractGrpcClient.channels.containsKey(fixture.target)); + var retryStub = fixture.client.getStub(fixture.session); + Assert.assertFalse(Arrays.asList(fixture.channels).contains(retryStub.getChannel())); + notification.get(5, TimeUnit.SECONDS); + } finally { + allowRetirement.countDown(); + executor.shutdownNow(); + Assert.assertTrue("Scan workers failed to stop", + executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + } + + @Test + public void testReceiverEnteringDuringRetirementDoesNotSeeEof() throws Exception { + try (FailureFixture fixture = new FailureFixture()) { + CountDownLatch retirementEntered = new CountDownLatch(1); + CountDownLatch allowRetirement = new CountDownLatch(1); + CountDownLatch receiverEntered = new CountDownLatch(1); + AtomicReference receiverThread = new AtomicReference<>(); + AtomicReference> response = new AtomicReference<>(); + StreamObserver requestObserver = mock(StreamObserver.class); + when(fixture.session.getStoreNode()).thenAnswer(invocation -> { + retirementEntered.countDown(); + Assert.assertTrue("Retirement gate timed out", + allowRetirement.await(5, TimeUnit.SECONDS)); + return fixture.node; + }); + KvPageScanner scanner = new KvPageScanner(fixture.session, + ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL).setTable("table"), + observer -> { + response.set(observer); + return requestObserver; + }); + ExecutorService executor = Executors.newFixedThreadPool(2); + var error = Status.UNAVAILABLE.withDescription("scan retirement in progress") + .asRuntimeException(); + try { + Future notification = executor.submit(() -> response.get().onError(error)); + Assert.assertTrue(retirementEntered.await(5, TimeUnit.SECONDS)); + Future failure = executor.submit(() -> { + receiverThread.set(Thread.currentThread()); + receiverEntered.countDown(); + try { + scanner.hasNext(); + Assert.fail("Retiring failed scan must not become normal EOF"); + return null; + } catch (HgStoreClientException e) { + return e.getCause(); + } + }); + Assert.assertTrue(receiverEntered.await(5, TimeUnit.SECONDS)); + awaitBlockedReceiver(receiverThread.get(), failure); + try { + failure.get(1, TimeUnit.SECONDS); + Assert.fail("Receiver finished before failed pool retirement"); + } catch (TimeoutException expected) { + // Terminal publication must wait for retirement, even for a new receiver. + } + Assert.assertSame(fixture.channels, AbstractGrpcClient.channels.get(fixture.target)); + allowRetirement.countDown(); + Assert.assertSame(error, failure.get(5, TimeUnit.SECONDS)); + notification.get(5, TimeUnit.SECONDS); + Assert.assertNull(fixture.manager.getStoreNode(fixture.nodeId)); + Assert.assertFalse(AbstractGrpcClient.channels.containsKey(fixture.target)); + var retryStub = fixture.client.getStub(fixture.session); + Assert.assertFalse(Arrays.asList(fixture.channels).contains(retryStub.getChannel())); + verify(requestObserver, never()).onNext(any()); + } finally { + allowRetirement.countDown(); + executor.shutdownNow(); + Assert.assertTrue("Scan workers failed to stop", + executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + } + + @Test + public void testServerCompletionAndClientCloseFinishEmptyScan() { + for (boolean clientClose : new boolean[]{false, true}) { + AtomicReference> response = new AtomicReference<>(); + StreamObserver requestObserver = mock(StreamObserver.class); + doAnswer(invocation -> { + ScanStreamReq request = invocation.getArgument(0); + Assert.assertEquals(clientClose ? 1 : 0, request.getCloseFlag()); + response.get().onCompleted(); + return null; + }).when(requestObserver).onNext(any()); + KvPageScanner scanner = new KvPageScanner(session(), + ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL).setTable("table"), + observer -> { + response.set(observer); + return requestObserver; + }); + if (clientClose) { + scanner.close(); + } + Assert.assertFalse(scanner.hasNext()); + Assert.assertFalse(scanner.hasNext()); + verify(requestObserver).onNext(any()); + } + } + + @Test + public void testFinalPageSurvivesRequestCompletionFailure() throws Exception { + AtomicReference> response = new AtomicReference<>(); + StreamObserver requestObserver = mock(StreamObserver.class); + var completionFailure = new IllegalStateException("request completion failed"); + doAnswer(invocation -> { + throw completionFailure; + }).when(requestObserver).onCompleted(); + KvPageScanner scanner = new KvPageScanner(session(), + ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL).setTable("table"), + observer -> { + response.set(observer); + return requestObserver; + }); + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + try { + response.get().onNext(page(true, 7)); + } catch (IllegalStateException e) { + Assert.assertSame(completionFailure, e); + } + Future receiver = executor.submit(() -> { + Assert.assertTrue("The final page must still be delivered", scanner.hasNext()); + Assert.assertEquals(7, scanner.next().getKey().byteAt(0) & 0xff); + Assert.assertFalse(scanner.hasNext()); + }); + receiver.get(2, TimeUnit.SECONDS); + verify(requestObserver, never()).onNext(any()); + } finally { + executor.shutdownNow(); + Assert.assertTrue("Scan receiver failed to stop", + executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + + @Test + public void testRequestTerminalsWaitForRequestSend() throws Exception { + var clientError = KvPageScanner.class.getDeclaredMethod("clientError", String.class); + clientError.setAccessible(true); + for (int terminal = 0; terminal < 3; terminal++) { + boolean failed = terminal != 0; + boolean cancelled = terminal == 2; + CountDownLatch sending = new CountDownLatch(1); + CountDownLatch releaseSend = new CountDownLatch(1); + AtomicBoolean overlap = new AtomicBoolean(); + AtomicBoolean inOnNext = new AtomicBoolean(); + AtomicInteger terminals = new AtomicInteger(); + AtomicReference> response = new AtomicReference<>(); + AtomicReference callbackThread = new AtomicReference<>(); + StreamObserver requestObserver = mock(StreamObserver.class); + doAnswer(invocation -> { + inOnNext.set(true); + sending.countDown(); + try { + Assert.assertTrue("Request send gate timed out", + releaseSend.await(5, TimeUnit.SECONDS)); + return null; + } finally { + inOnNext.set(false); + } + }).when(requestObserver).onNext(any()); + Answer terminalAnswer = invocation -> { + if (inOnNext.get()) { + overlap.set(true); + } + terminals.incrementAndGet(); + return null; + }; + doAnswer(terminalAnswer).when(requestObserver).onCompleted(); + doAnswer(terminalAnswer).when(requestObserver).onError(any()); + KvPageScanner scanner = new KvPageScanner(session(), + ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL).setTable("table"), + observer -> { + response.set(observer); + return requestObserver; + }); + var error = (cancelled ? Status.CANCELLED : Status.INVALID_ARGUMENT) + .withDescription("scan failed").asRuntimeException(); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future receiver = executor.submit(scanner::hasNext); + Assert.assertTrue(sending.await(5, TimeUnit.SECONDS)); + Future callback = executor.submit(() -> { + callbackThread.set(Thread.currentThread()); + if (cancelled) { + clientError.invoke(scanner, "scan timed out"); + response.get().onError(error); + } else if (failed) { + response.get().onError(error); + } else { + response.get().onNext(page(true, 7)); + } + return null; + }); + awaitRequestTerminalBlocked(callbackThread, callback, terminals); + Assert.assertFalse("Request terminal overlapped onNext", overlap.get()); + releaseSend.countDown(); + callback.get(5, TimeUnit.SECONDS); + Assert.assertFalse("Request terminal overlapped onNext", overlap.get()); + if (failed) { + try { + receiver.get(5, TimeUnit.SECONDS); + Assert.fail("The original scan error must reach the caller"); + } catch (java.util.concurrent.ExecutionException e) { + Assert.assertSame(error, e.getCause().getCause()); + } + } else { + Assert.assertTrue(receiver.get(5, TimeUnit.SECONDS)); + Assert.assertEquals(7, scanner.next().getKey().byteAt(0) & 0xff); + Assert.assertFalse(scanner.hasNext()); + } + response.get().onCompleted(); + scanner.close(); + Assert.assertEquals(1, terminals.get()); + verify(requestObserver).onNext(any()); + if (cancelled) { + verify(requestObserver).onError(any()); + verify(requestObserver, never()).onCompleted(); + } else { + verify(requestObserver).onCompleted(); + verify(requestObserver, never()).onError(any()); + } + } finally { + releaseSend.countDown(); + executor.shutdownNow(); + Assert.assertTrue("Scan workers failed to stop", + executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + } + + @Test + public void testTimeoutCancellationPreservesLateErrorWithoutFurtherRequests() throws Exception { + try (FailureFixture fixture = new FailureFixture()) { + AtomicReference> response = new AtomicReference<>(); + StreamObserver requestObserver = mock(StreamObserver.class); + KvPageScanner scanner = new KvPageScanner(fixture.session, + ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL).setTable("table"), + observer -> { + response.set(observer); + return requestObserver; + }); + // Exercise receive-timeout cancellation without waiting for the configured timeout. + var clientError = KvPageScanner.class.getDeclaredMethod("clientError", String.class); + clientError.setAccessible(true); + clientError.invoke(scanner, "scan timed out"); + clientError.invoke(scanner, "late timeout"); + scanner.close(); + var error = Status.UNAVAILABLE.withDescription("late transport failure") + .asRuntimeException(); + response.get().onError(error); + response.get().onCompleted(); + verify(requestObserver).onError(any()); + verify(requestObserver, never()).onNext(any()); + verify(requestObserver, never()).onCompleted(); + Assert.assertNull(fixture.manager.getStoreNode(fixture.nodeId)); + Assert.assertFalse(AbstractGrpcClient.channels.containsKey(fixture.target)); + try { + scanner.hasNext(); + Assert.fail("The original transport error must remain visible"); + } catch (HgStoreClientException e) { + Assert.assertSame(error, e.getCause()); + } + } + } + + private static void awaitRequestTerminalBlocked(AtomicReference callbackThread, + Future callback, + AtomicInteger terminals) throws Exception { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + while (System.nanoTime() < deadline) { + Thread thread = callbackThread.get(); + if (terminals.get() > 0 || callback.isDone() || + thread != null && thread.getState() == Thread.State.BLOCKED) { + break; + } + Thread.sleep(1); + } + Assert.assertEquals("Request terminal must wait until onNext returns", 0, terminals.get()); + Assert.assertNotNull(callbackThread.get()); + Assert.assertEquals(Thread.State.BLOCKED, callbackThread.get().getState()); + } + + private static void awaitBlockedReceiver(Thread receiver, Future result) throws Exception { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + while (receiver.getState() != Thread.State.TIMED_WAITING && System.nanoTime() < deadline) { + if (result.isDone()) { + result.get(5, TimeUnit.SECONDS); + Assert.fail("Scanner returned before waiting for its response"); + } + Thread.sleep(1); + } + Assert.assertEquals("Scanner must be blocked waiting for its response", + Thread.State.TIMED_WAITING, receiver.getState()); + } + + @Test + public void testCancelledAndInvalidScansPreserveHealthyPool() { + for (Status status : List.of(Status.CANCELLED, Status.INVALID_ARGUMENT)) { + try (FailureFixture fixture = new FailureFixture()) { + fixture.failScan(status); + Assert.assertSame(fixture.node, fixture.manager.getStoreNode(fixture.nodeId)); + Assert.assertSame(fixture.channels, AbstractGrpcClient.channels.get(fixture.target)); + for (ManagedChannel channel : fixture.channels) { + verify(channel, never()).shutdown(); + } + } + } + } + + @Test + public void testLateScanFailurePreservesSameAddressReplacement() { + try (FailureFixture fixture = new FailureFixture()) { + HgStoreNode replacement = mock(HgStoreNode.class); + when(replacement.getNodeId()).thenReturn(fixture.nodeId); + when(replacement.getAddress()).thenReturn(fixture.target); + fixture.manager.addNode(fixture.graph, replacement); + fixture.failScan(Status.UNAVAILABLE); + + Assert.assertSame(replacement, fixture.manager.getStoreNode(fixture.nodeId)); + Assert.assertSame(fixture.channels, AbstractGrpcClient.channels.get(fixture.target)); + for (ManagedChannel channel : fixture.channels) { + verify(channel, never()).shutdown(); + } + } + } + + private static class FailureFixture implements AutoCloseable { + + private final long nodeId = NODE_IDS.decrementAndGet(); + private final String graph = "scan-recovery-" + this.nodeId; + private final String target = this.graph + ":8500"; + private final HgStoreNodeManager manager = HgStoreNodeManager.getInstance(); + private final HgStoreNode node = mock(HgStoreNode.class); + private final HgStoreNodeSession session = mock(HgStoreNodeSession.class); + private final ManagedChannel[] channels = new ManagedChannel[AbstractGrpcClient.concurrency]; + private final GrpcStoreStreamClient client = new GrpcStoreStreamClient() { + @Override + protected ManagedChannel createChannel(String target) { + return mock(ManagedChannel.class); + } + }; + + FailureFixture() { + when(this.node.getNodeId()).thenReturn(this.nodeId); + when(this.node.getAddress()).thenReturn(this.target); + when(this.session.getStoreNode()).thenReturn(this.node); + when(this.session.getGraphName()).thenReturn(this.graph); + this.manager.addNode(this.graph, this.node); + Arrays.setAll(this.channels, ignored -> mock(ManagedChannel.class)); + AbstractGrpcClient.channels.put(this.target, this.channels); + } + + void failScan(Status status) { + this.failScan(status, false); + } + + void failScan(Status status, boolean failCompletion) { + var error = status.withDescription("scan transport failure").asRuntimeException(); + KvPageScanner scanner = new KvPageScanner(this.session, + ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL).setTable("table"), + response -> new StreamObserver() { + @Override + public void onNext(ScanStreamReq request) { + response.onError(error); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + if (failCompletion) { + throw new IllegalStateException("request completion failed"); + } + } + }); + try { + scanner.hasNext(); + Assert.fail("Failed scan must remain visible to the caller"); + } catch (HgStoreClientException e) { + Assert.assertSame(error, e.getCause()); + } + } + + @Override + public void close() { + HgStoreNode currentNode = this.manager.getStoreNode(this.nodeId); + if (currentNode != null) { + this.manager.notifying(this.graph, + HgStoreNotice.of(this.nodeId, HgNodeStatus.NOT_WORK), currentNode); + } + AbstractGrpcClient.closeChannel(this.target); + } + } + @Test public void testKeepsExplicitPageSizeAndRequestsNextPageOnDemand() { List requests = new ArrayList<>(); diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/query/CommonKvStreamObserverTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/query/CommonKvStreamObserverTest.java new file mode 100644 index 0000000000..71e88f7fa7 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/query/CommonKvStreamObserverTest.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.hugegraph.store.client.query; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hugegraph.store.HgKvIterator; +import org.junit.Test; + +public class CommonKvStreamObserverTest { + + @Test + public void testCompletedEmptyStreamSkipsTimedPoll() throws Exception { + RecordingQueue queue = new RecordingQueue(); + CommonKvStreamObserver, Integer> observer = observer(queue); + observer.onCompleted(); + StreamKvIterator stream = stream(observer); + assertFalse(stream.hasNext()); + stream.close(); + assertEquals("completed empty stream must not enter timed poll", 0, queue.emptyTimedPolls.get()); + } + + @Test + public void testCompletedSingleStreamAndCloseSkipEmptyTimedPoll() throws Exception { + RecordingQueue queue = new RecordingQueue(); + CommonKvStreamObserver, Integer> observer = observer(queue); + observer.onNext(Collections.singletonList(1)); + observer.onCompleted(); + StreamKvIterator stream = stream(observer); + assertTrue(stream.hasNext()); + assertEquals(Integer.valueOf(1), stream.next()); + assertFalse(stream.hasNext()); + assertFalse(stream.hasNext()); + stream.close(); + assertEquals("exhausted terminal stream must not enter empty timed poll", 0, queue.emptyTimedPolls.get()); + } + + @Test + public void testCompletedThreeStreamsMergeAndCloseSkipEmptyTimedPoll() throws Exception { + List queues = new ArrayList<>(); + MultiStreamIterator merged = merged(queues); + List actual = drain(merged); + merged.close(); + assertEquals(Arrays.asList(1, 2, 3), actual); + assertEquals("merged terminal streams and close must not enter empty timed poll", 0, + queues.stream().mapToInt(queue -> queue.emptyTimedPolls.get()).sum()); + } + + @Test(timeout = 5000) + public void testFinalBatchPublishedAtTerminalBoundaryIsNotLost() throws Exception { + CountDownLatch boundary = new CountDownLatch(1); + CountDownLatch published = new CountDownLatch(1); + GatedQueue queue = new GatedQueue(boundary, published); + CommonKvStreamObserver, Integer> observer = observer(queue); + StreamKvIterator stream = stream(observer); + AtomicReference failure = new AtomicReference<>(); + Thread producer = new Thread(() -> { + try { + assertTrue("consumer must reach terminal/queue boundary", boundary.await(2, TimeUnit.SECONDS)); + observer.onNext(Collections.singletonList(7)); + observer.onCompleted(); + } catch (Throwable error) { + failure.set(error); + } finally { + published.countDown(); + } + }, "publish-final-client-batch"); + producer.start(); + try { + assertTrue("final batch must remain visible when terminal state changes", stream.hasNext()); + assertEquals(Integer.valueOf(7), stream.next()); + assertFalse(stream.hasNext()); + assertEquals(null, failure.get()); + } finally { + boundary.countDown(); + published.countDown(); + producer.join(2000); + assertFalse("producer must finish", producer.isAlive()); + stream.close(); + } + } + + @Test(timeout = 5000) + public void testAcceptedResponseRefreshesTimeoutBeforeParsing() throws Exception { + CountDownLatch parsing = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicInteger polls = new AtomicInteger(); + RecordingQueue queue = new RecordingQueue() { + private static final long serialVersionUID = 1L; + + @Override + public Iterator poll(long timeout, TimeUnit unit) throws InterruptedException { + if (polls.getAndIncrement() == 0) { + // Exercise the timeout check while the accepted response is still being parsed. + return null; + } + release.countDown(); + return super.poll(timeout, unit); + } + }; + CommonKvStreamObserver, Integer> observer = parsingObserver(parsing, release); + observer.setTimeout(1000); + Field queueField = CommonKvStreamObserver.class.getDeclaredField("queue"); + queueField.setAccessible(true); + queueField.set(observer, queue); + Field current = CommonKvStreamObserver.class.getDeclaredField("current"); + current.setAccessible(true); + current.setLong(observer, System.nanoTime() - TimeUnit.SECONDS.toNanos(2)); + AtomicReference failure = new AtomicReference<>(); + Thread producer = new Thread(() -> { + try { + observer.onNext(Collections.singletonList(7)); + } catch (Throwable error) { + failure.set(error); + } + }); + producer.start(); + try { + assertTrue(parsing.await(2, TimeUnit.SECONDS)); + assertEquals(Integer.valueOf(7), observer.consume().next()); + producer.join(2000); + assertFalse(producer.isAlive()); + assertEquals(null, failure.get()); + assertEquals(null, observer.consume()); + } finally { + release.countDown(); + producer.join(2000); + observer.clear(); + } + } + + @Test(timeout = 5000) + public void testCompletionWaitsForResponseBeingParsed() throws Exception { + CountDownLatch parsing = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + CommonKvStreamObserver, Integer> observer = parsingObserver(parsing, release); + Thread producer = new Thread(() -> observer.onNext(Collections.singletonList(7))); + producer.start(); + try { + assertTrue(parsing.await(2, TimeUnit.SECONDS)); + observer.onCompleted(); + assertFalse("completion must wait for the accepted batch", observer.isServerFinished()); + release.countDown(); + producer.join(2000); + assertFalse(producer.isAlive()); + assertTrue(observer.isServerFinished()); + assertEquals(Integer.valueOf(7), observer.consume().next()); + assertEquals(null, observer.consume()); + } finally { + release.countDown(); + producer.join(2000); + observer.clear(); + } + } + + @Test(timeout = 5000) + public void testCloseDiscardsResponseBeingParsed() throws Exception { + CountDownLatch parsing = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + CommonKvStreamObserver, Integer> observer = parsingObserver(parsing, release); + AtomicInteger cancellations = new AtomicInteger(); + observer.setTransferComplete(finished -> { + assertFalse(finished); + cancellations.incrementAndGet(); + }); + Thread producer = new Thread(() -> observer.onNext(Collections.singletonList(7))); + producer.start(); + try { + assertTrue(parsing.await(2, TimeUnit.SECONDS)); + observer.clear(); + observer.clear(); + assertEquals(null, observer.consume()); + release.countDown(); + producer.join(2000); + assertFalse(producer.isAlive()); + observer.onCompleted(); + observer.onError(new IllegalStateException("late error")); + assertEquals(null, observer.consume()); + assertEquals(1, cancellations.get()); + } finally { + release.countDown(); + producer.join(2000); + } + } + + @Test(timeout = 5000) + public void testCancellationBypassesBlockedRequestSender() throws Exception { + CountDownLatch sending = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + CountDownLatch cancelled = new CountDownLatch(1); + CommonKvStreamObserver, Integer> observer = observer(new RecordingQueue()); + observer.setRequestSender(ignored -> { + sending.countDown(); + try { + release.await(); + } catch (InterruptedException error) { + Thread.currentThread().interrupt(); + throw new AssertionError(error); + } + }); + AtomicInteger cancellations = new AtomicInteger(); + observer.setTransferComplete(finished -> { + assertFalse(finished); + cancellations.incrementAndGet(); + cancelled.countDown(); + release.countDown(); + }); + Thread sender = new Thread(observer::sendRequest); + Thread closer = new Thread(observer::clear); + sender.start(); + try { + assertTrue(sending.await(2, TimeUnit.SECONDS)); + closer.start(); + assertTrue("cancel must not wait for the blocked sender", cancelled.await(2, TimeUnit.SECONDS)); + sender.join(1000); + closer.join(1000); + assertFalse(sender.isAlive()); + assertFalse(closer.isAlive()); + assertEquals(1, cancellations.get()); + } finally { + release.countDown(); + sender.join(1000); + closer.join(1000); + } + } + + @Test + public void testTerminalCallbacksAreIdempotentAndDoNotResurrectStream() throws Exception { + CommonKvStreamObserver, Integer> observer = observer(new RecordingQueue()); + observer.onError(new IllegalStateException("first error")); + observer.onCompleted(); + observer.onError(new IllegalStateException("duplicate error")); + observer.onNext(Collections.singletonList(7)); + assertTrue(observer.isServerFinished()); + Iterator error = observer.consume(); + assertTrue(error instanceof ErrorMessageIterator); + assertEquals(null, observer.consume()); + + CommonKvStreamObserver, Integer> completed = observer(new RecordingQueue()); + AtomicInteger halfCloses = new AtomicInteger(); + completed.setTransferComplete(finished -> { + assertTrue(finished); + halfCloses.incrementAndGet(); + }); + completed.onCompleted(); + completed.onCompleted(); + completed.onError(new IllegalStateException("late error")); + completed.onNext(Collections.singletonList(7)); + completed.clear(); + completed.clear(); + assertEquals(1, halfCloses.get()); + assertEquals(null, completed.consume()); + } + + @Test(timeout = 5000) + public void testErrorDiscardsResponseBeingParsed() throws Exception { + CountDownLatch parsing = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + CommonKvStreamObserver, Integer> observer = parsingObserver(parsing, release); + Thread producer = new Thread(() -> observer.onNext(Collections.singletonList(7))); + producer.start(); + try { + assertTrue(parsing.await(2, TimeUnit.SECONDS)); + observer.onError(new IllegalStateException("transport error")); + observer.onCompleted(); + assertTrue("error does not wait for a discarded batch", observer.isServerFinished()); + assertTrue(observer.consume() instanceof ErrorMessageIterator); + assertEquals(null, observer.consume()); + release.countDown(); + producer.join(2000); + assertFalse(producer.isAlive()); + assertEquals(null, observer.consume()); + } finally { + release.countDown(); + producer.join(2000); + observer.clear(); + } + } + + @Test + public void testParserErrorReleasesAcceptedResponse() { + CommonKvStreamObserver, Integer> observer = new CommonKvStreamObserver<>(values -> { + throw new AssertionError("parser error"); + }, ignored -> ResultState.FINISHED); + try { + observer.onNext(Collections.singletonList(7)); + throw new AssertionError("parser Error must propagate"); + } catch (AssertionError error) { + assertEquals("parser error", error.getMessage()); + } + assertTrue(observer.isServerFinished()); + assertTrue(observer.consume() instanceof ErrorMessageIterator); + assertEquals(null, observer.consume()); + } + + @Test(timeout = 5000) + public void testParserErrorOverridesConcurrentCompletion() throws Exception { + assertParserFailureOverridesCompletion(new AssertionError("parser error")); + } + + @Test(timeout = 5000) + public void testParserExceptionOverridesConcurrentCompletion() throws Exception { + assertParserFailureOverridesCompletion(new IllegalStateException("parser exception")); + } + + private static void assertParserFailureOverridesCompletion(Throwable failure) throws Exception { + CountDownLatch parsing = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + CommonKvStreamObserver, Integer> observer = new CommonKvStreamObserver<>(values -> { + parsing.countDown(); + await(release); + if (failure instanceof Error) { + throw (Error) failure; + } + throw (RuntimeException) failure; + }, ignored -> ResultState.FINISHED); + observer.setRequestSender(ignored -> { }); + observer.setTransferComplete(ignored -> { }); + AtomicReference propagated = new AtomicReference<>(); + Thread producer = new Thread(() -> { + try { + observer.onNext(Collections.singletonList(7)); + } catch (Throwable error) { + propagated.set(error); + } + }); + producer.start(); + try { + assertTrue(parsing.await(2, TimeUnit.SECONDS)); + observer.onCompleted(); + assertFalse(observer.isServerFinished()); + release.countDown(); + producer.join(2000); + assertFalse(producer.isAlive()); + assertEquals(failure instanceof Error ? failure : null, propagated.get()); + assertTrue(observer.isServerFinished()); + assertTrue("parser failure must remain visible to the consumer", + observer.consume() instanceof ErrorMessageIterator); + assertEquals(null, observer.consume()); + } finally { + release.countDown(); + producer.join(2000); + observer.clear(); + } + } + + private static CommonKvStreamObserver, Integer> parsingObserver( + CountDownLatch parsing, CountDownLatch release) { + CommonKvStreamObserver, Integer> observer = new CommonKvStreamObserver<>(values -> { + parsing.countDown(); + await(release); + return values.iterator(); + }, ignored -> ResultState.FINISHED); + observer.setRequestSender(ignored -> { }); + observer.setTransferComplete(ignored -> { }); + return observer; + } + + private static void await(CountDownLatch latch) { + try { + assertTrue(latch.await(2, TimeUnit.SECONDS)); + } catch (InterruptedException error) { + Thread.currentThread().interrupt(); + throw new AssertionError(error); + } + } + + private static List drain(HgKvIterator stream) { + List values = new ArrayList<>(); + while (stream.hasNext()) { + values.add(stream.next()); + } + return values; + } + + private static MultiStreamIterator merged(List queues) throws Exception { + List> streams = new ArrayList<>(); + for (int value : Arrays.asList(1, 2, 3)) { + RecordingQueue queue = new RecordingQueue(); + queues.add(queue); + CommonKvStreamObserver, Integer> observer = observer(queue); + observer.onNext(Collections.singletonList(value)); + observer.onCompleted(); + streams.add(stream(observer)); + } + return new MultiStreamIterator<>(streams); + } + + private static StreamKvIterator stream(CommonKvStreamObserver, Integer> observer) { + return new StreamKvIterator<>(ignored -> observer.clear(), observer::consume); + } + + private static CommonKvStreamObserver, Integer> observer(RecordingQueue queue) throws Exception { + CommonKvStreamObserver, Integer> observer = + new CommonKvStreamObserver<>(List::iterator, ignored -> ResultState.FINISHED); + observer.setRequestSender(ignored -> { }); + observer.setTransferComplete(ignored -> { }); + Field field = CommonKvStreamObserver.class.getDeclaredField("queue"); + field.setAccessible(true); + field.set(observer, queue); + return observer; + } + + private static class RecordingQueue extends LinkedBlockingQueue> { + + private static final long serialVersionUID = 1L; + final AtomicInteger emptyTimedPolls = new AtomicInteger(); + + @Override + public Iterator poll(long timeout, TimeUnit unit) throws InterruptedException { + if (super.isEmpty()) { + this.emptyTimedPolls.incrementAndGet(); + } + return super.poll(timeout, unit); + } + } + + private static final class GatedQueue extends RecordingQueue { + + private static final long serialVersionUID = 1L; + private final CountDownLatch boundary; + private final CountDownLatch published; + private final AtomicBoolean armed = new AtomicBoolean(true); + + private GatedQueue(CountDownLatch boundary, CountDownLatch published) { + this.boundary = boundary; + this.published = published; + } + + private void publishAtBoundary() { + if (this.armed.compareAndSet(true, false)) { + this.boundary.countDown(); + try { + assertTrue("final batch must be published", this.published.await(2, TimeUnit.SECONDS)); + } catch (InterruptedException error) { + Thread.currentThread().interrupt(); + throw new AssertionError(error); + } + } + } + + @Override + public boolean isEmpty() { + boolean empty = super.isEmpty(); + this.publishAtBoundary(); + return empty; + } + + @Override + public Iterator poll(long timeout, TimeUnit unit) throws InterruptedException { + this.publishAtBoundary(); + return super.poll(timeout, unit); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/query/MultiStreamIteratorTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/query/MultiStreamIteratorTest.java new file mode 100644 index 0000000000..675c6cb9b9 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/client/query/MultiStreamIteratorTest.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.hugegraph.store.client.query; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.List; + +import org.apache.hugegraph.store.HgKvIterator; +import org.junit.Test; + +public class MultiStreamIteratorTest { + + @Test + public void testCloseCancelsEveryChildBeforeIterationAndIsTerminal() { + HgKvIterator first = mock(HgKvIterator.class); + HgKvIterator second = mock(HgKvIterator.class); + MultiStreamIterator composite = new MultiStreamIterator<>(List.of(first, second, first)); + composite.close(); + composite.close(); + assertFalse(composite.hasNext()); + verify(first).close(); + verify(second).close(); + verifyNoMoreInteractions(first, second); + try { + composite.next(); + fail("Closed iterator must not resume a child stream"); + } catch (java.util.NoSuchElementException expected) { + // Closed is terminal. + } + } + + @Test + public void testCloseAttemptsAllChildrenAndPreservesPrimaryFailure() { + HgKvIterator first = mock(HgKvIterator.class); + HgKvIterator second = mock(HgKvIterator.class); + HgKvIterator third = mock(HgKvIterator.class); + RuntimeException primary = new IllegalStateException("first"); + RuntimeException later = new IllegalArgumentException("second"); + doThrow(primary).when(first).close(); + doThrow(later).when(second).close(); + MultiStreamIterator composite = new MultiStreamIterator<>(List.of(first, second, third)); + try { + composite.close(); + fail("Cleanup failure must be visible"); + } catch (RuntimeException error) { + assertSame(primary, error); + assertEquals(1, error.getSuppressed().length); + assertSame(later, error.getSuppressed()[0]); + } + verify(first).close(); + verify(second).close(); + verify(third).close(); + assertFalse(composite.hasNext()); + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/common/Base58EncoderTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/common/Base58EncoderTest.java new file mode 100644 index 0000000000..b5f7af35a3 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/common/Base58EncoderTest.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.hugegraph.store.common; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Random; + +import org.apache.hugegraph.store.client.util.Base58; +import org.apache.hugegraph.store.util.Base58Encoder; +import org.junit.Test; + +public class Base58EncoderTest { + + @Test + public void testKnownVectors() { + assertEquals("", Base58Encoder.convertToBase58(new byte[0])); + assertEquals("2NEpo7TZRRrLZSi2U", + Base58Encoder.convertToBase58("Hello World!".getBytes(StandardCharsets.US_ASCII))); + assertEquals("5Q", Base58Encoder.convertToBase58(new byte[]{(byte) 0xff})); + assertArrayEquals("Hello World!".getBytes(StandardCharsets.US_ASCII), + Base58Encoder.convertFromBase58("2NEpo7TZRRrLZSi2U")); + assertArrayEquals(new byte[0], Base58Encoder.convertFromBase58("")); + } + + @Test + public void testLeadingZeroBytesBecomeLeadingOnes() { + assertEquals("1", Base58Encoder.convertToBase58(new byte[]{0})); + assertEquals("111", Base58Encoder.convertToBase58(new byte[]{0, 0, 0})); + assertEquals("112", Base58Encoder.convertToBase58(new byte[]{0, 0, 1})); + + assertArrayEquals(new byte[]{0}, Base58Encoder.convertFromBase58("1")); + assertArrayEquals(new byte[]{0, 0, 1}, Base58Encoder.convertFromBase58("112")); + } + + @Test + public void testRoundTripDoesNotMutateInput() { + Random random = new Random(20261011L); + for (int i = 0; i < 200; i++) { + byte[] input = new byte[random.nextInt(40)]; + random.nextBytes(input); + if (input.length > 0 && random.nextBoolean()) { + input[0] = 0; + } + byte[] snapshot = input.clone(); + + String encoded = Base58Encoder.convertToBase58(input); + + assertArrayEquals("encoding must not modify its argument", snapshot, input); + assertArrayEquals(input, Base58Encoder.convertFromBase58(encoded)); + // Store-side and client-side encoders must agree on the wire representation + assertEquals(Base58.encode(input), encoded); + } + } + + @Test + public void testConvertToBigIntIgnoresLeadingZeros() { + assertEquals(BigInteger.ZERO, Base58Encoder.convertToBigInt("1")); + assertEquals(BigInteger.valueOf(57), Base58Encoder.convertToBigInt("z")); + assertEquals(BigInteger.valueOf(58), Base58Encoder.convertToBigInt("121")); + } + + @Test + public void testRejectsCharactersOutsideTheAlphabet() { + // '0', 'O', 'I' and 'l' are excluded from the Base58 alphabet to avoid ambiguity + for (String invalid : new String[]{"0", "O", "I", "l", "2N+", "abc def", "é"}) { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> Base58Encoder.convertFromBase58(invalid)); + assertTrue(e.getMessage(), e.getMessage().startsWith("Invalid character")); + } + assertThrows(IllegalArgumentException.class, () -> Base58Encoder.convertToBigInt("0")); + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/common/CommonSuiteTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/common/CommonSuiteTest.java index 2ef65cf3b0..350d812166 100644 --- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/common/CommonSuiteTest.java +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/common/CommonSuiteTest.java @@ -17,6 +17,7 @@ package org.apache.hugegraph.store.common; +import org.apache.hugegraph.store.client.HgPairTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -25,7 +26,10 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ ByteBufferAllocatorTest.class, - KVByteBufferTest.class + KVByteBufferTest.class, + Base58EncoderTest.class, + KeyUtilTest.class, + HgPairTest.class }) @Slf4j diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/common/KeyUtilTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/common/KeyUtilTest.java new file mode 100644 index 0000000000..7eef58889b --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/common/KeyUtilTest.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.hugegraph.store.common; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.apache.hugegraph.backend.BinaryId; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.IdUtil; +import org.apache.hugegraph.store.constant.HugeServerTables; +import org.apache.hugegraph.store.query.util.KeyUtil; +import org.apache.hugegraph.type.define.Directions; +import org.junit.Test; + +public class KeyUtilTest { + + private static final Id OWNER = IdGenerator.of("owner-vertex"); + private static final Id OTHER = IdGenerator.of(987654321L); + + private static EdgeId edge(Directions direction) { + return new EdgeId(OWNER, direction, IdGenerator.of(3L), IdGenerator.of(3L), "", OTHER); + } + + @Test + public void testEdgeKeysOfBothDirectionsRouteToTheOwnerVertex() { + byte[] expected = KeyUtil.idToBytes(OWNER); + for (Directions direction : new Directions[]{Directions.OUT, Directions.IN}) { + byte[] edgeKey = IdUtil.asBytes(edge(direction)); + String table = direction == Directions.OUT ? HugeServerTables.OUT_EDGE_TABLE : + HugeServerTables.IN_EDGE_TABLE; + + byte[] ownerKey = KeyUtil.getOwnerKey(table, edgeKey); + + assertArrayEquals(expected, ownerKey); + // The owner key is a serialized id that decodes back to the owner vertex + assertEquals(OWNER, IdUtil.fromBytes(ownerKey)); + } + } + + @Test + public void testOwnerKeyMatchesVertexKeySoEdgesColocateWithTheirVertex() { + byte[] vertexKey = IdUtil.asBytes(OWNER); + byte[] edgeOwnerKey = KeyUtil.getOwnerKey(HugeServerTables.OUT_EDGE_TABLE, + IdUtil.asBytes(edge(Directions.OUT))); + assertArrayEquals(KeyUtil.getOwnerKey(HugeServerTables.VERTEX_TABLE, vertexKey), + edgeOwnerKey); + } + + @Test + public void testNonEdgeTablesReturnTheKeyItself() { + byte[] key = IdUtil.asBytes(edge(Directions.OUT)); + for (String table : new String[]{HugeServerTables.VERTEX_TABLE, + HugeServerTables.INDEX_TABLE, + HugeServerTables.UNKNOWN_TABLE, null}) { + assertSame(key, KeyUtil.getOwnerKey(table, key)); + } + assertFalse(HugeServerTables.isEdgeTable(null)); + assertFalse(HugeServerTables.isEdgeTable("g+oe ")); + assertTrue(HugeServerTables.isEdgeTable("g+ie")); + } + + @Test + public void testNullOrEmptyKeyYieldsEmptyOwnerKey() { + assertEquals(0, KeyUtil.getOwnerKey(HugeServerTables.OUT_EDGE_TABLE, null).length); + assertEquals(0, KeyUtil.getOwnerKey(HugeServerTables.IN_EDGE_TABLE, new byte[0]).length); + assertEquals(0, KeyUtil.getOwnerKey(HugeServerTables.VERTEX_TABLE, null).length); + } + + @Test + public void testGetOwnerIdUnwrapsBinaryAndEdgeIds() { + assertArrayEquals(OWNER.asBytes(), KeyUtil.getOwnerId(OWNER)); + assertArrayEquals(OWNER.asBytes(), KeyUtil.getOwnerId(edge(Directions.IN))); + + EdgeId edgeId = edge(Directions.OUT); + BinaryId wrapped = new BinaryId(IdUtil.asBytes(edgeId), edgeId); + assertArrayEquals(OWNER.asBytes(), KeyUtil.getOwnerId(wrapped)); + + assertEquals(0, KeyUtil.getOwnerId(null).length); + assertEquals(0, KeyUtil.getOwnerId(new BinaryId(new byte[]{1}, null)).length); + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/BatchGraphIsolationTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/BatchGraphIsolationTest.java index 3d78821a63..da159100ce 100644 --- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/BatchGraphIsolationTest.java +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/BatchGraphIsolationTest.java @@ -63,6 +63,7 @@ public class BatchGraphIsolationTest { private static final int PARTITION_ID = 0; private static final int EMPTY_PARTITION_ID = 1; + private static final int ALLOCATION_PARTITION_ID = 2; private static final int KEY_CODE = 0; private static final int EMPTY_PARTITION_KEY_CODE = 32768; private static final byte[] SHARED_KEY = @@ -86,7 +87,7 @@ public static void setup() throws IOException { HgStoreEngineOptions.FakePdOptions fakePdOptions = new HgStoreEngineOptions.FakePdOptions(); - fakePdOptions.setPartitionCount(2); + fakePdOptions.setPartitionCount(3); fakePdOptions.setPeersList("127.0.0.1"); fakePdOptions.setStoreList("127.0.0.1"); options.setFakePdOptions(fakePdOptions); @@ -101,7 +102,8 @@ public String getDbDataPath(int partitionId, String dbName) { @Override public boolean hasPartition(String graphName, int partitionId) { - return partitionId == PARTITION_ID || partitionId == EMPTY_PARTITION_ID; + return partitionId == PARTITION_ID || partitionId == EMPTY_PARTITION_ID || + partitionId == ALLOCATION_PARTITION_ID; } @Override @@ -123,6 +125,18 @@ public static void teardown() { } } + @Test + public void testGraphIdAllocationDoesNotCreateVertexTable() { + String graph = "graph-id-allocation"; + + Assert.assertFalse(handler.existsTable(graph, ALLOCATION_PARTITION_ID, VERTEX_TABLE)); + + ((BusinessHandlerImpl) handler).getKeyCreator() + .getGraphIdOrCreate(ALLOCATION_PARTITION_ID, graph); + + Assert.assertFalse(handler.existsTable(graph, ALLOCATION_PARTITION_ID, VERTEX_TABLE)); + } + @Test(timeout = 5000L) public void testFirstBatchOnEmptyPartitionCompletes() { String graph = "first-batch-empty-partition"; diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/CoreSuiteTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/CoreSuiteTest.java index 830c348244..e9b5be8c48 100644 --- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/CoreSuiteTest.java +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/CoreSuiteTest.java @@ -17,8 +17,10 @@ package org.apache.hugegraph.store.core; +import org.apache.hugegraph.store.HeartbeatShutdownTest; import org.apache.hugegraph.store.StoreIdChangeTest; import org.apache.hugegraph.store.core.snapshot.HgSnapshotHandlerTest; +import org.apache.hugegraph.store.core.store.meta.PartitionMetaCodecTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -45,8 +47,11 @@ // HgBusinessImplTest.class @RunWith(Suite.class) @Suite.SuiteClasses({ + HeartbeatShutdownTest.class, + MetricSessionTest.class, HgSnapshotHandlerTest.class, - StoreIdChangeTest.class + StoreIdChangeTest.class, + PartitionMetaCodecTest.class }) @Slf4j public class CoreSuiteTest { diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/MetricSessionTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/MetricSessionTest.java new file mode 100644 index 0000000000..8bafb25c43 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/MetricSessionTest.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.hugegraph.store.core; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.apache.hugegraph.rocksdb.access.RocksDBFactory; +import org.apache.hugegraph.rocksdb.access.RocksDBSession; +import org.apache.hugegraph.store.HgStoreEngine; +import org.apache.hugegraph.store.business.BusinessHandler; +import org.apache.hugegraph.store.metric.SystemMetricService; +import org.apache.hugegraph.store.rocksdb.BaseRocksDbTest; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.rocksdb.MemoryUsageType; +import org.rocksdb.RocksDB; +import org.rocksdb.Statistics; +import org.rocksdb.TickerType; + +public class MetricSessionTest extends BaseRocksDbTest { + + @Test + public void testRepeatedMetricsReturnBorrowedSessions() throws Exception { + assertMetricsReturnSessions(false); + } + + @Test + public void testFailedMetricsReturnBorrowedSessions() throws Exception { + assertMetricsReturnSessions(true); + } + + private static void assertMetricsReturnSessions(boolean failStatistics) throws Exception { + RocksDBFactory factory = RocksDBFactory.getInstance(); + String name = "metric-session-" + UUID.randomUUID(); + RocksDBSession borrowed = factory.createGraphDB(Files.createTempDirectory(name).toString(), name); + Field sessionsField = RocksDBFactory.class.getDeclaredField("dbSessionMap"); + sessionsField.setAccessible(true); + @SuppressWarnings("unchecked") + Map sessions = (Map) sessionsField.get(factory); + RocksDBSession owner = sessions.get(name); + Field statisticsField = RocksDBSession.class.getDeclaredField("rocksDbStats"); + statisticsField.setAccessible(true); + RocksDB nativeOwner = owner.getDB(); + Statistics original = owner.getRocksDbStats(); + Statistics failing = Mockito.mock(Statistics.class); + Mockito.when(failing.getTickerCount(Mockito.any(TickerType.class))) + .thenThrow(new IllegalStateException("statistics fixture")); + try { + if (failStatistics) { + statisticsField.set(owner, failing); + } + HgStoreEngine engine = Mockito.mock(HgStoreEngine.class); + BusinessHandler business = Mockito.mock(BusinessHandler.class); + Mockito.when(engine.getBusinessHandler()).thenReturn(business); + Map memory = new HashMap<>(); + for (MemoryUsageType type : MemoryUsageType.values()) { + memory.put(type, 0L); + } + Mockito.when(business.getApproximateMemoryUsageByType(null)).thenReturn(memory); + SystemMetricService service = new SystemMetricService(); + service.setStoreEngine(engine); + Method collect = SystemMetricService.class.getDeclaredMethod("loadRocksDbInfo", Map.class); + collect.setAccessible(true); + int baseline = owner.getRefCount(); + for (int i = 0; i < 3; i++) { + Map metrics = new HashMap<>(); + collect.invoke(service, metrics); + if (!failStatistics) { + Assert.assertTrue(metrics.containsKey("rocksdb.graph." + name + ".number_keys_written")); + } + Assert.assertEquals("metric collection must return its borrowed session", baseline, + owner.getRefCount()); + } + if (failStatistics) { + Mockito.verify(failing, Mockito.times(3)).getTickerCount(TickerType.NUMBER_KEYS_WRITTEN); + } + statisticsField.set(owner, original); + borrowed.close(); + factory.releaseGraphDB(name); + Assert.assertFalse("the final owner release must close native RocksDB", nativeOwner.isOwningHandle()); + } finally { + statisticsField.set(owner, original); + borrowed.close(); + factory.releaseGraphDB(name); + // Also release the native fixture if the old implementation leaked its borrowed references. + Method shutdown = RocksDBSession.class.getDeclaredMethod("shutdown"); + shutdown.setAccessible(true); + shutdown.invoke(owner); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/raft/HgStoreStateMachineTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/raft/HgStoreStateMachineTest.java index 3e8545447b..5f75eab987 100644 --- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/raft/HgStoreStateMachineTest.java +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/raft/HgStoreStateMachineTest.java @@ -130,6 +130,11 @@ public void testOnApply() { final java.util.Iterator iterator = tasks.iterator(); Task task; + @Override + public void setAutoCommitPerLog(boolean autoCommit) { + // This test iterator does not model commit behavior. + } + @Override public ByteBuffer getData() { return task.getData(); diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/store/meta/PartitionMetaCodecTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/store/meta/PartitionMetaCodecTest.java new file mode 100644 index 0000000000..383306c4f8 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/store/meta/PartitionMetaCodecTest.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.hugegraph.store.core.store.meta; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; + +import java.util.List; + +import org.apache.hugegraph.pd.grpc.Metapb; +import org.apache.hugegraph.store.meta.Partition; +import org.apache.hugegraph.store.meta.Shard; +import org.apache.hugegraph.store.meta.ShardGroup; +import org.junit.Test; + +/** + * Partition and shard-group metadata is persisted and exchanged with PD as protobuf, so the + * conversion in both directions must be lossless for the fields the store keeps. + */ +public class PartitionMetaCodecTest { + + private static Metapb.Partition proto(Metapb.PartitionState state) { + return Metapb.Partition.newBuilder() + .setId(7) + .setGraphName("hugegraph/g") + .setStartKey(0x1000) + .setEndKey(0x2000) + .setVersion(42) + .setState(state) + .build(); + } + + private static Metapb.Shard shard(long storeId, Metapb.ShardRole role) { + return Metapb.Shard.newBuilder().setStoreId(storeId).setRole(role).build(); + } + + @Test + public void testPartitionProtoRoundTrip() { + for (Metapb.PartitionState state : new Metapb.PartitionState[]{ + Metapb.PartitionState.PState_Normal, Metapb.PartitionState.PState_Warn, + Metapb.PartitionState.PState_Offline}) { + Metapb.Partition original = proto(state); + Partition partition = new Partition(original); + + assertEquals(7, partition.getId()); + assertEquals("hugegraph/g", partition.getGraphName()); + assertEquals(0x1000, partition.getStartKey()); + assertEquals(0x2000, partition.getEndKey()); + assertEquals(42, partition.getVersion()); + assertEquals(state, partition.getWorkState()); + assertEquals(original, partition.getProtoObj()); + } + } + + @Test + public void testMissingOrUnknownPartitionStateFallsBackToNormal() { + Partition none = new Partition(proto(Metapb.PartitionState.PState_None)); + assertEquals(Metapb.PartitionState.PState_Normal, none.getWorkState()); + + Metapb.Partition future = proto(Metapb.PartitionState.PState_Normal).toBuilder() + .setStateValue(99) + .build(); + assertEquals(Metapb.PartitionState.UNRECOGNIZED, future.getState()); + assertEquals(Metapb.PartitionState.PState_Normal, new Partition(future).getWorkState()); + + Partition fromEmpty = new Partition(Metapb.Partition.getDefaultInstance()); + assertEquals(Metapb.PartitionState.PState_Normal, fromEmpty.getWorkState()); + assertEquals("", fromEmpty.getGraphName()); + assertEquals(Metapb.PartitionState.PState_Normal, new Partition().getWorkState()); + } + + @Test + public void testPartitionCloneIsIndependent() { + Partition partition = new Partition(proto(Metapb.PartitionState.PState_Normal)); + Partition copy = partition.clone(); + + assertNotSame(partition, copy); + assertEquals(partition, copy); + copy.setEndKey(0x3000); + copy.setWorkState(Metapb.PartitionState.PState_Offline); + assertEquals(0x2000, partition.getEndKey()); + assertEquals(Metapb.PartitionState.PState_Normal, partition.getWorkState()); + } + + @Test + public void testShardGroupProtoRoundTrip() { + Metapb.ShardGroup original = Metapb.ShardGroup.newBuilder() + .setId(3) + .setVersion(5) + .setConfVer(9) + .addShards(shard(11, Metapb.ShardRole.Leader)) + .addShards(shard(12, Metapb.ShardRole.Follower)) + .addShards(shard(13, Metapb.ShardRole.Learner)) + .build(); + ShardGroup group = ShardGroup.from(original); + + assertEquals(3, group.getId()); + assertEquals(5, group.getVersion()); + assertEquals(9, group.getConfVersion()); + assertEquals(3, group.getShards().size()); + assertEquals(original, group.getProtoObj()); + assertEquals("{ id:11,role:Leader },{ id:12,role:Follower },{ id:13,role:Learner }", + group.toString()); + + assertNull(ShardGroup.from(null)); + assertEquals("", new ShardGroup().toString()); + } + + @Test + public void testChangeShardListAssignsRolesAndBumpsConfVersion() { + ShardGroup group = ShardGroup.from(Metapb.ShardGroup.newBuilder() + .setId(1) + .setConfVer(4) + .addShards(shard(1, Metapb.ShardRole.Leader)) + .build()); + + group.changeShardList(List.of(21L, 22L), List.of(23L), 22L); + + assertEquals(5, group.getConfVersion()); + assertEquals(List.of(shard(21, Metapb.ShardRole.Follower), + shard(22, Metapb.ShardRole.Leader), + shard(23, Metapb.ShardRole.Learner)), + group.getMetaPbShard()); + } + + @Test + public void testEmptyPeerListLeavesTheGroupUnchanged() { + ShardGroup group = new ShardGroup(); + group.setConfVersion(4); + Shard leader = new Shard(); + leader.setStoreId(1); + leader.setRole(Metapb.ShardRole.Leader); + group.addShard(leader); + + // Learners alone must not wipe the voters + group.changeShardList(List.of(), List.of(9L), 9L); + + assertEquals(4, group.getConfVersion()); + assertEquals(List.of(shard(1, Metapb.ShardRole.Leader)), group.getMetaPbShard()); + } + + @Test + public void testChangeLeaderDemotesEveryOtherShard() { + ShardGroup group = ShardGroup.from(Metapb.ShardGroup.newBuilder() + .addShards(shard(1, Metapb.ShardRole.Leader)) + .addShards(shard(2, Metapb.ShardRole.Follower)) + .addShards(shard(3, Metapb.ShardRole.Follower)) + .build()); + group.changeLeader(3); + assertEquals(List.of(shard(1, Metapb.ShardRole.Follower), + shard(2, Metapb.ShardRole.Follower), + shard(3, Metapb.ShardRole.Leader)), + group.getMetaPbShard()); + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/store/util/IpUtilTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/store/util/IpUtilTest.java new file mode 100644 index 0000000000..036ad6d244 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/core/store/util/IpUtilTest.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.hugegraph.store.core.store.util; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import org.apache.hugegraph.store.util.IpUtil; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +public class IpUtilTest { + + @Test + public void testConfiguredLoopbackStays() throws SocketException { + Assume.assumeTrue(hasLocalIpv4("127.0.0.1", true)); + Assert.assertEquals("127.0.0.1:8510", + IpUtil.getNearestAddress("127.0.0.1:8510")); + } + + @Test + public void testNearestAddressDoesNotPreferLoopback() throws SocketException { + Assume.assumeTrue(hasLocalIpv4(null, false)); + String selected = IpUtil.getNearestAddress("127.0.0.2:8510"); + Assert.assertNotEquals("127.0.0.1:8510", selected); + Assert.assertFalse(selected.startsWith("127.")); + } + + @Test + public void testConfiguredLinkLocalStays() throws SocketException { + String linkLocal = findLocalLinkLocalIpv4(); + Assume.assumeTrue(linkLocal != null); + Assert.assertEquals(linkLocal + ":8510", + IpUtil.getNearestAddress(linkLocal + ":8510")); + } + + @Test + public void testHostnameIsReturnedWithoutError() { + Logger logger = (Logger) LogManager.getLogger(IpUtil.class); + MemoryAppender appender = new MemoryAppender(); + appender.start(); + Level previous = logger.getLevel(); + logger.addAppender(appender); + logger.setLevel(Level.ERROR); + try { + Assert.assertEquals("store0:8500", + IpUtil.getNearestAddress("store0:8500")); + Assert.assertEquals("hugegraph-store-0.svc.cluster.local:8510", + IpUtil.getNearestAddress( + "hugegraph-store-0.svc.cluster.local:8510")); + Assert.assertTrue(appender.errors.isEmpty()); + } finally { + logger.removeAppender(appender); + logger.setLevel(previous); + appender.stop(); + } + } + + @Test + public void testNonAsciiDigitsAreReturnedAsHostname() { + String unicodeDigits = "\u0661.\u0662.\u0663.\u0664:8510"; + Assert.assertEquals(unicodeDigits, + IpUtil.getNearestAddress(unicodeDigits)); + } + + private static String findLocalLinkLocalIpv4() throws SocketException { + Enumeration nics = NetworkInterface.getNetworkInterfaces(); + if (nics == null) { + return null; + } + while (nics.hasMoreElements()) { + NetworkInterface nic = nics.nextElement(); + Enumeration addresses = nic.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if (address instanceof Inet4Address && address.isLinkLocalAddress()) { + return address.getHostAddress(); + } + } + } + return null; + } + + private static boolean hasLocalIpv4(String expected, boolean includeLoopback) + throws SocketException { + Enumeration nics = NetworkInterface.getNetworkInterfaces(); + if (nics == null) { + return false; + } + while (nics.hasMoreElements()) { + NetworkInterface nic = nics.nextElement(); + Enumeration addresses = nic.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if (!(address instanceof Inet4Address) || address.isLinkLocalAddress()) { + continue; + } + if (!includeLoopback && address.isLoopbackAddress()) { + continue; + } + if (expected == null || expected.equals(address.getHostAddress())) { + return true; + } + } + } + return false; + } + + private static final class MemoryAppender extends AbstractAppender { + + private final List errors = new ArrayList<>(); + + private MemoryAppender() { + super("iputil-test", null, null, true, Property.EMPTY_ARRAY); + } + + @Override + public void append(LogEvent event) { + if (event.getLevel().isMoreSpecificThan(Level.WARN)) { + errors.add(event.getMessage().getFormattedMessage()); + } + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/meta/GraphIDManagerTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/meta/GraphIDManagerTest.java index 60bb542a9b..ac0d5d0d1e 100644 --- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/meta/GraphIDManagerTest.java +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/meta/GraphIDManagerTest.java @@ -17,69 +17,426 @@ package org.apache.hugegraph.store.meta; +import static org.apache.hugegraph.store.constant.HugeServerTables.VERTEX_TABLE; +import static org.apache.hugegraph.store.constant.HugeServerTables.OUT_EDGE_TABLE; +import static org.apache.hugegraph.store.constant.HugeServerTables.IN_EDGE_TABLE; +import static org.apache.hugegraph.store.constant.HugeServerTables.INDEX_TABLE; + import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; -import org.apache.hugegraph.pd.common.PDException; import org.apache.hugegraph.rocksdb.access.RocksDBSession; +import org.apache.hugegraph.rocksdb.access.SessionOperator; import org.apache.hugegraph.store.UnitTestBase; +import org.apache.hugegraph.store.business.BusinessHandlerImpl; import org.apache.hugegraph.store.meta.base.DBSessionBuilder; +import org.apache.hugegraph.store.term.Bits; +import org.apache.hugegraph.store.util.HgStoreException; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import com.google.protobuf.Int64Value; + public class GraphIDManagerTest extends UnitTestBase { + + private static final int PARTITION_ID = 0; + private static final int GRAPH_ID_LIMIT = 8; + + private String dbName; + private File dbPath; + private RocksDBSession session; + private DBSessionBuilder sessionBuilder; + private int previousMaxGraphId; + @Before public void init() { - String dbPath = "/tmp/junit"; - UnitTestBase.deleteDir(new File(dbPath)); - super.initDB(dbPath); - } - - @Test - public void test() throws PDException { - GraphIdManager.maxGraphID = 64; - int max = GraphIdManager.maxGraphID; - try (RocksDBSession session = getDBSession("test")) { - GraphIdManager gid = new GraphIdManager(new DBSessionBuilder() { - @Override - public RocksDBSession getSession(int partId) { - return session.clone(); - } - }, 0); - for (int i = 0; i < max; i++) { - Assert.assertEquals(i, gid.getCId("Test", max)); - } + this.previousMaxGraphId = GraphIdManager.maxGraphID; + GraphIdManager.maxGraphID = GRAPH_ID_LIMIT; + this.dbName = "graph-id-manager-" + System.nanoTime(); + this.dbPath = new File("target/graph-id-manager-test", this.dbName); + Map config = new HashMap<>(); + config.put("rocksdb.write_buffer_size", "1048576"); + config.put("rocksdb.bloom_filter_bits_per_key", "10"); + BusinessHandlerImpl.initRocksdb(config, null); + this.session = factory.createGraphDB(this.dbPath.getAbsolutePath(), + this.dbName); + this.sessionBuilder = partId -> this.session.clone(); + } + + @After + public void clear() { + GraphIdManager.maxGraphID = this.previousMaxGraphId; + if (this.session != null) { + this.session.close(); + } + if (this.dbName != null) { + factory.releaseGraphDB(this.dbName); + } + if (this.dbPath != null) { + UnitTestBase.deleteDir(this.dbPath); + } + } - Assert.assertEquals(-1, gid.getCId("Test", max)); + @Test + public void testAllocateBoundaryIdBeforeWrap() { + GraphIdManager manager = this.newManager(); - gid.delCId("Test", 3); - Assert.assertEquals(3, gid.getCId("Test", max)); - Assert.assertEquals(-1, gid.getCId("Test", max)); + Assert.assertEquals(0L, manager.getCId("boundary", 4)); + Assert.assertEquals(1L, manager.getCId("boundary", 4)); + Assert.assertEquals(2L, manager.getCId("boundary", 4)); + Assert.assertEquals(3L, manager.getCId("boundary", 4)); + } - long start = System.currentTimeMillis(); - for (int i = 0; i < GraphIdManager.maxGraphID; i++) { - long id = gid.getGraphId("g" + i); - Assert.assertEquals(i, id); - } - System.out.println("time is " + (System.currentTimeMillis() - start)); - { - gid.releaseGraphId("g" + 10); - long id = gid.getGraphId("g" + 10); - Assert.assertEquals(10, id); - } - start = System.currentTimeMillis(); - for (int i = 0; i < GraphIdManager.maxGraphID; i++) { - long id = gid.releaseGraphId("g" + i); - Assert.assertEquals(i, id); + @Test + public void testReturnMinusOneWhenAllIdsAreUsed() { + GraphIdManager manager = this.newManager(); + + for (int i = 0; i < 4; i++) { + Assert.assertEquals(i, manager.getCId("full", 4)); + } + Assert.assertEquals(-1L, manager.getCId("full", 4)); + } + + @Test + public void testReuseReleasedId() { + GraphIdManager manager = this.newManager(); + + for (int i = 0; i < 4; i++) { + Assert.assertEquals(i, manager.getCId("release", 4)); + } + manager.delCId("release", 1L); + + Assert.assertEquals(1L, manager.getCId("release", 4)); + Assert.assertEquals(-1L, manager.getCId("release", 4)); + } + + @Test + public void testWrapToBeginningAfterBoundary() { + GraphIdManager manager = this.newManager(); + + Assert.assertEquals(0L, manager.getCId("wrap", 4)); + Assert.assertEquals(1L, manager.getCId("wrap", 4)); + manager.delCId("wrap", 0L); + Assert.assertEquals(2L, manager.getCId("wrap", 4)); + Assert.assertEquals(3L, manager.getCId("wrap", 4)); + + Assert.assertEquals(0L, manager.getCId("wrap", 4)); + } + + @Test + public void testPersistGraphIdAcrossManagerRestart() { + GraphIdManager firstManager = this.newManager(); + + Assert.assertEquals(0L, firstManager.getGraphIdOrCreate("first")); + + this.reopenDatabase(); + GraphIdManager restartedManager = this.newManager(); + Assert.assertEquals(0L, restartedManager.getGraphId("first")); + Assert.assertEquals(1L, restartedManager.getGraphIdOrCreate("second")); + } + + @Test + public void testSkipGraphIdMappingWithoutSlot() { + this.persistGraphId("existing", 0L); + + Assert.assertEquals(1L, + this.newManager().getGraphIdOrCreate("new")); + } + + @Test + public void testUpdateGraphIdsRejectsPersistedCollisionBeforeWriting() { + this.persistGraphId("existing", 2L); + Map graphIds = new LinkedHashMap<>(); + graphIds.put("graph-a", 1L); + graphIds.put("graph-b", 2L); + + HgStoreException exception = Assert.assertThrows( + HgStoreException.class, + () -> this.newManager().updateGraphIds(graphIds)); + Assert.assertTrue(exception.getMessage().contains("existing")); + Assert.assertTrue(exception.getMessage().contains("graph-b")); + Assert.assertTrue(exception.getMessage().contains("2")); + Assert.assertEquals(GRAPH_ID_LIMIT, + this.newManager().getGraphId("graph-a")); + Assert.assertEquals(GRAPH_ID_LIMIT, + this.newManager().getGraphId("graph-b")); + } + + @Test + public void testUpdateGraphIdsPersistsMappingAndSlot() { + GraphIdManager manager = this.newManager(); + + manager.updateGraphIds(Collections.singletonMap("graph-a", 3L)); + + Assert.assertEquals(3L, this.newManager().getGraphId("graph-a")); + Int64Value slot = manager.get( + Int64Value.parser(), + manager.genCIDSlotKey(GraphIdManager.GRAPH_ID_PREFIX, 3L)); + Assert.assertNotNull(slot); + Assert.assertEquals(3L, slot.getValue()); + } + + @Test + public void testUpdateGraphIdsAllowsIdempotentAssignment() { + GraphIdManager manager = this.newManager(); + Map graphIds = + Collections.singletonMap("graph-a", 3L); + + manager.updateGraphIds(graphIds); + manager.updateGraphIds(graphIds); + + Assert.assertEquals(3L, this.newManager().getGraphId("graph-a")); + } + + @Test + public void testKeepPreviousGraphIdReservedAfterRemap() { + GraphIdManager manager = this.newManager(); + + manager.updateGraphIds(Collections.singletonMap("graph-a", 3L)); + manager.updateGraphIds(Collections.singletonMap("graph-a", 4L)); + manager.put(MetadataKeyHelper.getCidKey(GraphIdManager.GRAPH_ID_PREFIX), + Int64Value.of(3L)); + manager.flush(); + + Assert.assertNotEquals(3L, + this.newManager().getGraphIdOrCreate("graph-b")); + } + + @Test + public void testReservePreviousGraphIdWhenLegacySlotIsMissing() { + this.persistGraphId("graph-a", 3L); + GraphIdManager manager = this.newManager(); + manager.updateGraphIds(Collections.singletonMap("graph-a", 4L)); + manager.put(MetadataKeyHelper.getCidKey(GraphIdManager.GRAPH_ID_PREFIX), + Int64Value.of(3L)); + manager.flush(); + + this.reopenDatabase(); + Assert.assertNotEquals(3L, + this.newManager().getGraphIdOrCreate("graph-b")); + } + + @Test + public void testKeepOutEdgeResidueWhenVertexTableMissing() { + this.assertRepairKeepsOldIdReserved(OUT_EDGE_TABLE, false, false); + } + + @Test + public void testKeepInEdgeResidueWhenVertexTableMissing() { + this.assertRepairKeepsOldIdReserved(IN_EDGE_TABLE, false, false); + } + + @Test + public void testKeepIndexResidueWhenVertexTableMissing() { + this.assertRepairKeepsOldIdReserved(INDEX_TABLE, false, false); + } + + @Test + public void testKeepOutEdgeResidueWhenVertexTableEmpty() { + this.assertRepairKeepsOldIdReserved(OUT_EDGE_TABLE, true, false); + } + + @Test + public void testKeepInEdgeResidueWhenVertexTableEmpty() { + this.assertRepairKeepsOldIdReserved(IN_EDGE_TABLE, true, false); + } + + @Test + public void testKeepIndexResidueWhenVertexTableEmptyAfterReopen() { + this.assertRepairKeepsOldIdReserved(INDEX_TABLE, true, true); + } + + @Test + public void testKeepPreviousGraphIdSlotWhenStillAssigned() { + GraphIdManager manager = this.newManager(); + manager.updateGraphIds(Collections.singletonMap("graph-a", 3L)); + this.persistGraphId("graph-b", 3L); + + manager.updateGraphIds(Collections.singletonMap("graph-a", 4L)); + + Int64Value slot = manager.get( + Int64Value.parser(), + manager.genCIDSlotKey(GraphIdManager.GRAPH_ID_PREFIX, 3L)); + Assert.assertNotNull(slot); + Assert.assertEquals(3L, slot.getValue()); + Assert.assertEquals(3L, this.newManager().getGraphId("graph-b")); + } + + @Test + public void testRejectPersistedGraphIdsOutsideAllocatableDomain() { + long[] invalidGraphIds = {-1L, GRAPH_ID_LIMIT, 65536L}; + + for (long graphId : invalidGraphIds) { + String readGraph = "invalid-read-" + graphId; + this.persistGraphId(readGraph, graphId); + this.assertInvalidPersistedGraphId( + readGraph, graphId, + () -> this.newManager().getGraphId(readGraph)); + + String writeGraph = "invalid-write-" + graphId; + this.persistGraphId(writeGraph, graphId); + this.assertInvalidPersistedGraphId( + writeGraph, graphId, + () -> this.newManager().getGraphIdOrCreate(writeGraph)); + } + } + + @Test + public void testSkipVertexDataWithoutGraphIdSlot() { + GraphIdManager manager = this.newManager(); + this.persistVertexWithGraphId(0); + + Assert.assertEquals(1L, manager.getCId("stale-forward", 4)); + } + + @Test + public void testSkipVertexDataWithoutGraphIdSlotAfterWrap() { + String key = "stale-wrap"; + GraphIdManager manager = this.newManager(); + this.persistVertexWithGraphId(0); + manager.put(MetadataKeyHelper.getCidKey(key), Int64Value.of(2L)); + manager.put(manager.genCIDSlotKey(key, 2L), Int64Value.of(2L)); + manager.put(manager.genCIDSlotKey(key, 3L), Int64Value.of(3L)); + + Assert.assertEquals(1L, manager.getCId(key, 4)); + } + + @Test + public void testGraphIdDomainExcludesMissingSentinel() { + GraphIdManager manager = this.newManager(); + + for (int i = 0; i < GRAPH_ID_LIMIT; i++) { + Assert.assertEquals(i, manager.getGraphIdOrCreate("graph-" + i)); + } + Assert.assertEquals(GRAPH_ID_LIMIT, manager.getGraphId("missing")); + Assert.assertThrows(HgStoreException.class, + () -> manager.getGraphIdOrCreate("overflow")); + } + + @Test + public void testConcurrentCreateSameGraphReturnsSameId() throws Exception { + int threadCount = 8; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch start = new CountDownLatch(1); + List> futures = new ArrayList<>(threadCount); + + try { + for (int i = 0; i < threadCount; i++) { + futures.add(executor.submit(() -> { + GraphIdManager manager = this.newManager(); + start.await(); + return manager.getGraphIdOrCreate("same-graph"); + })); } - System.out.println("time is " + (System.currentTimeMillis() - start)); - start = System.currentTimeMillis(); - for (int i = 0; i < GraphIdManager.maxGraphID; i++) { - long id = gid.getCId(GraphIdManager.GRAPH_ID_PREFIX, GraphIdManager.maxGraphID); - // long id = gid.getGraphId("g" + i); - Assert.assertTrue(id >= 0); + start.countDown(); + + Set graphIds = new HashSet<>(); + for (Future future : futures) { + graphIds.add(future.get(10L, TimeUnit.SECONDS)); } - System.out.println("time is " + (System.currentTimeMillis() - start)); + Assert.assertEquals(1, graphIds.size()); + Assert.assertTrue(graphIds.contains(0L)); + } finally { + executor.shutdownNow(); + } + } + + private GraphIdManager newManager() { + return new GraphIdManager(this.sessionBuilder, PARTITION_ID); + } + + private void assertRepairKeepsOldIdReserved(String residualTable, + boolean vertexTableExists, + boolean reopen) { + GraphIdManager manager = this.newManager(); + manager.updateGraphIds(Collections.singletonMap("graph-a", 3L)); + if (vertexTableExists) { + this.session.createTables(VERTEX_TABLE); + } else { + Assert.assertFalse(this.session.tableIsExist(VERTEX_TABLE)); + } + this.session.createTables(residualTable); + byte[] key = new byte[5]; + Bits.putShort(key, 0, 3); + key[2] = 1; + Bits.putShort(key, 3, 0); + SessionOperator operator = this.session.sessionOp(); + try { + operator.prepare(); + operator.put(residualTable, key, new byte[]{42}); + operator.commit(); + } catch (RuntimeException e) { + operator.rollback(); + throw e; + } + + manager.updateGraphIds(Collections.singletonMap("graph-a", 4L)); + manager.put(MetadataKeyHelper.getCidKey(GraphIdManager.GRAPH_ID_PREFIX), + Int64Value.of(3L)); + manager.flush(); + if (reopen) { + this.reopenDatabase(); + } + + Assert.assertArrayEquals(new byte[]{42}, + this.session.sessionOp().get(residualTable, key)); + Assert.assertNotEquals("Residual data must keep the old graph ID reserved", + 3L, this.newManager().getGraphIdOrCreate("graph-b")); + } + + private void persistGraphId(String graphName, long graphId) { + GraphIdManager manager = this.newManager(); + manager.put(MetadataKeyHelper.getGraphIDKey(graphName), + Int64Value.of(graphId)); + manager.flush(); + } + + private void persistVertexWithGraphId(int graphId) { + this.session.createTables(VERTEX_TABLE); + byte[] key = new byte[3]; + Bits.putShort(key, 0, graphId); + key[2] = 1; + + SessionOperator operator = this.session.sessionOp(); + try { + operator.prepare(); + operator.put(VERTEX_TABLE, key, new byte[]{1}); + operator.commit(); + } catch (RuntimeException e) { + operator.rollback(); + throw e; } } + + private void assertInvalidPersistedGraphId(String graphName, long graphId, + Runnable action) { + HgStoreException exception = + Assert.assertThrows(HgStoreException.class, action::run); + Assert.assertTrue(exception.getMessage().contains("Invalid graph ID")); + Assert.assertTrue(exception.getMessage().contains(String.valueOf(graphId))); + Assert.assertTrue(exception.getMessage().contains(graphName)); + } + + private void reopenDatabase() { + this.session.close(); + this.session = null; + factory.releaseGraphDB(this.dbName); + this.session = factory.createGraphDB(this.dbPath.getAbsolutePath(), + this.dbName); + this.sessionBuilder = partId -> this.session.clone(); + } } diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/BatchScanHalfCloseTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/BatchScanHalfCloseTest.java new file mode 100644 index 0000000000..6d60ff32e2 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/BatchScanHalfCloseTest.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.hugegraph.store.node.grpc; + +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.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hugegraph.rocksdb.access.RocksDBSession; +import org.apache.hugegraph.rocksdb.access.ScanIterator; +import org.apache.hugegraph.store.HgScanQuery; +import org.apache.hugegraph.store.buffer.KVByteBuffer; +import org.apache.hugegraph.store.client.grpc.KvBatchScanner; +import org.apache.hugegraph.store.client.grpc.KvBatchScannerMerger; +import org.apache.hugegraph.store.grpc.common.Header; +import org.apache.hugegraph.store.grpc.common.ScanMethod; +import org.apache.hugegraph.store.grpc.stream.HgStoreStreamGrpc; +import org.apache.hugegraph.store.grpc.stream.KvPageRes; +import org.apache.hugegraph.store.grpc.stream.KvStream; +import org.apache.hugegraph.store.grpc.stream.ScanCancelRequest; +import org.apache.hugegraph.store.grpc.stream.ScanQueryRequest; +import org.apache.hugegraph.store.grpc.stream.ScanStreamBatchReq; +import org.junit.Test; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; + +public class BatchScanHalfCloseTest { + + @Test(timeout = 5000) + public void testQueuedQueryHalfCloseDeliversFinalPartialBatch() throws Exception { + for (boolean encoded : new boolean[]{false, true}) { + DeferredExecutor executor = new DeferredExecutor(); + ScanIterator source = rows(3); + HgStoreWrapperEx wrapper = wrapper(source); + Output output = new Output(); + StreamObserver input = input(encoded, output, wrapper, executor); + try { + input.onNext(request(2)); + input.onCompleted(); + assertEquals("half-close must preserve the queued query", 0, output.completions); + executor.runAll(); + assertEquals(3, output.rows); + assertTrue(output.over); + assertEquals(1, output.completions); + assertEquals(null, output.error); + verify(source).close(); + input.onCompleted(); + assertEquals(1, output.completions); + } finally { + input.onError(Status.CANCELLED.asRuntimeException()); + executor.runAll(); + executor.shutdownNow(); + } + } + } + + @Test(timeout = 5000) + public void testHalfCloseWithExhaustedCreditFailsInsteadOfTruncating() throws Exception { + for (boolean encoded : new boolean[]{false, true}) { + DeferredExecutor executor = new DeferredExecutor(); + Output output = new Output(); + ScanIterator iterator; + StreamObserver input; + if (encoded) { + ScanBatchResponse response = new ScanBatchResponse(output.streams(), mock(HgStoreWrapperEx.class), + executor); + iterator = batches(20); + set(response, "iterator", iterator); + set(response, "limit", Long.MAX_VALUE); + set(response, "queryReceived", true); + input = response; + } else { + iterator = rows(21); + input = input(false, output, wrapper(iterator), executor); + input.onNext(request(1)); + } + try { + input.onCompleted(); + executor.runAll(); + assertTrue("granted batches must precede the credit failure", output.rows > 0); + assertTrue("the full result exceeds granted credit", output.rows < 20); + assertFalse(output.over); + assertEquals(0, output.completions); + assertEquals(Status.Code.FAILED_PRECONDITION, Status.fromThrowable(output.error).getCode()); + verify(iterator).close(); + input.onCompleted(); + assertEquals(1, output.errors); + } finally { + input.onError(Status.CANCELLED.asRuntimeException()); + executor.runAll(); + executor.shutdownNow(); + } + } + } + + @Test(timeout = 5000) + public void testExplicitAndTransportCancellationWinBeforeFinalDelivery() throws Exception { + for (boolean encoded : new boolean[]{false, true}) { + for (boolean transport : new boolean[]{false, true}) { + DeferredExecutor executor = new DeferredExecutor(); + Output output = new Output(); + AtomicReference> request = new AtomicReference<>(); + AtomicInteger checks = new AtomicInteger(); + ScanIterator source = rows(2); + when(source.hasNext()).thenAnswer(ignored -> { + if (checks.getAndIncrement() == 0) { + if (transport) { + request.get().onError(Status.CANCELLED.asRuntimeException()); + } else { + request.get().onNext(ScanStreamBatchReq.newBuilder().setCancelRequest( + ScanCancelRequest.getDefaultInstance()).build()); + } + } + return false; + }); + StreamObserver input; + if (encoded) { + ScanBatchResponse response = new ScanBatchResponse(output.streams(), mock(HgStoreWrapperEx.class), + executor); + set(response, "iterator", source); + set(response, "limit", Long.MAX_VALUE); + set(response, "queryReceived", true); + input = response; + } else { + input = input(false, output, wrapper(source), executor); + input.onNext(request(1)); + } + request.set(input); + try { + input.onCompleted(); + executor.runAll(); + assertEquals(0, output.rows); + assertFalse(output.over); + assertEquals(0, output.completions); + assertEquals(1, output.errors); + assertEquals(Status.Code.CANCELLED, Status.fromThrowable(output.error).getCode()); + input.onCompleted(); + input.onError(Status.CANCELLED.asRuntimeException()); + verify(source).close(); + assertEquals(1, output.errors); + } finally { + executor.shutdownNow(); + } + } + } + } + + @Test(timeout = 5000) + public void testClientEarlyCloseSendsCancelButNormalFinishOnlyHalfCloses() throws Exception { + List commands = new ArrayList<>(); + AtomicInteger completions = new AtomicInteger(); + String name = InProcessServerBuilder.generateName(); + Server server = InProcessServerBuilder.forName(name).directExecutor().addService( + new HgStoreStreamGrpc.HgStoreStreamImplBase() { + @Override + public StreamObserver scanBatch2(StreamObserver output) { + return new StreamObserver() { + @Override + public void onNext(ScanStreamBatchReq value) { + commands.add(value.getQueryCase()); + } + + @Override + public void onError(Throwable error) { + throw new AssertionError(error); + } + + @Override + public void onCompleted() { + completions.incrementAndGet(); + output.onCompleted(); + } + }; + } + }).build().start(); + ManagedChannel channel = InProcessChannelBuilder.forName(name).directExecutor().build(); + try { + KvBatchScanner early = new KvBatchScanner(HgStoreStreamGrpc.newStub(channel), "g", HgScanQuery.tableOf("t"), + mock(KvBatchScannerMerger.class)); + early.close(); + early.close(); + assertEquals(java.util.Arrays.asList(ScanStreamBatchReq.QueryCase.QUERY_REQUEST, + ScanStreamBatchReq.QueryCase.CANCEL_REQUEST), commands); + assertEquals(1, completions.get()); + commands.clear(); + KvBatchScanner normal = new KvBatchScanner(HgStoreStreamGrpc.newStub(channel), "g", HgScanQuery.tableOf("t"), + mock(KvBatchScannerMerger.class)); + normal.dataComplete(); + assertEquals(Collections.singletonList(ScanStreamBatchReq.QueryCase.QUERY_REQUEST), commands); + assertEquals(2, completions.get()); + } finally { + channel.shutdownNow(); + server.shutdownNow(); + } + } + + private static StreamObserver input(boolean encoded, Output output, + HgStoreWrapperEx wrapper, DeferredExecutor executor) { + return encoded ? new ScanBatchResponse(output.streams(), wrapper, executor) : + ScanBatchResponse3.of(output.pages(), wrapper, executor); + } + + private static ScanStreamBatchReq request(int pageSize) { + return ScanStreamBatchReq.newBuilder().setHeader(Header.newBuilder().setGraph("g")) + .setQueryRequest(ScanQueryRequest.newBuilder().setMethod(ScanMethod.ALL).setTable("t") + .setLimit(Long.MAX_VALUE).setPerKeyMax(Long.MAX_VALUE).setPageSize(pageSize)).build(); + } + + private static HgStoreWrapperEx wrapper(ScanIterator source) { + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(source); + return wrapper; + } + + private static ScanIterator rows(int size) { + ScanIterator source = mock(ScanIterator.class); + AtomicInteger read = new AtomicInteger(); + when(source.hasNext()).thenAnswer(ignored -> read.get() < size); + when(source.next()).thenAnswer(ignored -> { + read.incrementAndGet(); + return RocksDBSession.BackendColumn.of(new byte[4], new byte[4]); + }); + when(source.position()).thenReturn(new byte[4]); + return source; + } + + private static ScanIterator batches(int size) { + ScanIterator source = mock(ScanIterator.class); + AtomicInteger read = new AtomicInteger(); + when(source.hasNext()).thenAnswer(ignored -> read.get() < size); + when(source.next()).thenAnswer(ignored -> { + read.incrementAndGet(); + return Collections.singletonList(ParallelScanIterator.KV.of( + RocksDBSession.BackendColumn.of(new byte[4], new byte[4]))); + }); + return source; + } + + private static void set(Object target, String name, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } + + private static final class DeferredExecutor extends ThreadPoolExecutor { + + private final Queue tasks = new ArrayDeque<>(); + + DeferredExecutor() { + super(1, 1, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); + } + + @Override + public void execute(Runnable task) { + this.tasks.add(task); + } + + void runAll() { + Runnable task; + while ((task = this.tasks.poll()) != null) { + task.run(); + } + } + } + + private static final class Output { + + int rows; + int completions; + int errors; + boolean over; + Throwable error; + + StreamObserver pages() { + return new StreamObserver() { + @Override + public void onNext(KvPageRes value) { + rows += value.getDataCount(); + over |= value.getOver(); + } + + @Override + public void onError(Throwable failure) { + error = failure; + errors++; + } + + @Override + public void onCompleted() { + completions++; + } + }; + } + + StreamObserver streams() { + return new StreamObserver() { + @Override + public void onNext(KvStream value) { + over |= value.getOver(); + if (value.getStream() != null) { + KVByteBuffer buffer = new KVByteBuffer(value.getStream().duplicate()); + while (buffer.hasRemaining()) { + buffer.getBytes(); + buffer.getBytes(); + rows++; + } + } + } + + @Override + public void onError(Throwable failure) { + error = failure; + errors++; + } + + @Override + public void onCompleted() { + completions++; + } + }; + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryLifecycleTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryLifecycleTest.java new file mode 100644 index 0000000000..0760ef10ce --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryLifecycleTest.java @@ -0,0 +1,940 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.store.node.grpc.query; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +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; + +import io.grpc.Status; +import io.grpc.stub.StreamObserver; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.hugegraph.rocksdb.access.RocksDBSession; +import org.apache.hugegraph.rocksdb.access.ScanIterator; +import org.apache.hugegraph.store.business.MultiPartitionIterator; +import org.apache.hugegraph.store.grpc.query.QueryRequest; +import org.apache.hugegraph.store.grpc.query.QueryResponse; +import org.apache.hugegraph.store.node.grpc.query.model.PipelineResult; +import org.apache.hugegraph.store.node.grpc.query.model.QueryPlan; +import org.junit.Test; + +public class AggregativeQueryLifecycleTest extends AggregativeQueryTestSupport { + + @Test(timeout = 5000) + public void testPartitionTimeoutDoesNotClearPlanOrFinishBeforeChildFinally() throws Exception { + ThreadPoolExecutor pool = pool(2); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch releaseChild = new CountDownLatch(1); + CountDownLatch parentReturned = new CountDownLatch(1); + CountDownLatch childClosed = new CountDownLatch(1); + ScanIterator child = mock(ScanIterator.class); + when(child.hasNext()).thenAnswer(invocation -> { + reading.countDown(); + awaitUninterruptibly(releaseChild); + return false; + }); + doAnswer(invocation -> { + childClosed.countDown(); + return null; + }).when(child).close(); + MultiPartitionIterator iterator = mock(MultiPartitionIterator.class); + when(iterator.getIterators()).thenReturn(Arrays.asList(child)); + QueryPlan plan = mock(QueryPlan.class); + when(plan.hasIteratorResult()).thenReturn(true); + AggregativeQueryService service = new AggregativeQueryService(pool, 50, 10) { + @Override + AggregativeQueryObserver newObserver(StreamObserver sender) { + AggregativeQueryObserver observer = super.newObserver(sender); + return fixture(observer, iterator, plan, parentReturned); + } + }; + ResponseRecorder response = new ResponseRecorder(); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("timeout-query").build()); + assertTrue(reading.await(1, TimeUnit.SECONDS)); + assertTrue("parent timeout must not wait on its own executor's queued children", + parentReturned.await(1, TimeUnit.SECONDS)); + assertTrue("a live client must receive the timeout error", response.received.await(1, TimeUnit.SECONDS)); + assertError(response, "timeout-query"); + assertEquals("response must not complete before child finally", 0, response.completed.get()); + verify(plan, never()).clear(); + verify(iterator, never()).close(); + FutureTask closing = close(service); + awaitShutdownWait(start(closing)); + assertFalse(closing.isDone()); + releaseChild.countDown(); + closing.get(2, TimeUnit.SECONDS); + assertTrue(childClosed.await(1, TimeUnit.SECONDS)); + verify(plan).clear(); + verify(iterator).close(); + assertTrue(pool.awaitTermination(1, TimeUnit.SECONDS)); + assertEquals(0, response.completed.get()); + assertEquals(1, response.errors.get()); + } finally { + releaseChild.countDown(); + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testRejectedPartitionsReleaseAllOpenedIterators() throws Exception { + AtomicInteger submissions = new AtomicInteger(); + ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, + new LinkedBlockingQueue<>()) { + @Override + public void execute(Runnable task) { + if (submissions.incrementAndGet() > 1) { + throw new RejectedExecutionException("partition queue is full"); + } + super.execute(task); + } + }; + ScanIterator first = mock(ScanIterator.class); + ScanIterator second = mock(ScanIterator.class); + MultiPartitionIterator iterator = mock(MultiPartitionIterator.class); + when(iterator.getIterators()).thenReturn(Arrays.asList(first, second)); + QueryPlan plan = mock(QueryPlan.class); + when(plan.hasIteratorResult()).thenReturn(true); + CountDownLatch parentReturned = new CountDownLatch(1); + AggregativeQueryService service = new AggregativeQueryService(pool, 500, 10) { + @Override + AggregativeQueryObserver newObserver(StreamObserver sender) { + return fixture(super.newObserver(sender), iterator, plan, parentReturned); + } + }; + try { + service.query(sender()).onNext(QueryRequest.getDefaultInstance()); + assertTrue(parentReturned.await(1, TimeUnit.SECONDS)); + FutureTask closing = close(service); + start(closing); + closing.get(2, TimeUnit.SECONDS); + verify(first).close(); + verify(second).close(); + verify(iterator).close(); + verify(plan).clear(); + } finally { + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testHasNextFailureSendsErrorBeforeSingleCompletion() throws Exception { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + when(iterator.hasNext()).thenThrow(new IllegalStateException("iterator read failed")); + QueryPlan plan = mock(QueryPlan.class); + when(plan.isEmpty()).thenReturn(true); + ResponseRecorder response = new ResponseRecorder(); + AggregativeQueryService service = service(pool, iterator, plan, 500); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("read-query").build()); + assertTrue(response.finished.await(1, TimeUnit.SECONDS)); + assertError(response, "read-query"); + assertEquals(1, response.completed.get()); + assertEquals(0, response.errors.get()); + service.shutdownQueries(); + verify(iterator).close(); + verify(plan).clear(); + } finally { + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testPartitionFailureSendsErrorBeforeSingleCompletion() throws Exception { + ThreadPoolExecutor pool = pool(2); + ScanIterator child = mock(ScanIterator.class); + when(child.hasNext()).thenThrow(new IllegalStateException("partition read failed")); + MultiPartitionIterator iterator = mock(MultiPartitionIterator.class); + when(iterator.getIterators()).thenReturn(Arrays.asList(child)); + QueryPlan plan = mock(QueryPlan.class); + when(plan.hasIteratorResult()).thenReturn(true); + ResponseRecorder response = new ResponseRecorder(); + AggregativeQueryService service = service(pool, iterator, plan, 500); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("partition-query").build()); + assertTrue(response.finished.await(1, TimeUnit.SECONDS)); + assertError(response, "partition-query"); + assertEquals(1, response.completed.get()); + service.shutdownQueries(); + verify(child).close(); + verify(iterator).close(); + verify(plan).clear(); + } finally { + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testAcceptedChildAndRejectedRemainingChildrenRetainCleanupOwnership() throws Exception { + CountDownLatch childStarted = new CountDownLatch(1); + CountDownLatch releaseChild = new CountDownLatch(1); + CountDownLatch parentReturned = new CountDownLatch(1); + AtomicInteger submissions = new AtomicInteger(); + ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 2, 0, TimeUnit.SECONDS, + new LinkedBlockingQueue<>()) { + @Override + public void execute(Runnable task) { + int submitted = submissions.incrementAndGet(); + if (submitted >= 3) { + throw new RejectedExecutionException("second partition rejected"); + } + super.execute(task); + if (submitted == 2) { + awaitUninterruptibly(childStarted); + } + } + }; + ScanIterator accepted = mock(ScanIterator.class); + when(accepted.hasNext()).thenAnswer(invocation -> { + childStarted.countDown(); + awaitUninterruptibly(releaseChild); + return false; + }); + ScanIterator rejected = mock(ScanIterator.class); + ScanIterator notSubmitted = mock(ScanIterator.class); + MultiPartitionIterator iterator = mock(MultiPartitionIterator.class); + when(iterator.getIterators()).thenReturn(Arrays.asList(accepted, rejected, notSubmitted)); + QueryPlan plan = mock(QueryPlan.class); + when(plan.hasIteratorResult()).thenReturn(true); + AggregativeQueryService service = new AggregativeQueryService(pool, 500, 10) { + @Override + AggregativeQueryObserver newObserver(StreamObserver sender) { + return fixture(super.newObserver(sender), iterator, plan, parentReturned); + } + }; + ResponseRecorder response = new ResponseRecorder(); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("mixed-query").build()); + assertTrue(childStarted.await(1, TimeUnit.SECONDS)); + assertTrue(parentReturned.await(1, TimeUnit.SECONDS)); + assertError(response, "mixed-query"); + assertEquals(0, response.completed.get()); + verify(rejected).close(); + verify(notSubmitted).close(); + verify(accepted, never()).close(); + verify(iterator, never()).close(); + verify(plan, never()).clear(); + FutureTask closing = close(service); + awaitShutdownWait(start(closing)); + assertFalse("accepted child's finally still owns the query", closing.isDone()); + releaseChild.countDown(); + closing.get(2, TimeUnit.SECONDS); + assertEquals(0, response.completed.get()); + assertEquals(1, response.errors.get()); + verify(accepted).close(); + verify(iterator).close(); + verify(plan).clear(); + } finally { + releaseChild.countDown(); + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testShutdownReportsUnavailableWithoutReleasingActiveIteratorEarly() throws Exception { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + when(iterator.hasNext()).thenAnswer(invocation -> { + reading.countDown(); + awaitUninterruptibly(release); + return false; + }); + QueryPlan plan = new QueryPlan(); + ResponseRecorder response = new ResponseRecorder() { + @Override + public void onError(Throwable error) { + assertEquals(Status.Code.UNAVAILABLE, Status.fromThrowable(error).getCode()); + super.onError(error); + } + }; + AggregativeQueryService service = service(pool, iterator, plan, 5000); + try { + StreamObserver request = service.query(response); + request.onNext(QueryRequest.newBuilder().setQueryId("shutdown-query").build()); + assertTrue(reading.await(1, TimeUnit.SECONDS)); + FutureTask closing = close(service); + start(closing); + assertTrue(response.finished.await(1, TimeUnit.SECONDS)); + assertFalse("termination must retain active iterator ownership", closing.isDone()); + verify(iterator, never()).close(); + release.countDown(); + closing.get(2, TimeUnit.SECONDS); + request.onCompleted(); + request.onError(Status.CANCELLED.asRuntimeException()); + assertTrue(response.responses.isEmpty()); + assertEquals(0, response.completed.get()); + assertEquals(1, response.errors.get()); + verify(iterator).close(); + assertTrue(pool.awaitTermination(1, TimeUnit.SECONDS)); + } finally { + release.countDown(); + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testShutdownReportsUnavailableForIdleQueryOnce() { + ThreadPoolExecutor pool = pool(1); + AggregativeQueryService service = service(pool, mock(ScanIterator.class), new QueryPlan(), 500); + ResponseRecorder response = new ResponseRecorder(); + try { + StreamObserver request = service.query(response); + service.shutdownQueries(); + request.onCompleted(); + service.shutdownQueries(); + assertEquals(0, response.completed.get()); + assertEquals(1, response.errors.get()); + assertTrue(response.responses.isEmpty()); + } finally { + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testNormalIdleHalfCloseCompletesOnceAndExternalErrorNeverCompletesNormally() { + ThreadPoolExecutor pool = pool(1); + AggregativeQueryService service = service(pool, mock(ScanIterator.class), new QueryPlan(), 500); + try { + ResponseRecorder normal = new ResponseRecorder(); + StreamObserver normalRequest = service.query(normal); + normalRequest.onCompleted(); + normalRequest.onCompleted(); + assertEquals(1, normal.completed.get()); + ResponseRecorder failed = new ResponseRecorder(); + StreamObserver failedRequest = service.query(failed); + failedRequest.onError(Status.CANCELLED.asRuntimeException()); + failedRequest.onCompleted(); + assertEquals(0, failed.completed.get()); + service.shutdownQueries(); + assertEquals(1, normal.completed.get()); + assertEquals(0, failed.completed.get()); + } finally { + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testActiveNormalHalfCloseCompletesAfterWorkerCleanupOnce() throws Exception { + ThreadPoolExecutor pool = pool(1); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + ScanIterator iterator = mock(ScanIterator.class); + AtomicInteger reads = new AtomicInteger(); + when(iterator.next()).thenReturn(RocksDBSession.BackendColumn.of(new byte[]{1}, new byte[]{2})); + when(iterator.hasNext()).thenAnswer(invocation -> { + reading.countDown(); + assertTrue(release.await(2, TimeUnit.SECONDS)); + return reads.incrementAndGet() == 1; + }); + ResponseRecorder response = new ResponseRecorder(); + AggregativeQueryService service = service(pool, iterator, new QueryPlan(), 500); + try { + StreamObserver request = service.query(response); + request.onNext(QueryRequest.newBuilder().setQueryId("half-close-query").build()); + assertTrue(reading.await(1, TimeUnit.SECONDS)); + request.onCompleted(); + assertFalse(response.finished.await(50, TimeUnit.MILLISECONDS)); + release.countDown(); + assertTrue("normal half-close must complete after cleanup", response.finished.await(1, TimeUnit.SECONDS)); + request.onCompleted(); + service.shutdownQueries(); + assertEquals(1, response.completed.get()); + assertEquals(0, response.errors.get()); + assertEquals(1, response.responses.size()); + assertTrue(response.responses.get(0).getIsFinished()); + assertTrue(response.responses.get(0).getIsOk()); + assertEquals(1, response.responses.get(0).getDataCount()); + assertEquals(com.google.protobuf.ByteString.copyFrom(new byte[]{2}), + response.responses.get(0).getData(0).getValue()); + verify(iterator).close(); + } finally { + release.countDown(); + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testHalfCloseWithoutEnoughFeedbackReportsError() throws Exception { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + when(iterator.hasNext()).thenReturn(true); + when(iterator.next()).thenReturn(RocksDBSession.BackendColumn.of(new byte[]{1}, new byte[]{2})); + ResponseRecorder response = new ResponseRecorder(); + AggregativeQueryService service = service(pool, iterator, new QueryPlan(), 5000); + try { + StreamObserver request = service.query(response); + request.onNext(QueryRequest.newBuilder().setQueryId("feedback-half-close").build()); + request.onCompleted(); + assertTrue(response.finished.await(2, TimeUnit.SECONDS)); + assertEquals(17, response.responses.size()); + QueryResponse error = response.responses.get(16); + assertFalse(error.getIsOk()); + assertTrue(error.getMessage().contains("without enough feedback")); + assertEquals(1, response.completed.get()); + verify(iterator).close(); + } finally { + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test(timeout = 15000) + public void testTransportCancellationAfterHalfCloseReleasesWorker() throws Exception { + assertTransportCancellationAfterHalfClose(false); + } + + @Test(timeout = 15000) + public void testDeadlineAfterHalfCloseReleasesWorker() throws Exception { + assertTransportCancellationAfterHalfClose(true); + } + + private static void assertTransportCancellationAfterHalfClose(boolean deadline) throws Exception { + ThreadPoolExecutor pool = pool(1); + ScanIterator source = mock(ScanIterator.class); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch interrupted = new CountDownLatch(1); + CountDownLatch released = new CountDownLatch(1); + CountDownLatch fallbackRelease = new CountDownLatch(1); + when(source.hasNext()).thenAnswer(invocation -> { + reading.countDown(); + try { + fallbackRelease.await(); + } catch (InterruptedException expected) { + interrupted.countDown(); + Thread.currentThread().interrupt(); + } + return false; + }); + doAnswer(invocation -> { + released.countDown(); + return null; + }).when(source).close(); + AggregativeQueryService service = new AggregativeQueryService(pool, 5000, 10) { + @Override + AggregativeQueryObserver newObserver(StreamObserver sender) { + return fixture(super.newObserver(sender), source, new QueryPlan(), null); + } + }; + String name = io.grpc.inprocess.InProcessServerBuilder.generateName(); + io.grpc.Server server = io.grpc.inprocess.InProcessServerBuilder.forName(name) + .directExecutor().addService(service).build().start(); + io.grpc.ManagedChannel channel = io.grpc.inprocess.InProcessChannelBuilder.forName(name) + .directExecutor().build(); + io.grpc.CallOptions options = deadline ? io.grpc.CallOptions.DEFAULT.withDeadlineAfter(2, TimeUnit.SECONDS) : + io.grpc.CallOptions.DEFAULT; + io.grpc.ClientCall call = channel.newCall( + org.apache.hugegraph.store.grpc.query.QueryServiceGrpc.getQueryMethod(), options); + CountDownLatch terminated = new CountDownLatch(1); + java.util.concurrent.atomic.AtomicReference terminal = + new java.util.concurrent.atomic.AtomicReference<>(); + try { + call.start(new io.grpc.ClientCall.Listener() { + @Override + public void onClose(Status status, io.grpc.Metadata trailers) { + terminal.set(status); + terminated.countDown(); + } + }, new io.grpc.Metadata()); + call.request(1); + call.sendMessage(QueryRequest.newBuilder().setQueryId("half-close-cancel").build()); + assertTrue(reading.await(1, TimeUnit.SECONDS)); + call.halfClose(); + if (!deadline) { + call.cancel("cancel after half-close", null); + } + assertTrue(terminated.await(3, TimeUnit.SECONDS)); + assertEquals(deadline ? Status.Code.DEADLINE_EXCEEDED : Status.Code.CANCELLED, + terminal.get().getCode()); + assertTrue("transport cancellation must interrupt the worker after half-close", + interrupted.await(1, TimeUnit.SECONDS)); + assertTrue("transport cancellation must release the source without manual progress", + released.await(1, TimeUnit.SECONDS)); + verify(source).close(); + } finally { + fallbackRelease.countDown(); + call.cancel("test cleanup", null); + channel.shutdownNow(); + server.shutdownNow(); + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test(timeout = 10000) + public void testRealQueryClientCloseCancelsEarlyAndPreservesNormalCompletion() throws Exception { + assertClientIteratorClose(true); + assertClientIteratorClose(false); + } + + private static void assertClientIteratorClose(boolean early) throws Exception { + ThreadPoolExecutor pool = pool(1); + ScanIterator source = mock(ScanIterator.class); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch released = new CountDownLatch(1); + CountDownLatch completed = new CountDownLatch(1); + if (early) { + when(source.hasNext()).thenAnswer(invocation -> { + reading.countDown(); + try { + new CountDownLatch(1).await(); + } catch (InterruptedException expected) { + Thread.currentThread().interrupt(); + } + return false; + }); + } + doAnswer(invocation -> { + released.countDown(); + return null; + }).when(source).close(); + java.util.concurrent.atomic.AtomicReference observed = + new java.util.concurrent.atomic.AtomicReference<>(); + AggregativeQueryService service = new AggregativeQueryService(pool, 5000, 10) { + @Override + AggregativeQueryObserver newObserver(StreamObserver sender) { + StreamObserver tracked = new StreamObserver() { + @Override + public void onNext(QueryResponse value) { + sender.onNext(value); + } + + @Override + public void onError(Throwable failure) { + sender.onError(failure); + } + + @Override + public void onCompleted() { + sender.onCompleted(); + completed.countDown(); + } + }; + AggregativeQueryObserver observer = fixture(super.newObserver(tracked), source, new QueryPlan(), null); + observed.set(observer); + return observer; + } + }; + String name = io.grpc.inprocess.InProcessServerBuilder.generateName(); + io.grpc.Server server = io.grpc.inprocess.InProcessServerBuilder.forName(name) + .directExecutor().addService(service).build().start(); + io.grpc.ManagedChannel channel = io.grpc.inprocess.InProcessChannelBuilder.forName(name) + .directExecutor().build(); + org.apache.hugegraph.store.client.query.QueryV2Client.setTestChannel(channel); + try { + org.apache.hugegraph.store.client.query.QueryExecutor executor = + new org.apache.hugegraph.store.client.query.QueryExecutor(null, null, 5000L); + java.lang.reflect.Method getIterator = executor.getClass().getDeclaredMethod( + "getIterator", String.class, QueryRequest.class); + getIterator.setAccessible(true); + org.apache.hugegraph.store.HgKvIterator iterator = + (org.apache.hugegraph.store.HgKvIterator) getIterator.invoke( + executor, "in-process-query-close", QueryRequest.newBuilder() + .setQueryId("client-close").setTable("test").build()); + if (early) { + assertTrue(reading.await(2, TimeUnit.SECONDS)); + FutureTask first = new FutureTask<>(() -> { + iterator.close(); + return null; + }); + Thread closer = start(first); + iterator.close(); + first.get(2, TimeUnit.SECONDS); + closer.join(1000); + } else { + assertFalse(iterator.hasNext()); + iterator.close(); + // A finished batch arrives before transport completion; don't cancel that RPC in teardown. + assertTrue("normal RPC must finish before channel teardown", completed.await(2, TimeUnit.SECONDS)); + } + assertTrue("iterator close must release resources before service shutdown", + released.await(2, TimeUnit.SECONDS)); + if (early) { + org.mockito.ArgumentCaptor failure = + org.mockito.ArgumentCaptor.forClass(Throwable.class); + verify(observed.get()).onError(failure.capture()); + verify(observed.get()).cancel(); + assertEquals(Status.Code.CANCELLED, Status.fromThrowable(failure.getValue()).getCode()); + } else { + verify(observed.get(), never()).onError(org.mockito.ArgumentMatchers.any()); + verify(observed.get(), never()).cancel(); + } + verify(observed.get()).onNext(org.mockito.ArgumentMatchers.any()); + verify(source).close(); + } finally { + channel.shutdownNow(); + server.shutdownNow(); + service.shutdownQueries(); + pool.shutdownNow(); + org.apache.hugegraph.store.client.query.QueryV2Client.setTestChannel(null); + } + } + + @Test(timeout = 5000) + public void testIteratorInitializationFailureKeepsOriginalThrowWithoutNormalCompletion() { + ThreadPoolExecutor pool = pool(1); + IllegalStateException failure = new IllegalStateException("iterator initialization failed"); + AggregativeQueryService service = new AggregativeQueryService(pool, 500, 10) { + @Override + AggregativeQueryObserver newObserver(StreamObserver sender) { + AggregativeQueryObserver observer = spy(super.newObserver(sender)); + doThrow(failure).when(observer).getIterator(org.mockito.ArgumentMatchers.any()); + return observer; + } + }; + ResponseRecorder response = new ResponseRecorder(); + try { + try { + service.query(response).onNext(QueryRequest.getDefaultInstance()); + fail("synchronous iterator initialization must fail"); + } catch (IllegalStateException expected) { + org.junit.Assert.assertSame(failure, expected); + } + assertEquals(0, response.completed.get()); + assertTrue(response.responses.isEmpty()); + service.shutdownQueries(); + } finally { + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testParentSubmissionRejectionKeepsOriginalThrowWithoutNormalCompletion() { + ThreadPoolExecutor pool = pool(1); + pool.shutdown(); + ScanIterator iterator = mock(ScanIterator.class); + QueryPlan plan = mock(QueryPlan.class); + AggregativeQueryService service = service(pool, iterator, plan, 500); + ResponseRecorder response = new ResponseRecorder(); + try { + service.query(response).onNext(QueryRequest.getDefaultInstance()); + fail("synchronous parent submission must be rejected"); + } catch (RejectedExecutionException expected) { + assertEquals(0, response.completed.get()); + assertTrue(response.responses.isEmpty()); + verify(iterator).close(); + verify(plan).clear(); + } finally { + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testFinalSuccessIsSentOnlyAfterPlanAndIteratorCleanup() throws Exception { + ThreadPoolExecutor pool = pool(2); + ScanIterator child = mock(ScanIterator.class); + MultiPartitionIterator iterator = mock(MultiPartitionIterator.class); + when(iterator.getIterators()).thenReturn(Arrays.asList(child)); + ScanIterator result = mock(ScanIterator.class); + when(result.hasNext()).thenReturn(true, false); + when(result.next()).thenReturn(PipelineResult.EMPTY); + QueryPlan plan = mock(QueryPlan.class); + when(plan.hasIteratorResult()).thenReturn(true); + when(plan.execute(PipelineResult.EMPTY)).thenReturn(result); + AtomicBoolean cleared = new AtomicBoolean(); + AtomicBoolean closed = new AtomicBoolean(); + doAnswer(invocation -> { + cleared.set(true); + return null; + }).when(plan).clear(); + doAnswer(invocation -> { + closed.set(true); + return null; + }).when(iterator).close(); + ResponseRecorder response = new ResponseRecorder() { + @Override + public void onNext(QueryResponse batch) { + assertTrue("success cannot precede plan cleanup", cleared.get()); + assertTrue("success cannot precede iterator cleanup", closed.get()); + super.onNext(batch); + } + }; + AggregativeQueryService service = service(pool, iterator, plan, 500); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("success-query").build()); + assertTrue(response.finished.await(1, TimeUnit.SECONDS)); + assertEquals(1, response.responses.size()); + assertTrue(response.responses.get(0).getIsOk()); + assertTrue(response.responses.get(0).getIsFinished()); + service.shutdownQueries(); + verify(child).close(); + } finally { + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test + public void testPartitionInitializationFailureClosesEveryOpenedChild() { + ScanIterator first = mock(ScanIterator.class); + ScanIterator second = mock(ScanIterator.class); + when(first.hasNext()).thenReturn(true); + when(second.hasNext()).thenThrow(new IllegalStateException("child initialization failed")); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1, 2), + (id, key) -> id == 1 ? first : second); + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::getIterators); + verify(first).close(); + verify(second).close(); + iterator.close(); + } + + @Test + public void testPartitionInitializationCleanupFailureIsRetainedByParent() { + ScanIterator first = mock(ScanIterator.class); + ScanIterator second = mock(ScanIterator.class); + when(first.hasNext()).thenReturn(true); + when(second.hasNext()).thenThrow(new IllegalStateException("child initialization failed")); + doThrow(new IllegalStateException("first cleanup failed")).when(first).close(); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1, 2), + (id, key) -> id == 1 ? first : second); + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::getIterators); + verify(first).close(); + verify(second).close(); + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::close); + verify(first).close(); + } + + @Test + public void testEmptyPartitionIteratorsAreReleasedBeforeOwnershipTransfer() { + ScanIterator empty = mock(ScanIterator.class); + ScanIterator nonempty = mock(ScanIterator.class); + when(nonempty.hasNext()).thenReturn(true); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1, 2, 3), + (id, key) -> id == 1 ? empty : + id == 2 ? null : nonempty); + assertEquals(Arrays.asList(nonempty), iterator.getIterators()); + verify(empty).close(); + verify(nonempty, never()).close(); + nonempty.close(); + } + + @Test + public void testSequentialScanReleasesEveryEmptyPartitionBeforeAdvancing() { + ScanIterator first = mock(ScanIterator.class); + ScanIterator second = mock(ScanIterator.class); + ScanIterator nonempty = mock(ScanIterator.class); + when(nonempty.hasNext()).thenReturn(true); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1, 2, 3, 4), + (id, key) -> id == 1 ? first : id == 2 ? second : id == 3 ? null : nonempty); + + assertTrue(iterator.hasNext()); + verify(first).close(); + verify(second).close(); + verify(nonempty, never()).close(); + iterator.close(); + iterator.close(); + verify(nonempty).close(); + } + + @Test + public void testClosedSequentialIteratorDoesNotOpenRemainingPartitions() { + for (boolean initialized : new boolean[]{false, true}) { + ScanIterator first = mock(ScanIterator.class); + ScanIterator second = mock(ScanIterator.class); + when(first.hasNext()).thenReturn(true); + when(second.hasNext()).thenReturn(true); + AtomicInteger opened = new AtomicInteger(); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1, 2), (id, key) -> { + opened.incrementAndGet(); + return id == 1 ? first : second; + }); + if (initialized) { + assertTrue(iterator.hasNext()); + } + iterator.close(); + iterator.close(); + assertFalse(iterator.hasNext()); + assertFalse(iterator.isValid()); + assertEquals(0L, iterator.count()); + assertTrue(iterator.getIterators().isEmpty()); + org.junit.Assert.assertThrows(java.util.NoSuchElementException.class, iterator::next); + assertEquals(initialized ? 1 : 0, opened.get()); + if (initialized) { + verify(first).close(); + } else { + verify(first, never()).close(); + } + verify(second, never()).close(); + } + } + + @Test + public void testSequentialCloseFailureDoesNotReopenRemainingPartitions() { + ScanIterator first = mock(ScanIterator.class); + ScanIterator second = mock(ScanIterator.class); + when(first.hasNext()).thenReturn(true); + when(second.hasNext()).thenReturn(true); + IllegalStateException cleanup = new IllegalStateException("partition cleanup failed"); + doThrow(cleanup).when(first).close(); + AtomicInteger opened = new AtomicInteger(); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1, 2), (id, key) -> { + opened.incrementAndGet(); + return id == 1 ? first : second; + }); + assertTrue(iterator.hasNext()); + IllegalStateException retained = org.junit.Assert.assertThrows(IllegalStateException.class, iterator::close); + org.junit.Assert.assertSame(cleanup, retained.getCause()); + assertFalse(iterator.hasNext()); + assertFalse(iterator.isValid()); + assertEquals(0L, iterator.count()); + assertTrue(iterator.getIterators().isEmpty()); + org.junit.Assert.assertThrows(java.util.NoSuchElementException.class, iterator::next); + org.junit.Assert.assertSame(retained, + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::close)); + assertEquals(1, opened.get()); + verify(first).close(); + verify(second, never()).close(); + } + + @Test + public void testSequentialInitializationFailureReleasesItsCreatedIterator() { + ScanIterator child = mock(ScanIterator.class); + IllegalStateException original = new IllegalStateException("partition initialization failed"); + when(child.hasNext()).thenThrow(original); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1), (id, key) -> child); + + org.junit.Assert.assertSame(original, + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::hasNext)); + verify(child).close(); + iterator.close(); + } + + @Test + public void testSequentialInitializationCleanupFailureRetainsOriginalErrorAndBarrier() { + ScanIterator child = mock(ScanIterator.class); + IllegalStateException original = new IllegalStateException("partition initialization failed"); + IllegalStateException cleanup = new IllegalStateException("partition cleanup failed"); + when(child.hasNext()).thenThrow(original); + doThrow(cleanup).when(child).close(); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1), (id, key) -> child); + + org.junit.Assert.assertSame(original, + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::hasNext)); + assertEquals(1, original.getSuppressed().length); + org.junit.Assert.assertSame(cleanup, original.getSuppressed()[0]); + IllegalStateException retained = org.junit.Assert.assertThrows(IllegalStateException.class, iterator::close); + org.junit.Assert.assertSame(cleanup, retained.getCause()); + verify(child).close(); + } + + @Test + public void testSequentialCountIncludesAlreadyOpenedPartitionAndClosesAllSources() { + ScanIterator first = mock(ScanIterator.class); + ScanIterator second = mock(ScanIterator.class); + when(first.hasNext()).thenReturn(true); + when(second.hasNext()).thenReturn(true); + when(first.count()).thenReturn(7L); + when(second.count()).thenReturn(11L); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1, 2), + (id, key) -> id == 1 ? first : second); + + assertTrue(iterator.hasNext()); + assertEquals(18L, iterator.count()); + assertFalse(iterator.hasNext()); + iterator.close(); + verify(first).close(); + verify(second).close(); + } + + @Test + public void testSequentialCountFailureReleasesCurrentSource() { + ScanIterator child = mock(ScanIterator.class); + IllegalStateException original = new IllegalStateException("partition count failed"); + when(child.hasNext()).thenReturn(true); + when(child.count()).thenThrow(original); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1), (id, key) -> child); + + org.junit.Assert.assertSame(original, + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::count)); + iterator.close(); + verify(child).close(); + } + + @Test + public void testSequentialCountCleanupFailureRetainsOriginalErrorAndBarrier() { + ScanIterator child = mock(ScanIterator.class); + IllegalStateException original = new IllegalStateException("partition count failed"); + IllegalStateException cleanup = new IllegalStateException("partition cleanup failed"); + when(child.hasNext()).thenReturn(true); + when(child.count()).thenThrow(original); + doThrow(cleanup).when(child).close(); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1), (id, key) -> child); + + org.junit.Assert.assertSame(original, + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::count)); + assertEquals(1, original.getSuppressed().length); + org.junit.Assert.assertSame(cleanup, original.getSuppressed()[0]); + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::close); + verify(child).close(); + } + + @Test + public void testSequentialExhaustionCleanupFailureRemainsVisibleToParent() { + ScanIterator child = mock(ScanIterator.class); + when(child.hasNext()).thenReturn(true, false); + IllegalStateException cleanup = new IllegalStateException("exhausted partition cleanup failed"); + doThrow(cleanup).when(child).close(); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1), (id, key) -> child); + + assertTrue(iterator.hasNext()); + org.junit.Assert.assertSame(cleanup, + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::next)); + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::close); + verify(child).close(); + } + + @Test + public void testPlanCleanupContinuesAfterStageFailure() { + QueryStage failed = mock(QueryStage.class); + QueryStage remaining = mock(QueryStage.class); + doThrow(new IllegalStateException("stage cleanup failed")).when(failed).close(); + QueryPlan plan = new QueryPlan(); + plan.addStage(failed); + plan.addStage(remaining); + org.junit.Assert.assertThrows(IllegalStateException.class, plan::clear); + verify(failed).close(); + verify(remaining).close(); + assertTrue(plan.isEmpty()); + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryShutdownTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryShutdownTest.java new file mode 100644 index 0000000000..6712961320 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryShutdownTest.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.hugegraph.store.node.grpc.query; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +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; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.hugegraph.rocksdb.access.RocksDBScanIterator; +import org.apache.hugegraph.rocksdb.access.ScanIterator; +import org.apache.hugegraph.store.business.InnerKeyFilter; +import org.apache.hugegraph.store.business.MultiPartitionIterator; +import org.apache.hugegraph.store.grpc.query.QueryRequest; +import org.apache.hugegraph.store.grpc.query.QueryResponse; +import org.apache.hugegraph.store.node.grpc.GrpcShutdownBarrier; +import org.apache.hugegraph.store.node.grpc.HgStoreStreamImpl; +import org.apache.hugegraph.store.node.grpc.query.model.PipelineResult; +import org.apache.hugegraph.store.node.grpc.query.model.QueryPlan; +import org.apache.hugegraph.store.node.listener.ContextClosedListener; +import org.apache.hugegraph.store.node.task.TTLCleaner; +import org.junit.Test; +import org.rocksdb.RocksIterator; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +public class AggregativeQueryShutdownTest extends AggregativeQueryTestSupport { + + @Test(timeout = 5000) + public void testContextCloseContinuesAfterCancellationResponseFailure() throws Exception { + assertContextCloseContinuesAfterCancellationFailure( + new IllegalStateException("shutdown response failed")); + } + + @Test(timeout = 5000) + public void testContextCloseContinuesAfterCancellationResponseError() throws Exception { + assertContextCloseContinuesAfterCancellationFailure(new AssertionError("shutdown response failed")); + } + + @Test(timeout = 5000) + public void testContextWaitsForIteratorFinallyAfterRpcCancellation() throws Exception { + ThreadPoolExecutor pool = pool(1); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch closingIterator = new CountDownLatch(1); + CountDownLatch allowClose = new CountDownLatch(1); + AtomicBoolean databaseClosed = new AtomicBoolean(); + ScanIterator iterator = mock(ScanIterator.class); + when(iterator.hasNext()).thenAnswer(invocation -> { + reading.countDown(); + try { + new CountDownLatch(1).await(); + } catch (InterruptedException ignored) { + return false; + } + return false; + }); + doAnswer(invocation -> { + closingIterator.countDown(); + awaitUninterruptibly(allowClose); + return null; + }).when(iterator).close(); + AggregativeQueryService service = service(pool, iterator, new QueryPlan(), 500); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + try { + context.getBeanFactory().registerSingleton("queryService", service); + context.getBeanFactory().registerSingleton("storeStream", mock(HgStoreStreamImpl.class)); + context.getBeanFactory().registerSingleton("cleaner", mock(TTLCleaner.class)); + context.getDefaultListableBeanFactory().registerDisposableBean( + "database", () -> databaseClosed.set(true)); + context.register(ContextClosedListener.class, GrpcShutdownBarrier.class); + context.refresh(); + StreamObserver request = service.query(sender()); + request.onNext(QueryRequest.getDefaultInstance()); + assertTrue(reading.await(1, TimeUnit.SECONDS)); + request.onError(Status.CANCELLED.asRuntimeException()); + assertTrue(closingIterator.await(1, TimeUnit.SECONDS)); + FutureTask closing = new FutureTask<>(() -> { + context.close(); + return Thread.currentThread().isInterrupted(); + }); + Thread closer = start(closing); + awaitShutdownWait(closer); + closer.interrupt(); + awaitShutdownWait(closer); + assertFalse("RPC cancellation must not release a still-closing iterator", closing.isDone()); + assertFalse(databaseClosed.get()); + allowClose.countDown(); + assertTrue("context close must preserve interruption", closing.get(2, TimeUnit.SECONDS)); + assertTrue(databaseClosed.get()); + verify(iterator).close(); + } finally { + allowClose.countDown(); + context.close(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testQueuedParentsAreDrainedAndNewQueriesAreRejected() throws Exception { + ThreadPoolExecutor pool = pool(1); + CountDownLatch blockerStarted = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + ScanIterator iterator = mock(ScanIterator.class); + AtomicInteger closed = new AtomicInteger(); + doAnswer(invocation -> { + closed.incrementAndGet(); + return null; + }).when(iterator).close(); + AggregativeQueryService service = service(pool, iterator, new QueryPlan(), 500); + pool.execute(() -> { + blockerStarted.countDown(); + awaitUninterruptibly(release); + }); + try { + assertTrue(blockerStarted.await(1, TimeUnit.SECONDS)); + service.query(sender()).onNext(QueryRequest.getDefaultInstance()); + service.query(sender()).onNext(QueryRequest.getDefaultInstance()); + service.stopAcceptingQueries(); + try { + service.query(sender()); + fail("new queries must be rejected after admission closes"); + } catch (StatusRuntimeException expected) { + assertEquals(Status.Code.UNAVAILABLE, expected.getStatus().getCode()); + } + FutureTask closing = close(service); + awaitShutdownWait(start(closing)); + assertFalse(closing.isDone()); + release.countDown(); + closing.get(2, TimeUnit.SECONDS); + assertEquals(2, closed.get()); + verify(iterator, never()).hasNext(); + assertTrue(pool.awaitTermination(1, TimeUnit.SECONDS)); + } finally { + release.countDown(); + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testQueuedPartitionRunsCleanupAfterParentTimeout() throws Exception { + CountDownLatch childQueued = new CountDownLatch(1); + CountDownLatch releaseSubmission = new CountDownLatch(1); + AtomicInteger submissions = new AtomicInteger(); + ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, + new LinkedBlockingQueue<>()) { + @Override + public void execute(Runnable task) { + super.execute(task); + if (submissions.incrementAndGet() == 2) { + childQueued.countDown(); + awaitUninterruptibly(releaseSubmission); + } + } + }; + ScanIterator child = mock(ScanIterator.class); + MultiPartitionIterator iterator = mock(MultiPartitionIterator.class); + when(iterator.getIterators()).thenReturn(Arrays.asList(child)); + QueryPlan plan = mock(QueryPlan.class); + when(plan.hasIteratorResult()).thenReturn(true); + AggregativeQueryService service = service(pool, iterator, plan, 50); + try { + StreamObserver request = service.query(sender()); + request.onNext(QueryRequest.getDefaultInstance()); + assertTrue(childQueued.await(1, TimeUnit.SECONDS)); + request.onError(Status.CANCELLED.asRuntimeException()); + FutureTask closing = close(service); + start(closing); + releaseSubmission.countDown(); + closing.get(2, TimeUnit.SECONDS); + verify(child).close(); + verify(child, never()).hasNext(); + verify(plan).clear(); + verify(iterator).close(); + } finally { + releaseSubmission.countDown(); + service.shutdownQueries(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testIdleRpcAndInitializationFailureDoNotBlockShutdown() throws Exception { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + AggregativeQueryService service = new AggregativeQueryService(pool, 500, 10) { + @Override + AggregativeQueryObserver newObserver(StreamObserver sender) { + AggregativeQueryObserver observer = spy(super.newObserver(sender)); + doReturn(iterator).when(observer).getIterator(org.mockito.ArgumentMatchers.any()); + org.mockito.Mockito.doThrow(new IllegalArgumentException("invalid query plan")) + .when(observer).buildPlan(org.mockito.ArgumentMatchers.any()); + return observer; + } + }; + ResponseRecorder failedResponse = new ResponseRecorder(); + try { + service.query(sender()); + try { + service.query(failedResponse).onNext(QueryRequest.getDefaultInstance()); + fail("invalid query must fail initialization"); + } catch (IllegalArgumentException expected) { + assertEquals("invalid query plan", expected.getMessage()); + } + assertEquals("framework must retain the synchronous error", 0, failedResponse.completed.get()); + assertTrue(failedResponse.responses.isEmpty()); + service.shutdownQueries(); + verify(iterator).close(); + } finally { + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testParentCleanupFailuresReportErrorAndKeepShutdownBlocked() throws Exception { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + QueryPlan plan = mock(QueryPlan.class); + when(plan.onlyStopStage()).thenReturn(true); + IllegalStateException planFailure = new IllegalStateException("plan cleanup failed"); + IllegalStateException iteratorFailure = new IllegalStateException("iterator cleanup failed"); + doThrow(planFailure).when(plan).clear(); + doThrow(iteratorFailure).when(iterator).close(); + ResponseRecorder response = new ResponseRecorder(); + AggregativeQueryService service = service(pool, iterator, plan, 500); + FutureTask closing = close(service); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("cleanup-query").build()); + assertTrue(response.finished.await(1, TimeUnit.SECONDS)); + assertError(response, "cleanup-query"); + assertEquals("plan cleanup failed", response.responses.get(0).getMessage()); + assertEquals(1, planFailure.getSuppressed().length); + org.junit.Assert.assertSame(iteratorFailure, planFailure.getSuppressed()[0]); + verify(plan).clear(); + verify(iterator).close(); + awaitShutdownWait(start(closing)); + assertFalse(closing.isDone()); + assertFalse("failed cleanup cannot release the executor", pool.isShutdown()); + } finally { + releaseMockQueries(service); + closing.run(); + closing.get(1, TimeUnit.SECONDS); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testSpringDoesNotDestroyDatabaseWhenIteratorCloseFails() throws Exception { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + doThrow(new IllegalStateException("native iterator close failed")).when(iterator).close(); + AggregativeQueryService service = service(pool, iterator, new QueryPlan(), 500); + AtomicBoolean databaseClosed = new AtomicBoolean(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.getBeanFactory().registerSingleton("queryService", service); + context.getBeanFactory().registerSingleton("storeStream", mock(HgStoreStreamImpl.class)); + context.getBeanFactory().registerSingleton("cleaner", mock(TTLCleaner.class)); + context.getDefaultListableBeanFactory().registerDisposableBean( + "database", () -> databaseClosed.set(true)); + context.register(ContextClosedListener.class, GrpcShutdownBarrier.class); + context.refresh(); + ResponseRecorder response = new ResponseRecorder(); + FutureTask closing = new FutureTask<>(() -> { + context.close(); + return null; + }); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("spring-query").build()); + assertTrue(response.finished.await(1, TimeUnit.SECONDS)); + assertError(response, "spring-query"); + Thread closer = start(closing); + awaitShutdownWait(closer); + closer.interrupt(); + awaitShutdownWait(closer); + assertFalse(closing.isDone()); + assertFalse("Spring must retain the database after cleanup failure", databaseClosed.get()); + } finally { + releaseMockQueries(service); + closing.run(); + closing.get(1, TimeUnit.SECONDS); + context.close(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testSynchronousFailureRetainsCleanupErrorAndShutdownBarrier() throws Exception { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + IllegalStateException original = new IllegalStateException("plan initialization failed"); + IllegalStateException cleanup = new IllegalStateException("iterator cleanup failed"); + doThrow(cleanup).when(iterator).close(); + AggregativeQueryService service = new AggregativeQueryService(pool, 500, 10) { + @Override + AggregativeQueryObserver newObserver(StreamObserver sender) { + AggregativeQueryObserver observer = spy(super.newObserver(sender)); + doReturn(iterator).when(observer).getIterator(org.mockito.ArgumentMatchers.any()); + doThrow(original).when(observer).buildPlan(org.mockito.ArgumentMatchers.any()); + return observer; + } + }; + ResponseRecorder response = new ResponseRecorder(); + FutureTask closing = close(service); + try { + IllegalStateException thrown = org.junit.Assert.assertThrows(IllegalStateException.class, + () -> service.query(response).onNext(QueryRequest.getDefaultInstance())); + org.junit.Assert.assertSame(original, thrown); + assertEquals(1, thrown.getSuppressed().length); + org.junit.Assert.assertSame(cleanup, thrown.getSuppressed()[0]); + assertEquals(0, response.completed.get()); + assertTrue(response.responses.isEmpty()); + awaitShutdownWait(start(closing)); + assertFalse(closing.isDone()); + } finally { + releaseMockQueries(service); + closing.run(); + closing.get(1, TimeUnit.SECONDS); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testPartialPartitionInitializationCleanupFailureRetainsQueryBarrier() throws Exception { + ThreadPoolExecutor pool = pool(1); + ScanIterator first = mock(ScanIterator.class); + when(first.hasNext()).thenReturn(true); + doThrow(new IllegalStateException("opened partition cleanup failed")).when(first).close(); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1, 2), (id, key) -> { + if (id == 1) { + return first; + } + throw new IllegalStateException("second partition initialization failed"); + }); + QueryPlan plan = mock(QueryPlan.class); + when(plan.hasIteratorResult()).thenReturn(true); + ResponseRecorder response = new ResponseRecorder(); + AggregativeQueryService service = service(pool, iterator, plan, 500); + FutureTask closing = close(service); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("init-query").build()); + assertTrue(response.finished.await(1, TimeUnit.SECONDS)); + assertError(response, "init-query"); + verify(first).close(); + verify(plan).clear(); + awaitShutdownWait(start(closing)); + assertFalse(closing.isDone()); + } finally { + releaseMockQueries(service); + closing.run(); + closing.get(1, TimeUnit.SECONDS); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testSequentialEmptyPartitionCleanupFailureRetainsQueryBarrier() throws Exception { + ThreadPoolExecutor pool = pool(1); + ScanIterator child = mock(ScanIterator.class); + doThrow(new IllegalStateException("empty partition cleanup failed")).when(child).close(); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1), (id, key) -> child); + AggregativeQueryService service = service(pool, iterator, new QueryPlan(), 500); + ResponseRecorder response = new ResponseRecorder(); + FutureTask closing = close(service); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("empty-query").build()); + assertTrue(response.finished.await(1, TimeUnit.SECONDS)); + assertError(response, "empty-query"); + verify(child).close(); + awaitShutdownWait(start(closing)); + assertFalse(closing.isDone()); + assertFalse("failed sequential cleanup cannot release the executor", pool.isShutdown()); + } finally { + releaseMockQueries(service); + closing.run(); + closing.get(1, TimeUnit.SECONDS); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testNativeAutomaticCloseFailureRetainsSequentialQueryBarrier() throws Exception { + ThreadPoolExecutor pool = pool(1); + RocksIterator raw = mock(RocksIterator.class); + when(raw.isOwningHandle()).thenReturn(true); + IllegalStateException failure = new IllegalStateException("native close failed once"); + doThrow(failure).doNothing().when(raw).close(); + // Native close fails before the callback or reference release can be reached. + RocksDBScanIterator child = new RocksDBScanIterator<>(raw, null, null, + ScanIterator.Trait.SCAN_ANY, null, ignored -> { }); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1), (id, key) -> child); + AggregativeQueryService service = service(pool, iterator, new QueryPlan(), 500); + ResponseRecorder response = new ResponseRecorder(); + FutureTask closing = close(service); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("native-close-query").build()); + assertTrue(response.finished.await(1, TimeUnit.SECONDS)); + assertError(response, "native-close-query"); + org.junit.Assert.assertSame(failure, + org.junit.Assert.assertThrows(IllegalStateException.class, child::close)); + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::close); + verify(raw).close(); + awaitShutdownWait(start(closing)); + assertFalse("automatic native-close failure must retain the query registry", closing.isDone()); + assertFalse(pool.isShutdown()); + } finally { + releaseMockQueries(service); + closing.run(); + closing.get(1, TimeUnit.SECONDS); + pool.shutdownNow(); + } + } + + @Test(timeout = 10000) + public void testSupplierInnerKeyFilterCloseFailureRetainsQueryBarrier() throws Exception { + for (int constructor = 0; constructor < 3; constructor++) { + assertSupplierFilterCloseFailureRetainsQueryBarrier(constructor); + } + } + + @Test(timeout = 5000) + public void testPartitionCloseFailurePreventsSuccessfulAggregation() throws Exception { + ThreadPoolExecutor pool = pool(2); + ScanIterator child = mock(ScanIterator.class); + doThrow(new IllegalStateException("child cleanup failed")).when(child).close(); + MultiPartitionIterator iterator = mock(MultiPartitionIterator.class); + when(iterator.getIterators()).thenReturn(Arrays.asList(child)); + QueryPlan plan = mock(QueryPlan.class); + when(plan.hasIteratorResult()).thenReturn(true); + ResponseRecorder response = new ResponseRecorder(); + AggregativeQueryService service = service(pool, iterator, plan, 500); + FutureTask closing = close(service); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("child-query").build()); + assertTrue(response.finished.await(1, TimeUnit.SECONDS)); + assertError(response, "child-query"); + verify(plan, never()).execute(PipelineResult.EMPTY); + verify(plan).clear(); + verify(iterator).close(); + verify(child).close(); + awaitShutdownWait(start(closing)); + assertFalse(closing.isDone()); + } finally { + releaseMockQueries(service); + closing.run(); + closing.get(1, TimeUnit.SECONDS); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testRejectedPartitionCloseFailureStillClosesRemainingChildren() throws Exception { + AtomicInteger submissions = new AtomicInteger(); + ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, + new LinkedBlockingQueue<>()) { + @Override + public void execute(Runnable task) { + if (submissions.incrementAndGet() > 1) { + throw new RejectedExecutionException("partition rejected"); + } + super.execute(task); + } + }; + ScanIterator failed = mock(ScanIterator.class); + doThrow(new IllegalStateException("rejected child cleanup failed")).when(failed).close(); + ScanIterator remaining = mock(ScanIterator.class); + MultiPartitionIterator iterator = mock(MultiPartitionIterator.class); + when(iterator.getIterators()).thenReturn(Arrays.asList(failed, remaining)); + QueryPlan plan = mock(QueryPlan.class); + when(plan.hasIteratorResult()).thenReturn(true); + ResponseRecorder response = new ResponseRecorder(); + AggregativeQueryService service = service(pool, iterator, plan, 500); + FutureTask closing = close(service); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("reject-query").build()); + assertTrue(response.finished.await(1, TimeUnit.SECONDS)); + assertError(response, "reject-query"); + verify(failed).close(); + verify(remaining).close(); + verify(plan).clear(); + verify(iterator).close(); + awaitShutdownWait(start(closing)); + assertFalse(closing.isDone()); + } finally { + releaseMockQueries(service); + closing.run(); + closing.get(1, TimeUnit.SECONDS); + pool.shutdownNow(); + } + } + + private static void assertSupplierFilterCloseFailureRetainsQueryBarrier(int constructor) throws Exception { + ThreadPoolExecutor pool = pool(1); + RocksIterator raw = mock(RocksIterator.class); + when(raw.isOwningHandle()).thenReturn(true); + IllegalStateException failure = new IllegalStateException("supplier iterator close failed once"); + doThrow(failure).doNothing().when(raw).close(); + RocksDBScanIterator child = new RocksDBScanIterator<>(raw, null, null, + ScanIterator.Trait.SCAN_ANY, null, ignored -> { }); + AtomicInteger supplierReturned = new AtomicInteger(); + MultiPartitionIterator iterator = MultiPartitionIterator.of(Arrays.asList(1), (id, key) -> { + InnerKeyFilter filter; + if (constructor == 0) { + filter = new InnerKeyFilter<>(child); + } else if (constructor == 1) { + filter = new InnerKeyFilter<>(child, true); + } else { + filter = new InnerKeyFilter<>(child, 10, 20); + } + supplierReturned.incrementAndGet(); + return filter; + }); + AggregativeQueryService service = service(pool, iterator, new QueryPlan(), 500); + ResponseRecorder response = new ResponseRecorder(); + FutureTask closing = close(service); + try { + service.query(response).onNext(QueryRequest.newBuilder().setQueryId("supplier-close-query").build()); + assertTrue(response.finished.await(1, TimeUnit.SECONDS)); + assertError(response, "supplier-close-query"); + assertEquals("factory must transfer ownership before any failing prefetch", 1, supplierReturned.get()); + org.junit.Assert.assertThrows(IllegalStateException.class, iterator::close); + verify(raw).close(); + awaitShutdownWait(start(closing)); + assertFalse("factory prefetch failure must retain the aggregate query barrier", closing.isDone()); + assertFalse(pool.isShutdown()); + } finally { + releaseMockQueries(service); + closing.run(); + closing.get(1, TimeUnit.SECONDS); + pool.shutdownNow(); + } + } + + private static void assertContextCloseContinuesAfterCancellationFailure(Throwable failure) throws Exception { + ThreadPoolExecutor pool = pool(1); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch closingIterator = new CountDownLatch(1); + CountDownLatch allowClose = new CountDownLatch(1); + AtomicBoolean iteratorClosed = new AtomicBoolean(); + AtomicBoolean databaseClosed = new AtomicBoolean(); + AtomicBoolean prematureDestruction = new AtomicBoolean(); + ScanIterator iterator = mock(ScanIterator.class); + when(iterator.hasNext()).thenAnswer(invocation -> { + reading.countDown(); + try { + new CountDownLatch(1).await(); + } catch (InterruptedException ignored) { + return false; + } + return false; + }); + doAnswer(invocation -> { + closingIterator.countDown(); + awaitUninterruptibly(allowClose); + iteratorClosed.set(true); + return null; + }).when(iterator).close(); + AggregativeQueryService service = service(pool, iterator, new QueryPlan(), 500); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + try { + context.getBeanFactory().registerSingleton("queryService", service); + context.getBeanFactory().registerSingleton("storeStream", mock(HgStoreStreamImpl.class)); + context.getBeanFactory().registerSingleton("cleaner", mock(TTLCleaner.class)); + context.getDefaultListableBeanFactory().registerDisposableBean("database", () -> { + prematureDestruction.set(!iteratorClosed.get() || !pool.isTerminated()); + databaseClosed.set(true); + }); + context.register(ContextClosedListener.class, GrpcShutdownBarrier.class); + context.refresh(); + StreamObserver firstSender = sender(); + StreamObserver secondSender = sender(); + StreamObserver first = service.query(firstSender); + StreamObserver second = service.query(secondSender); + Field field = AggregativeQueryService.class.getDeclaredField("queries"); + field.setAccessible(true); + // Fail the first cancellation in the actual HashSet snapshot, regardless of identity hashes. + boolean firstCancelsFirst = ((Set) field.get(service)).iterator().next() == first; + StreamObserver broken = firstCancelsFirst ? firstSender : secondSender; + StreamObserver active = firstCancelsFirst ? second : first; + doThrow(failure).when(broken).onError(org.mockito.ArgumentMatchers.any(Throwable.class)); + active.onNext(QueryRequest.getDefaultInstance()); + assertTrue(reading.await(1, TimeUnit.SECONDS)); + FutureTask closing = new FutureTask<>(() -> { + context.close(); + return null; + }); + Thread closer = start(closing); + assertTrue("other queries must still be cancelled", closingIterator.await(1, TimeUnit.SECONDS)); + awaitShutdownWait(closer); + assertFalse(closing.isDone()); + assertFalse(databaseClosed.get()); + assertFalse(pool.isShutdown()); + allowClose.countDown(); + closing.get(2, TimeUnit.SECONDS); + assertTrue(databaseClosed.get()); + assertFalse("Spring must destroy databases only after query cleanup and worker termination", + prematureDestruction.get()); + verify(broken).onError(org.mockito.ArgumentMatchers.any(Throwable.class)); + verify(broken, never()).onCompleted(); + verify(iterator).close(); + } finally { + allowClose.countDown(); + service.shutdownQueries(); + context.close(); + pool.shutdownNow(); + } + } + + private static void releaseMockQueries(AggregativeQueryService service) throws Exception { + // Only test mocks are discarded here; production deliberately has no bypass for this barrier. + Field field = AggregativeQueryService.class.getDeclaredField("queries"); + field.setAccessible(true); + synchronized (service) { + ((Set) field.get(service)).clear(); + service.notifyAll(); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryTestSupport.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryTestSupport.java new file mode 100644 index 0000000000..b8d7f40563 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/query/AggregativeQueryTestSupport.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.hugegraph.store.node.grpc.query; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import io.grpc.stub.StreamObserver; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.LockSupport; +import org.apache.hugegraph.rocksdb.access.ScanIterator; +import org.apache.hugegraph.store.grpc.query.QueryResponse; +import org.apache.hugegraph.store.node.grpc.query.model.QueryPlan; + +abstract class AggregativeQueryTestSupport { + + static void awaitShutdownWait(Thread closer) { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + while (System.nanoTime() < deadline) { + if (closer.getState() == Thread.State.TIMED_WAITING && + Arrays.stream(closer.getStackTrace()).anyMatch(frame -> + frame.getClassName().equals(AggregativeQueryService.class.getName()) && + frame.getMethodName().equals("shutdownQueries"))) { + return; + } + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1)); + } + fail("shutdown must enter the aggregate-query cleanup wait"); + } + + static void assertError(ResponseRecorder response, String queryId) { + assertEquals(1, response.responses.size()); + QueryResponse error = response.responses.get(0); + assertEquals(queryId, error.getQueryId()); + assertFalse(error.getIsOk()); + assertFalse(error.getIsFinished()); + assertFalse(error.getMessage().isEmpty()); + } + + static ThreadPoolExecutor pool(int size) { + return (ThreadPoolExecutor) Executors.newFixedThreadPool(size); + } + + static AggregativeQueryService service(ThreadPoolExecutor pool, ScanIterator iterator, + QueryPlan plan, long timeout) { + return new AggregativeQueryService(pool, timeout, 10) { + @Override + AggregativeQueryObserver newObserver(StreamObserver sender) { + return fixture(super.newObserver(sender), iterator, plan, null); + } + }; + } + + static AggregativeQueryObserver fixture(AggregativeQueryObserver observer, + ScanIterator iterator, + QueryPlan plan, CountDownLatch parentReturned) { + AggregativeQueryObserver managed = spy(observer); + doReturn(iterator).when(managed).getIterator(org.mockito.ArgumentMatchers.any()); + doReturn(plan).when(managed).buildPlan(org.mockito.ArgumentMatchers.any()); + if (parentReturned != null) { + doAnswer(invocation -> { + try { + invocation.callRealMethod(); + return null; + } finally { + parentReturned.countDown(); + } + }).when(managed).sendData(); + } + return managed; + } + + @SuppressWarnings("unchecked") + static StreamObserver sender() { + return mock(StreamObserver.class); + } + + static FutureTask close(AggregativeQueryService service) { + return new FutureTask<>(() -> { + service.shutdownQueries(); + return null; + }); + } + + static Thread start(FutureTask task) { + Thread thread = new Thread(task, "test-aggregate-query-close"); + thread.setDaemon(true); + thread.start(); + return thread; + } + + static void awaitUninterruptibly(CountDownLatch latch) { + boolean interrupted = false; + try { + for (;;) { + try { + latch.await(); + return; + } catch (InterruptedException e) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + static class ResponseRecorder implements StreamObserver { + + final List responses = new CopyOnWriteArrayList<>(); + final AtomicInteger completed = new AtomicInteger(); + final AtomicInteger errors = new AtomicInteger(); + final CountDownLatch received = new CountDownLatch(1); + final CountDownLatch finished = new CountDownLatch(1); + + @Override + public void onNext(QueryResponse response) { + this.responses.add(response); + this.received.countDown(); + } + + @Override + public void onError(Throwable error) { + this.errors.incrementAndGet(); + this.finished.countDown(); + } + + @Override + public void onCompleted() { + this.completed.incrementAndGet(); + this.finished.countDown(); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/query/UnaryQueryLifecycleTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/query/UnaryQueryLifecycleTest.java new file mode 100644 index 0000000000..f48164563f --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/node/grpc/query/UnaryQueryLifecycleTest.java @@ -0,0 +1,544 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.store.node.grpc.query; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.grpc.Context; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.ServerCallStreamObserver; +import io.grpc.stub.StreamObserver; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +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 java.util.concurrent.atomic.AtomicReference; +import org.apache.hugegraph.rocksdb.access.ScanIterator; +import org.apache.hugegraph.store.business.MultiPartitionIterator; +import org.apache.hugegraph.store.grpc.query.AggregateFunc; +import org.apache.hugegraph.store.grpc.query.QueryRequest; +import org.apache.hugegraph.store.grpc.query.QueryResponse; +import org.apache.hugegraph.store.query.KvSerializer; +import org.junit.Test; + +public class UnaryQueryLifecycleTest extends AggregativeQueryTestSupport { + + private static AggregativeQueryService unaryService(ThreadPoolExecutor pool, ScanIterator iterator, + RuntimeException initializationFailure) { + return new AggregativeQueryService(pool, 1000, 10) { + @Override + AggregativeQueryObserver newObserver(StreamObserver sender) { + AggregativeQueryObserver owner = spy(super.newObserver(sender)); + if (initializationFailure == null) { + doReturn(iterator).when(owner).getIterator(org.mockito.ArgumentMatchers.any()); + doReturn(iterator).when(owner).getCountIterator(org.mockito.ArgumentMatchers.any()); + } else { + doThrow(initializationFailure).when(owner).getIterator(org.mockito.ArgumentMatchers.any()); + doThrow(initializationFailure).when(owner).getCountIterator(org.mockito.ArgumentMatchers.any()); + } + return owner; + } + }; + } + + private static void call(AggregativeQueryService service, boolean count, + StreamObserver response) { + QueryRequest request = QueryRequest.newBuilder().setQueryId("unary-query").build(); + if (count) { + service.count(request, response); + } else { + service.query0(request, response); + } + } + + @Test(timeout = 5000) + public void testSuccessFollowsIteratorReleaseAndCompletesOnce() { + for (boolean count : new boolean[]{false, true}) { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + AtomicBoolean closed = new AtomicBoolean(); + doAnswer(invocation -> { closed.set(true); return null; }).when(iterator).close(); + ResponseRecorder response = new ResponseRecorder() { + @Override + public void onNext(QueryResponse value) { + assertTrue(closed.get()); + super.onNext(value); + } + }; + AggregativeQueryService service = unaryService(pool, iterator, null); + try { + call(service, count, response); + assertEquals(1, response.responses.size()); + assertTrue(response.responses.get(0).getIsOk()); + assertTrue(response.responses.get(0).getIsFinished()); + assertEquals(count ? 1 : 0, response.responses.get(0).getDataCount()); + assertEquals(1, response.completed.get()); + verify(iterator).close(); + service.shutdownQueries(); + } finally { + pool.shutdownNow(); + } + } + } + + @Test(timeout = 5000) + public void testCountPreservesFunctionPayload() { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + when(iterator.hasNext()).thenReturn(true, true, false); + AggregativeQueryService service = unaryService(pool, iterator, null); + ResponseRecorder response = new ResponseRecorder(); + try { + QueryRequest request = QueryRequest.newBuilder().setQueryId("unary-query") + .addFunctions(AggregateFunc.getDefaultInstance()) + .addFunctions(AggregateFunc.getDefaultInstance()).build(); + service.count(request, response); + assertArrayEquals(KvSerializer.toBytes(List.of()), + response.responses.get(0).getData(0).getKey().toByteArray()); + assertArrayEquals(KvSerializer.toBytes(List.of(new AtomicLong(2), new AtomicLong(2))), + response.responses.get(0).getData(0).getValue().toByteArray()); + service.shutdownQueries(); + } finally { + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testCountPartitionsRunInParallelAndCloseBeforeResponse() throws Exception { + ThreadPoolExecutor pool = pool(2); + CountDownLatch entered = new CountDownLatch(2); + CountDownLatch release = new CountDownLatch(1); + MultiPartitionIterator iterator = mock(MultiPartitionIterator.class); + ScanIterator first = mock(ScanIterator.class); + ScanIterator second = mock(ScanIterator.class); + when(iterator.getIterators()).thenReturn(List.of(first, second)); + for (ScanIterator child : List.of(first, second)) { + when(child.hasNext()).thenAnswer(invocation -> { + entered.countDown(); + awaitUninterruptibly(release); + return false; + }); + } + AggregativeQueryService service = unaryService(pool, iterator, null); + ResponseRecorder response = new ResponseRecorder(); + FutureTask request = new FutureTask<>(() -> { call(service, true, response); return null; }); + try { + start(request); + assertTrue(entered.await(1, TimeUnit.SECONDS)); + assertTrue(response.responses.isEmpty()); + release.countDown(); + request.get(1, TimeUnit.SECONDS); + verify(first).close(); + verify(second).close(); + verify(iterator).close(); + assertTrue(response.responses.get(0).getIsOk()); + assertEquals(1, response.completed.get()); + service.shutdownQueries(); + } finally { + release.countDown(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testRejectedCountChildWaitsForAcceptedChildCleanup() throws Exception { + AtomicInteger submissions = new AtomicInteger(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch rejected = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>()) { + @Override + public void execute(Runnable task) { + if (submissions.incrementAndGet() == 2) { + rejected.countDown(); + throw new RejectedExecutionException("rejected child"); + } + super.execute(task); + } + }; + MultiPartitionIterator iterator = mock(MultiPartitionIterator.class); + ScanIterator first = mock(ScanIterator.class); + ScanIterator second = mock(ScanIterator.class); + when(iterator.getIterators()).thenReturn(List.of(first, second)); + when(first.hasNext()).thenAnswer(invocation -> { + entered.countDown(); + awaitUninterruptibly(release); + return false; + }); + AggregativeQueryService service = unaryService(pool, iterator, null); + ResponseRecorder response = new ResponseRecorder(); + FutureTask request = new FutureTask<>(() -> { call(service, true, response); return null; }); + try { + start(request); + assertTrue(entered.await(1, TimeUnit.SECONDS)); + assertTrue(rejected.await(1, TimeUnit.SECONDS)); + assertTrue(response.responses.isEmpty()); + assertFalse(request.isDone()); + release.countDown(); + request.get(1, TimeUnit.SECONDS); + verify(first).close(); + verify(second).close(); + verify(iterator).close(); + assertError(response, "unary-query"); + assertEquals("rejected child", response.responses.get(0).getMessage()); + assertEquals(1, response.completed.get()); + service.shutdownQueries(); + } finally { + release.countDown(); + pool.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testInitializationFailureReportsSingleErrorAndReleasesOwner() { + for (boolean count : new boolean[]{false, true}) { + ThreadPoolExecutor pool = pool(1); + AggregativeQueryService service = unaryService(pool, null, new IllegalStateException("init")); + ResponseRecorder response = new ResponseRecorder(); + try { + call(service, count, response); + assertError(response, "unary-query"); + assertEquals("init", response.responses.get(0).getMessage()); + assertEquals(1, response.completed.get()); + service.shutdownQueries(); + } finally { + pool.shutdownNow(); + } + } + } + + @Test(timeout = 5000) + public void testReadFailureClosesIteratorAndPreservesPrimaryError() { + for (boolean count : new boolean[]{false, true}) { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + when(iterator.hasNext()).thenThrow(new IllegalStateException("read")); + AggregativeQueryService service = unaryService(pool, iterator, null); + ResponseRecorder response = new ResponseRecorder(); + try { + call(service, count, response); + assertError(response, "unary-query"); + assertEquals("read", response.responses.get(0).getMessage()); + assertEquals(1, response.completed.get()); + verify(iterator).close(); + service.shutdownQueries(); + } finally { + pool.shutdownNow(); + } + } + } + + @Test(timeout = 5000) + public void testCloseFailureRetainsOwnerAndSuppressesFailureOnReadError() throws Exception { + for (boolean count : new boolean[]{false, true}) { + for (boolean readFailure : new boolean[]{false, true}) { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + IllegalStateException primary = new IllegalStateException("read"); + IllegalStateException cleanup = new IllegalStateException("close"); + if (readFailure) { + when(iterator.hasNext()).thenThrow(primary); + } + doThrow(cleanup).when(iterator).close(); + AggregativeQueryService service = unaryService(pool, iterator, null); + ResponseRecorder response = new ResponseRecorder(); + try { + call(service, count, response); + assertError(response, "unary-query"); + assertEquals(readFailure ? "read" : "close", response.responses.get(0).getMessage()); + assertEquals(1, response.completed.get()); + if (readFailure) { + assertEquals(1, primary.getSuppressed().length); + assertSame(cleanup, primary.getSuppressed()[0]); + } + Field field = AggregativeQueryService.class.getDeclaredField("queries"); + field.setAccessible(true); + assertEquals(1, ((Set) field.get(service)).size()); + } finally { + pool.shutdownNow(); + } + } + } + } + + @Test(timeout = 5000) + public void testCancellationDuringReadWaitsForIteratorCleanup() throws Exception { + for (boolean count : new boolean[]{false, true}) { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicBoolean closed = new AtomicBoolean(); + when(iterator.hasNext()).thenAnswer(invocation -> { + entered.countDown(); + awaitUninterruptibly(release); + return false; + }); + doAnswer(invocation -> { closed.set(true); return null; }).when(iterator).close(); + AggregativeQueryService service = unaryService(pool, iterator, null); + ResponseRecorder response = new ResponseRecorder(); + FutureTask request = new FutureTask<>(() -> { call(service, count, response); return null; }); + FutureTask closing = close(service); + try { + start(request); + assertTrue(entered.await(1, TimeUnit.SECONDS)); + awaitShutdownWait(start(closing)); + assertFalse(closing.isDone()); + assertFalse(closed.get()); + release.countDown(); + request.get(1, TimeUnit.SECONDS); + closing.get(1, TimeUnit.SECONDS); + assertTrue(closed.get()); + assertTrue(response.responses.isEmpty()); + assertEquals(0, response.completed.get()); + assertEquals(1, response.errors.get()); + verify(iterator).close(); + } finally { + release.countDown(); + pool.shutdownNow(); + } + } + } + + @Test(timeout = 5000) + @SuppressWarnings("unchecked") + public void testTransportCancellationClosesWithoutTerminalCallback() throws Exception { + for (boolean count : new boolean[]{false, true}) { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + when(iterator.hasNext()).thenAnswer(invocation -> { + entered.countDown(); + awaitUninterruptibly(release); + return false; + }); + AggregativeQueryService service = unaryService(pool, iterator, null); + ServerCallStreamObserver response = mock(ServerCallStreamObserver.class); + AtomicReference cancellation = new AtomicReference<>(); + doAnswer(invocation -> { cancellation.set(invocation.getArgument(0)); return null; }) + .when(response).setOnCancelHandler(org.mockito.ArgumentMatchers.any()); + FutureTask request = new FutureTask<>(() -> { call(service, count, response); return null; }); + try { + start(request); + assertTrue(entered.await(1, TimeUnit.SECONDS)); + cancellation.get().run(); + release.countDown(); + request.get(1, TimeUnit.SECONDS); + service.shutdownQueries(); + verify(iterator).close(); + verify(response, org.mockito.Mockito.never()).onCompleted(); + verify(response, org.mockito.Mockito.never()).onNext(org.mockito.ArgumentMatchers.any()); + verify(response, org.mockito.Mockito.never()).onError(org.mockito.ArgumentMatchers.any()); + } finally { + release.countDown(); + pool.shutdownNow(); + } + } + } + + @Test(timeout = 5000) + public void testContextCancellationInterruptsBothUnaryReads() throws Exception { + for (boolean count : new boolean[]{false, true}) { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch interrupted = new CountDownLatch(1); + CountDownLatch fallback = new CountDownLatch(1); + when(iterator.hasNext()).thenAnswer(invocation -> { + entered.countDown(); + try { + fallback.await(); + } catch (InterruptedException expected) { + interrupted.countDown(); + Thread.currentThread().interrupt(); + } + return false; + }); + AggregativeQueryService service = unaryService(pool, iterator, null); + ResponseRecorder response = new ResponseRecorder(); + Context.CancellableContext context = Context.current().withCancellation(); + FutureTask request = new FutureTask<>(() -> { + context.run(() -> call(service, count, response)); + return null; + }); + try { + start(request); + assertTrue(entered.await(1, TimeUnit.SECONDS)); + context.cancel(new IllegalStateException("client canceled")); + assertTrue("context must interrupt the unary read", interrupted.await(1, TimeUnit.SECONDS)); + request.get(1, TimeUnit.SECONDS); + verify(iterator).close(); + assertTrue(response.responses.isEmpty()); + assertEquals(0, response.completed.get()); + assertEquals(0, response.errors.get()); + assertNoUnaryOwners(service); + } finally { + fallback.countDown(); + context.cancel(null); + service.shutdownQueries(); + pool.shutdownNow(); + } + } + } + + @Test(timeout = 5000) + public void testAlreadyCanceledContextNeverOpensUnaryIterator() throws Exception { + for (boolean count : new boolean[]{false, true}) { + ThreadPoolExecutor pool = pool(1); + ScanIterator iterator = mock(ScanIterator.class); + AggregativeQueryService service = unaryService(pool, iterator, null); + ResponseRecorder response = new ResponseRecorder(); + Context.CancellableContext context = Context.current().withCancellation(); + context.cancel(null); + try { + context.run(() -> call(service, count, response)); + verify(iterator, org.mockito.Mockito.never()).hasNext(); + verify(iterator, org.mockito.Mockito.never()).close(); + assertTrue(response.responses.isEmpty()); + assertEquals(0, response.completed.get()); + assertNoUnaryOwners(service); + } finally { + Thread.interrupted(); + service.shutdownQueries(); + pool.shutdownNow(); + } + } + } + + @Test(timeout = 15000) + public void testRealNettyCancellationInterruptsBothUnaryReads() throws Exception { + for (boolean count : new boolean[]{false, true}) { + ThreadPoolExecutor pool = pool(2); + ExecutorService callbacks = Executors.newFixedThreadPool(2); + ScanIterator iterator = mock(ScanIterator.class); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch interrupted = new CountDownLatch(1); + CountDownLatch closed = new CountDownLatch(1); + CountDownLatch fallback = new CountDownLatch(1); + when(iterator.hasNext()).thenAnswer(invocation -> { + entered.countDown(); + try { + fallback.await(); + } catch (InterruptedException expected) { + interrupted.countDown(); + Thread.currentThread().interrupt(); + } + return false; + }); + doAnswer(invocation -> { closed.countDown(); return null; }).when(iterator).close(); + AggregativeQueryService service = unaryService(pool, iterator, null); + io.grpc.Server server = io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.forPort(0) + .executor(callbacks).addService(service).build().start(); + io.grpc.ManagedChannel channel = io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder + .forAddress("localhost", server.getPort()).usePlaintext().build(); + io.grpc.ClientCall call = channel.newCall(count ? + org.apache.hugegraph.store.grpc.query.QueryServiceGrpc.getCountMethod() : + org.apache.hugegraph.store.grpc.query.QueryServiceGrpc.getQuery0Method(), + io.grpc.CallOptions.DEFAULT); + CountDownLatch terminated = new CountDownLatch(1); + AtomicReference terminal = new AtomicReference<>(); + AtomicInteger responses = new AtomicInteger(); + try { + call.start(new io.grpc.ClientCall.Listener() { + @Override + public void onMessage(QueryResponse response) { + responses.incrementAndGet(); + } + + @Override + public void onClose(Status status, io.grpc.Metadata trailers) { + terminal.set(status); + terminated.countDown(); + } + }, new io.grpc.Metadata()); + call.request(1); + call.sendMessage(QueryRequest.newBuilder().setQueryId("netty-unary").build()); + call.halfClose(); + assertTrue(entered.await(2, TimeUnit.SECONDS)); + call.cancel("cancel blocked unary read", null); + assertTrue(terminated.await(2, TimeUnit.SECONDS)); + assertEquals(Status.Code.CANCELLED, terminal.get().getCode()); + assertTrue("transport context must interrupt unary read", interrupted.await(2, TimeUnit.SECONDS)); + assertTrue("cancellation must release iterator", closed.await(2, TimeUnit.SECONDS)); + service.shutdownQueries(); + verify(iterator).close(); + assertEquals(0, responses.get()); + assertNoUnaryOwners(service); + } finally { + fallback.countDown(); + call.cancel("test cleanup", null); + channel.shutdownNow(); + server.shutdownNow(); + service.shutdownQueries(); + callbacks.shutdownNow(); + pool.shutdownNow(); + } + } + } + + private static void assertNoUnaryOwners(AggregativeQueryService service) throws Exception { + Field field = AggregativeQueryService.class.getDeclaredField("queries"); + field.setAccessible(true); + synchronized (service) { + assertTrue(((Set) field.get(service)).isEmpty()); + } + } + + @Test(timeout = 5000) + public void testBothUnaryEndpointsRejectRequestsAfterAdmissionCloses() { + ThreadPoolExecutor pool = pool(1); + AggregativeQueryService service = unaryService(pool, mock(ScanIterator.class), null); + try { + service.stopAcceptingQueries(); + for (boolean count : new boolean[]{false, true}) { + try { + call(service, count, new ResponseRecorder()); + org.junit.Assert.fail("closed service must reject unary RPCs"); + } catch (StatusRuntimeException e) { + assertEquals(Status.Code.UNAVAILABLE, e.getStatus().getCode()); + } + } + } finally { + pool.shutdownNow(); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/raftcore/RaftSuiteTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/raftcore/RaftSuiteTest.java index be24270ea7..ff6d441765 100644 --- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/raftcore/RaftSuiteTest.java +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/raftcore/RaftSuiteTest.java @@ -17,8 +17,10 @@ package org.apache.hugegraph.store.raftcore; +import org.apache.hugegraph.store.core.raft.HgStoreStateMachineTest; import org.apache.hugegraph.store.core.raft.PartitionStateMachineTest; import org.apache.hugegraph.store.core.snapshot.SnapshotHandlerTest; +import org.apache.hugegraph.store.util.FutureClosureTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -27,7 +29,9 @@ BytesCarrierTest.class, ZeroByteStringHelperTest.class, SnapshotHandlerTest.class, - PartitionStateMachineTest.class + PartitionStateMachineTest.class, + HgStoreStateMachineTest.class, + FutureClosureTest.class }) public class RaftSuiteTest { diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/CRC64Test.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/CRC64Test.java new file mode 100644 index 0000000000..51fa24374b --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/CRC64Test.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.hugegraph.store.rocksdb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import java.nio.charset.StandardCharsets; + +import org.apache.hugegraph.rocksdb.access.util.CRC64; +import org.junit.Test; + +public class CRC64Test { + + private static final byte[] CHECK_INPUT = "123456789".getBytes(StandardCharsets.US_ASCII); + + @Test + public void testMatchesCrc64Ecma182CheckValue() { + CRC64 crc = new CRC64(); + assertEquals(0L, crc.getValue()); + crc.update(CHECK_INPUT); + // Standard check value of CRC-64/ECMA-182 (poly 0x42F0E1EBA9EA3693, init 0, no reflection) + assertEquals(0x6C40DF5F0B497347L, crc.getValue()); + } + + @Test + public void testAllUpdateOverloadsAreEquivalent() { + CRC64 whole = new CRC64(); + whole.update(CHECK_INPUT); + + CRC64 perInt = new CRC64(); + for (byte b : CHECK_INPUT) { + // update(int) must only consider the low eight bits + perInt.update(b | 0xABCD00); + } + + CRC64 split = new CRC64(); + split.update(CHECK_INPUT, 0, 4); + split.update(CHECK_INPUT, 4, CHECK_INPUT.length - 4); + + assertEquals(whole.getValue(), perInt.getValue()); + assertEquals(whole.getValue(), split.getValue()); + } + + @Test + public void testOffsetAndLengthSelectOnlyTheRequestedSlice() { + byte[] padded = new byte[CHECK_INPUT.length + 6]; + padded[0] = 'x'; + padded[1] = 'y'; + padded[2] = 'z'; + System.arraycopy(CHECK_INPUT, 0, padded, 3, CHECK_INPUT.length); + padded[padded.length - 1] = 'q'; + + CRC64 crc = new CRC64(); + crc.update(padded, 3, CHECK_INPUT.length); + assertEquals(0x6C40DF5F0B497347L, crc.getValue()); + + CRC64 empty = new CRC64(); + empty.update(padded, 5, 0); + assertEquals(0L, empty.getValue()); + } + + @Test + public void testResetAndSensitivityToSingleBitChanges() { + CRC64 crc = new CRC64(); + crc.update(CHECK_INPUT); + long expected = crc.getValue(); + + byte[] flipped = CHECK_INPUT.clone(); + flipped[4] ^= 0x01; + crc.reset(); + assertEquals(0L, crc.getValue()); + crc.update(flipped); + assertNotEquals(expected, crc.getValue()); + + crc.reset(); + crc.update(CHECK_INPUT); + assertEquals(expected, crc.getValue()); + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/RocksDbSuiteTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/RocksDbSuiteTest.java index 59c1cd9986..16e0c2c495 100644 --- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/RocksDbSuiteTest.java +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/RocksDbSuiteTest.java @@ -24,7 +24,10 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ - RocksDBFactoryTest.class + RocksDBFactoryTest.class, + CRC64Test.class, + ZipUtilsPathSafetyTest.class, + SessionOperatorTest.class }) @Slf4j diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/SessionOperatorTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/SessionOperatorTest.java new file mode 100644 index 0000000000..70de5aee32 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/SessionOperatorTest.java @@ -0,0 +1,293 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.store.rocksdb; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.hugegraph.rocksdb.access.DBStoreException; +import org.apache.hugegraph.rocksdb.access.RocksDBSession; +import org.apache.hugegraph.rocksdb.access.RocksDBSession.BackendColumn; +import org.apache.hugegraph.rocksdb.access.ScanIterator; +import org.apache.hugegraph.rocksdb.access.SessionOperator; +import org.apache.hugegraph.rocksdb.access.SessionOperatorImpl; +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.Timeout; + +public class SessionOperatorTest extends BaseRocksDbTest { + + private static final String TABLE = "tbl"; + private static final String GRAPH = "op-graph"; + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + // Runs @Before, the test and @After on one thread, so a stuck CF lock fails the test + @Rule + public Timeout timeout = Timeout.seconds(60); + + private ExecutorService prober; + private String dataPath; + private RocksDBSession session; + private int probes; + + private static byte[] b(String s) { + return s.getBytes(StandardCharsets.UTF_8); + } + + private static List keys(ScanIterator it) { + List keys = new ArrayList<>(); + try { + while (it.hasNext()) { + BackendColumn col = it.next(); + keys.add(new String(col.name, StandardCharsets.UTF_8)); + } + } finally { + it.close(); + } + return keys; + } + + @Before + public void setUp() throws Exception { + this.prober = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "cf-lock-prober"); + t.setDaemon(true); + return t; + }); + this.dataPath = this.folder.newFolder("data").getAbsolutePath(); + this.session = new RocksDBSession(hConfig, this.dataPath, GRAPH, 0); + this.session.checkTable(TABLE); + } + + @After + public void tearDown() { + try { + if (this.session != null) { + releaseCfReadLockHolds(); + this.session.close(); + } + } finally { + this.prober.shutdownNow(); + } + } + + /** + * A test that fails between prepare() and commit() still holds the CF read lock on this + * thread, and close() would then wait forever for the CF write lock. + */ + private void releaseCfReadLockHolds() { + ReentrantReadWriteLock.ReadLock readLock = this.session.getCfHandleReadLock(); + while (true) { + try { + readLock.unlock(); + } catch (IllegalMonitorStateException e) { + return; + } + } + } + + private void putAll(String... keys) { + SessionOperator op = this.session.sessionOp(); + op.prepare(); + for (String key : keys) { + op.put(TABLE, b(key), b("v-" + key)); + } + op.commit(); + } + + private List allKeys() { + return keys(this.session.sessionOp().scan(TABLE)); + } + + /** + * Creating a column family takes the CF write lock, so another thread can only do it when + * the caller does not leak the read lock taken by prepare(). + */ + private void assertCfLockReleased() throws Exception { + String probe = "probe-" + this.probes++; + Future future = this.prober.submit(() -> this.session.checkTable(probe)); + future.get(10, TimeUnit.SECONDS); + assertTrue(this.session.tableIsExist(probe)); + } + + @Test + public void testIncreaseOneCarriesAndRejectsOverflow() { + assertArrayEquals(new byte[]{1, 3}, SessionOperatorImpl.increaseOne(new byte[]{1, 2})); + assertArrayEquals(new byte[]{2, 0, 0}, + SessionOperatorImpl.increaseOne(new byte[]{1, (byte) 0xff, (byte) 0xff})); + assertThrows(DBStoreException.class, + () -> SessionOperatorImpl.increaseOne(new byte[]{(byte) 0xff, (byte) 0xff})); + } + + @Test + public void testCommitAppliesTheBatchAndReturnsItsSize() throws Exception { + SessionOperator op = this.session.sessionOp(); + op.prepare(); + op.put(TABLE, b("k1"), b("v1")); + op.put(TABLE, b("k2"), b("v2")); + op.delete(TABLE, b("k1")); + assertNull("batched writes are invisible before commit", op.get(TABLE, b("k2"))); + assertEquals(Integer.valueOf(3), op.commit()); + + assertNull(op.get(TABLE, b("k1"))); + assertArrayEquals(b("v2"), op.get(TABLE, b("k2"))); + + op.prepare(); + assertEquals(Integer.valueOf(0), op.commit()); + assertCfLockReleased(); + } + + @Test + public void testRollbackDiscardsPendingWritesAndReleasesTheLock() throws Exception { + putAll("keep"); + SessionOperator op = this.session.sessionOp(); + op.prepare(); + op.put(TABLE, b("lost"), b("v")); + op.delete(TABLE, b("keep")); + op.rollback(); + + assertNull(op.get(TABLE, b("lost"))); + assertArrayEquals(b("v-keep"), op.get(TABLE, b("keep"))); + assertCfLockReleased(); + + // The operator stays usable after a rollback + op.prepare(); + op.put(TABLE, b("after"), b("v")); + assertEquals(Integer.valueOf(1), op.commit()); + assertArrayEquals(b("v"), op.get(TABLE, b("after"))); + } + + @Test + public void testDeletePrefixRemovesOnlyPrefixedKeys() throws Exception { + putAll("a", "ab", "ab0", "ab~", "ac", "b"); + SessionOperator op = this.session.sessionOp(); + op.prepare(); + op.deletePrefix(TABLE, b("ab")); + op.commit(); + + assertEquals(List.of("a", "ac", "b"), allKeys()); + + byte[] unbounded = new byte[]{(byte) 0xff, (byte) 0xff}; + op.prepare(); + try { + assertThrows(DBStoreException.class, () -> op.deletePrefix(TABLE, unbounded)); + } finally { + op.rollback(); + } + assertCfLockReleased(); + } + + @Test + public void testDeleteRangeIsHalfOpenAndValidatesBounds() throws Exception { + putAll("k1", "k2", "k3", "k4", "k5"); + SessionOperator op = this.session.sessionOp(); + + op.deleteRange(TABLE, b("k2"), b("k4")); + assertEquals(List.of("k1", "k4", "k5"), allKeys()); + + DBStoreException inverted = assertThrows(DBStoreException.class, + () -> op.deleteRange(TABLE, b("k5"), b("k1"))); + assertTrue(inverted.getMessage(), inverted.getMessage().contains("is lower than")); + assertThrows(IllegalArgumentException.class, () -> op.deleteRange(TABLE, null, b("k5"))); + assertThrows(IllegalArgumentException.class, () -> op.deleteRange(TABLE, b("k1"), null)); + + assertEquals("rejected ranges must not delete anything", List.of("k1", "k4", "k5"), + allKeys()); + assertCfLockReleased(); + } + + @Test + public void testScanIteratorHoldsASessionReferenceUntilClosed() { + putAll("p1", "p2", "q1"); + assertEquals(1, this.session.getRefCount()); + + ScanIterator it = this.session.sessionOp().scan(TABLE, b("p")); + assertEquals(2, this.session.getRefCount()); + assertEquals(1, this.session.getIteratorMap().size()); + assertTrue(it.hasNext()); + it.close(); + it.close(); + assertEquals(1, this.session.getRefCount()); + assertTrue(this.session.getIteratorMap().isEmpty()); + + // Exhausting an iterator releases it without an explicit close + assertEquals(2L, this.session.sessionOp().keyCount(b("p"), b("q"), TABLE)); + assertEquals(1, this.session.getRefCount()); + assertTrue(this.session.getIteratorMap().isEmpty()); + } + + @Test + public void testPrefixAndRangeScansHonourTheirBounds() { + putAll("p1", "p2", "pz", "q1", "r1"); + SessionOperator op = this.session.sessionOp(); + + assertEquals(List.of("p1", "p2", "pz"), keys(op.scan(TABLE, b("p")))); + assertEquals(List.of("p2", "pz", "q1"), + keys(op.scan(TABLE, b("p2"), b("r1"), ScanIterator.Trait.SCAN_LT_END))); + assertEquals(List.of("p2", "pz", "q1", "r1"), + keys(op.scan(TABLE, b("p2"), b("r1"), ScanIterator.Trait.SCAN_LTE_END))); + assertTrue(keys(op.scan(TABLE, b("x"))).isEmpty()); + assertEquals(1, this.session.getRefCount()); + } + + @Test + public void testSnapshotRoundTripAndMissingSnapshot() throws Exception { + putAll("s1", "s2"); + String snapshot = new File(this.folder.getRoot(), "snapshot").getAbsolutePath(); + this.session.saveSnapshot(snapshot); + assertTrue(new File(snapshot, "CURRENT").exists()); + assertFalse("temporary checkpoint dir must be renamed away", + new File(snapshot + "_temp").exists()); + + DBStoreException missing = assertThrows(DBStoreException.class, () -> + this.session.loadSnapshot(snapshot + "-missing", 7)); + assertTrue(missing.getMessage(), missing.getMessage().contains("not exists")); + // The CF write lock taken by loadSnapshot must be released on failure + putAll("s3"); + assertCfLockReleased(); + + this.session.loadSnapshot(snapshot, 7); + assertTrue(new File(this.dataPath, GRAPH + "_7").isDirectory()); + this.session.close(); + this.session = null; + + // Reopening picks the newest versioned path, i.e. the loaded snapshot without "s3" + this.session = new RocksDBSession(hConfig, this.dataPath, GRAPH, 7); + assertTrue(this.session.getDbPath().endsWith(GRAPH + "_7")); + assertEquals(List.of("s1", "s2"), allKeys()); + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/ZipUtilsPathSafetyTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/ZipUtilsPathSafetyTest.java new file mode 100644 index 0000000000..d1724dde60 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/rocksdb/ZipUtilsPathSafetyTest.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.hugegraph.store.rocksdb; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.apache.hugegraph.rocksdb.access.util.CRC64; +import org.apache.hugegraph.rocksdb.access.util.ZipUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** + * Snapshot archives are produced and unpacked by two ZipUtils copies: the RocksDB one used for + * partition snapshots and the core one. Both must keep extracted files inside the target dir. + */ +public class ZipUtilsPathSafetyTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + private static void write(File file, String content) throws IOException { + Files.createDirectories(file.getParentFile().toPath()); + Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8)); + } + + private static String read(File file) throws IOException { + return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + } + + private File maliciousZip(String... entryNames) throws IOException { + File zip = this.folder.newFile(); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip))) { + zos.putNextEntry(new ZipEntry("snapshot/ok.sst")); + zos.write("ok".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + for (String name : entryNames) { + zos.putNextEntry(new ZipEntry(name)); + zos.write("pwned".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + } + } + return zip; + } + + @Test + public void testRoundTripKeepsNestedLayoutAndChecksumsAgree() throws IOException { + File root = this.folder.newFolder("root"); + write(new File(root, "snapshot/CURRENT"), "MANIFEST-000001"); + write(new File(root, "snapshot/sub/dir/000004.sst"), "sst-bytes"); + Files.createDirectories(new File(root, "snapshot/empty").toPath()); + File zip = new File(this.folder.getRoot(), "snapshot.zip"); + File out = this.folder.newFolder("out"); + + CRC64 written = new CRC64(); + ZipUtils.compress(root.getAbsolutePath(), "snapshot", zip.getAbsolutePath(), written); + CRC64 read = new CRC64(); + ZipUtils.decompress(zip.getAbsolutePath(), out.getAbsolutePath(), read); + + assertEquals("MANIFEST-000001", read(new File(out, "snapshot/CURRENT"))); + assertEquals("sst-bytes", read(new File(out, "snapshot/sub/dir/000004.sst"))); + // The checksum covers the whole archive on both sides, so it detects transfer damage + assertTrue(written.getValue() != 0L); + assertEquals(written.getValue(), read.getValue()); + + File coreOut = this.folder.newFolder("core-out"); + CRC32 coreRead = new CRC32(); + org.apache.hugegraph.store.util.ZipUtils.decompress(zip.getAbsolutePath(), + coreOut.getAbsolutePath(), coreRead); + assertArrayEquals(Files.readAllBytes(new File(out, "snapshot/sub/dir/000004.sst").toPath()), + Files.readAllBytes(new File(coreOut, "snapshot/sub/dir/000004.sst").toPath())); + } + + @Test + public void testRocksDbDecompressSkipsEntriesEscapingTheTargetDir() throws IOException { + File out = this.folder.newFolder("out"); + String absoluteEntry = new File(this.folder.getRoot(), "absolute.txt") + .getAbsolutePath().replace(File.separatorChar, '/'); + assertFalse(out.toPath().resolve(absoluteEntry).normalize().startsWith(out.toPath())); + File zip = maliciousZip("../escaped.txt", "snapshot/../../escaped2.txt", "../out-evil/x.txt", + absoluteEntry); + + ZipUtils.decompress(zip.getAbsolutePath(), out.getAbsolutePath(), new CRC32()); + + assertEquals("ok", read(new File(out, "snapshot/ok.sst"))); + assertFalse(new File(this.folder.getRoot(), "escaped.txt").exists()); + assertFalse(new File(this.folder.getRoot(), "escaped2.txt").exists()); + assertFalse(new File(this.folder.getRoot(), "out-evil").exists()); + assertFalse(new File(this.folder.getRoot(), "absolute.txt").exists()); + } + + @Test + public void testCoreDecompressRejectsEntriesEscapingTheTargetDir() throws IOException { + File out = this.folder.newFolder("out"); + // "../out-evil" shares the textual prefix of the target dir and must still be rejected + for (String evil : new String[]{"../escaped.txt", "snapshot/../../escaped.txt", + "../out-evil/x.txt"}) { + File zip = maliciousZip(evil); + IOException e = assertThrows(IOException.class, () -> + org.apache.hugegraph.store.util.ZipUtils.decompress( + zip.getAbsolutePath(), out.getAbsolutePath(), new CRC32())); + assertTrue(e.getMessage(), e.getMessage().contains("outside of the target dir")); + assertFalse(new File(this.folder.getRoot(), "escaped.txt").exists()); + assertFalse(new File(this.folder.getRoot(), "out-evil").exists()); + // Each attack has its own archive; TemporaryFolder owns cleanup. + } + } + + @Test + public void testCoreZipUtilsValidatesArguments() throws IOException { + String root = this.folder.newFolder("root").getAbsolutePath(); + String zip = new File(this.folder.getRoot(), "x.zip").getAbsolutePath(); + String out = this.folder.newFolder("out").getAbsolutePath(); + + assertThrows(IllegalArgumentException.class, () -> + org.apache.hugegraph.store.util.ZipUtils.compress(null, "s", zip, new CRC32())); + assertThrows(IllegalArgumentException.class, () -> + org.apache.hugegraph.store.util.ZipUtils.compress(root, "s", zip, null)); + assertThrows(IllegalArgumentException.class, () -> + org.apache.hugegraph.store.util.ZipUtils.decompress(zip, null, new CRC32())); + + IOException missingDir = assertThrows(IOException.class, () -> + org.apache.hugegraph.store.util.ZipUtils.compress(root, "missing", zip, new CRC32())); + assertTrue(missingDir.getMessage().contains("Source directory does not exist")); + assertFalse("no archive may be created for a missing source", new File(zip).exists()); + + IOException missingZip = assertThrows(IOException.class, () -> + org.apache.hugegraph.store.util.ZipUtils.decompress(zip, out, new CRC32())); + assertTrue(missingZip.getMessage().contains("Source file does not exist")); + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/schema/SchemaDriverTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/schema/SchemaDriverTest.java new file mode 100644 index 0000000000..4040451427 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/schema/SchemaDriverTest.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.hugegraph.store.schema; + +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import org.apache.hugegraph.HugeGraphSupplier; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.exception.NotAllowException; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.pd.client.KvClient; +import org.apache.hugegraph.pd.client.PDConfig; +import org.apache.hugegraph.pd.common.PDException; +import org.apache.hugegraph.pd.grpc.kv.KResponse; +import org.apache.hugegraph.pd.grpc.kv.ScanPrefixResponse; +import org.apache.hugegraph.pd.grpc.kv.WatchEvent; +import org.apache.hugegraph.pd.grpc.kv.WatchKv; +import org.apache.hugegraph.pd.grpc.kv.WatchResponse; +import org.apache.hugegraph.pd.grpc.kv.WatchType; +import org.apache.hugegraph.store.HgStoreEngine; +import org.apache.hugegraph.store.business.BusinessHandlerImpl; +import org.apache.hugegraph.store.options.HgStoreEngineOptions; +import org.apache.hugegraph.store.util.HgStoreException; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.junit.Assert; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class SchemaDriverTest { + + private AtomicReference instance; + private SchemaDriver previousInstance; + + @Before + public void setUp() throws Exception { + this.instance = instanceReference(); + this.previousInstance = this.instance.getAndSet(null); + } + + @After + public void tearDown() { + this.instance.set(this.previousInstance); + } + + @Test + public void testDestroyClosesOwnedKvClient() { + TrackingKvClient client = new TrackingKvClient(); + SchemaDriver driver = new SchemaDriver(client, 10, 60_000L); + this.instance.set(driver); + + try { + SchemaDriver.destroy(); + + Assert.assertTrue(client.closed); + Assert.assertNull(SchemaDriver.getInstance()); + } finally { + client.close(); + } + } + + @Test + public void testDestroyUnpublishesInstanceBeforeClosingResources() throws Exception { + BlockingCloseKvClient client = new BlockingCloseKvClient(); + SchemaDriver driver = new SchemaDriver(client, 10, 60_000L); + this.instance.set(driver); + Thread destroyThread = new Thread(SchemaDriver::destroy, "schema-driver-destroy"); + try { + destroyThread.start(); + client.awaitCloseStarted(); + + Assert.assertNull(SchemaDriver.getInstance()); + } finally { + client.allowClose.countDown(); + destroyThread.join(5000L); + client.close(); + } + + Assert.assertFalse(destroyThread.isAlive()); + Assert.assertTrue(client.closed); + } + + @Test + public void testInitDoesNotWaitForDestroyCleanup() throws Exception { + BlockingCloseKvClient client = new BlockingCloseKvClient(); + SchemaDriver driver = new SchemaDriver(client, 10, 60_000L); + this.instance.set(driver); + AtomicReference initFailure = new AtomicReference<>(); + CountDownLatch initFinished = new CountDownLatch(1); + Thread destroyThread = new Thread(SchemaDriver::destroy, "schema-driver-destroy"); + Thread initThread = new Thread(() -> { + try { + SchemaDriver.init(PDConfig.of("127.0.0.1:8686"), 10, 60_000L); + } catch (Throwable throwable) { + initFailure.set(throwable); + } finally { + initFinished.countDown(); + } + }, "schema-driver-init"); + boolean initCompletedDuringCleanup; + try { + destroyThread.start(); + client.awaitCloseStarted(); + initThread.start(); + initCompletedDuringCleanup = initFinished.await(1L, TimeUnit.SECONDS); + } finally { + client.allowClose.countDown(); + destroyThread.join(5000L); + initThread.join(5000L); + client.close(); + } + + Assert.assertTrue(initCompletedDuringCleanup); + Assert.assertTrue(initFailure.get() instanceof NotAllowException); + Assert.assertFalse(destroyThread.isAlive()); + Assert.assertFalse(initThread.isAlive()); + } + + @Test + public void testConcurrentDestroyWaitsForCleanup() throws Exception { + BlockingCloseKvClient client = new BlockingCloseKvClient(); + SchemaDriver driver = new SchemaDriver(client, 10, 60_000L); + this.instance.set(driver); + CountDownLatch secondDestroyStarted = new CountDownLatch(1); + CountDownLatch secondDestroyReturned = new CountDownLatch(1); + Thread firstDestroy = new Thread(SchemaDriver::destroy, "schema-driver-destroy-first"); + Thread secondDestroy = new Thread(() -> { + secondDestroyStarted.countDown(); + SchemaDriver.destroy(); + secondDestroyReturned.countDown(); + }, "schema-driver-destroy-second"); + boolean returnedBeforeCleanup; + try { + firstDestroy.start(); + client.awaitCloseStarted(); + secondDestroy.start(); + Assert.assertTrue(secondDestroyStarted.await(5L, TimeUnit.SECONDS)); + returnedBeforeCleanup = secondDestroyReturned.await(500L, TimeUnit.MILLISECONDS); + } finally { + client.allowClose.countDown(); + firstDestroy.join(5000L); + secondDestroy.join(5000L); + client.close(); + } + + Assert.assertFalse(returnedBeforeCleanup); + Assert.assertFalse(firstDestroy.isAlive()); + Assert.assertFalse(secondDestroy.isAlive()); + } + + @Test + public void testConstructorFailureClosesOwnedKvClient() { + FailingListenKvClient client = new FailingListenKvClient(); + + try { + new SchemaDriver(client, 10, 60_000L); + Assert.fail("SchemaDriver construction should fail"); + } catch (HugeException expected) { + Assert.assertEquals(2, client.listenCalls); + Assert.assertTrue(client.closed); + } finally { + client.close(); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testStoreCleanupClosesDriverAndReleasesGraphWrappers() throws Exception { + Field cacheField = BusinessHandlerImpl.class.getDeclaredField("GRAPH_SUPPLIER_CACHE"); + cacheField.setAccessible(true); + Map cache = (Map) cacheField.get(null); + Map previousCache = new HashMap<>(cache); + HugeGraphSupplier supplier = (HugeGraphSupplier) Proxy.newProxyInstance( + HugeGraphSupplier.class.getClassLoader(), new Class[]{HugeGraphSupplier.class}, + (proxy, method, args) -> null); + TrackingKvClient client = new TrackingKvClient(); + SchemaDriver driver = new SchemaDriver(client, 10, 60_000L); + this.instance.set(driver); + try { + cache.put("space/graph", supplier); + BusinessHandlerImpl.clearCache(); + Assert.assertTrue(cache.isEmpty()); + Assert.assertFalse(client.closed); + Assert.assertSame(driver, SchemaDriver.getInstance()); + + cache.put("space/graph", supplier); + BusinessHandlerImpl.closeSchemaResources(); + Assert.assertTrue(client.closed); + Assert.assertNull(SchemaDriver.getInstance()); + Assert.assertTrue(cache.isEmpty()); + BusinessHandlerImpl.closeSchemaResources(); + + TrackingKvClient replacementClient = new TrackingKvClient(); + this.instance.set(new SchemaDriver(replacementClient, 10, 60_000L)); + BusinessHandlerImpl.closeSchemaResources(); + Assert.assertTrue(replacementClient.closed); + } finally { + SchemaDriver.destroy(); + cache.putAll(previousCache); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testStoreCleanupReleasesWrappersWhenClientCloseFails() throws Exception { + Field cacheField = BusinessHandlerImpl.class.getDeclaredField("GRAPH_SUPPLIER_CACHE"); + cacheField.setAccessible(true); + Map cache = (Map) cacheField.get(null); + Map previousCache = new HashMap<>(cache); + HugeGraphSupplier supplier = (HugeGraphSupplier) Proxy.newProxyInstance( + HugeGraphSupplier.class.getClassLoader(), new Class[]{HugeGraphSupplier.class}, + (proxy, method, args) -> null); + TrackingKvClient client = new TrackingKvClient() { + @Override + public void close() { + super.close(); + throw new IllegalStateException("close failed"); + } + }; + this.instance.set(new SchemaDriver(client, 10, 60_000L)); + try { + cache.put("space/graph", supplier); + try { + BusinessHandlerImpl.closeSchemaResources(); + Assert.fail("Client close failure must propagate"); + } catch (IllegalStateException expected) { + Assert.assertEquals("close failed", expected.getMessage()); + } + Assert.assertTrue(client.closed); + Assert.assertNull(SchemaDriver.getInstance()); + Assert.assertTrue(cache.isEmpty()); + BusinessHandlerImpl.closeSchemaResources(); + } finally { + SchemaDriver.destroy(); + cache.putAll(previousCache); + } + } + + @Test + public void testClosingStoreRejectsNewGraphSupplierWithoutOpeningPdClient() { + HgStoreEngine engine = HgStoreEngine.getInstance(); + boolean previousClosing = engine.isClosing().getAndSet(true); + try { + BusinessHandlerImpl.getGraphSupplier("schema-driver-test/closing"); + Assert.fail("Closing Store must reject schema client creation"); + } catch (HgStoreException expected) { + Assert.assertEquals("Store is closing", expected.getMessage()); + Assert.assertNull(SchemaDriver.getInstance()); + } finally { + engine.isClosing().set(previousClosing); + } + } + + @Test + public void testSchemaLookupCachesByIdAndNameAndWatchClearsOnlyTargetGraph() { + CachingKvClient client = new CachingKvClient(); + SchemaDriver driver = new SchemaDriver(client, 10, 60_000L); + this.instance.set(driver); + try { + Assert.assertEquals(4, client.listeners.size()); + PropertyKey first = driver.propertyKey("space", "graph", IdGenerator.of(1), null); + Assert.assertSame(first, driver.propertyKey("space", "graph", "name", null)); + PropertyKey other = driver.propertyKey("space", "other", IdGenerator.of(1), null); + Assert.assertEquals(2, client.getCalls); + + client.publish("HUGEGRAPH/hg/EVENT/GRAPH/SCHEMA/CLEAR", "space-graph"); + Assert.assertNotSame(first, driver.propertyKey("space", "graph", IdGenerator.of(1), null)); + Assert.assertSame(other, driver.propertyKey("space", "other", IdGenerator.of(1), null)); + Assert.assertEquals(3, client.getCalls); + } finally { + SchemaDriver.destroy(); + } + } + + @Test + public void testSchemaClearJsonFromServerEvictsOnlyNamedGraphAndKeepsLegacyFormat() { + CachingKvClient client = new CachingKvClient(); + SchemaDriver driver = new SchemaDriver(client, "alternate", 10, 60_000L); + this.instance.set(driver); + String eventKey = "HUGEGRAPH/alternate/EVENT/GRAPH/SCHEMA/CLEAR"; + try { + PropertyKey first = driver.propertyKey("space", "graph-with-hyphen", IdGenerator.of(1), null); + PropertyKey other = driver.propertyKey("space", "other", IdGenerator.of(1), null); + // Current Server CachedSchemaTransactionV2 publishes a source-bearing JSON event. + client.publish(eventKey, "{\"graph\":\"space-graph-with-hyphen\",\"source\":\"server-process-1\"}"); + PropertyKey refreshed = driver.propertyKey("space", "graph-with-hyphen", IdGenerator.of(1), null); + Assert.assertNotSame(first, refreshed); + Assert.assertSame(other, driver.propertyKey("space", "other", IdGenerator.of(1), null)); + Assert.assertEquals(3, client.getCalls); + + client.publish(eventKey, "space-graph-with-hyphen"); + Assert.assertNotSame(refreshed, + driver.propertyKey("space", "graph-with-hyphen", IdGenerator.of(1), null)); + Assert.assertSame(other, driver.propertyKey("space", "other", IdGenerator.of(1), null)); + Assert.assertEquals(4, client.getCalls); + } finally { + SchemaDriver.destroy(); + } + } + + @Test + public void testMalformedSchemaClearPayloadDoesNotInvalidateUnrelatedCaches() { + CachingKvClient client = new CachingKvClient(); + SchemaDriver driver = new SchemaDriver(client, 10, 60_000L); + this.instance.set(driver); + String eventKey = "HUGEGRAPH/hg/EVENT/GRAPH/SCHEMA/CLEAR"; + try { + PropertyKey cached = driver.propertyKey("space", "graph", IdGenerator.of(1), null); + for (String payload : new String[]{"{broken", "{\"source\":\"server-process-1\"}", "{\"graph\":null}"}) { + client.publish(eventKey, payload); + } + Assert.assertSame(cached, driver.propertyKey("space", "graph", IdGenerator.of(1), null)); + Assert.assertEquals(1, client.getCalls); + } finally { + SchemaDriver.destroy(); + } + } + + @Test + public void testGraphAndGraphSpaceRemovalInvalidateSchemaCaches() { + CachingKvClient client = new CachingKvClient(); + SchemaDriver driver = new SchemaDriver(client, 10, 60_000L); + this.instance.set(driver); + try { + PropertyKey first = driver.propertyKey("space", "graph", IdGenerator.of(1), null); + PropertyKey other = driver.propertyKey("space", "other", IdGenerator.of(1), null); + PropertyKey elsewhere = driver.propertyKey("elsewhere", "graph", IdGenerator.of(1), null); + + client.publish("HUGEGRAPH/hg/EVENT/GRAPH/REMOVE", "space-graph"); + Assert.assertNotSame(first, driver.propertyKey("space", "graph", IdGenerator.of(1), null)); + Assert.assertSame(other, driver.propertyKey("space", "other", IdGenerator.of(1), null)); + Assert.assertEquals(4, client.getCalls); + + client.publish("HUGEGRAPH/hg/EVENT/GRAPHSPACE/REMOVE", "space"); + Assert.assertNotSame(other, driver.propertyKey("space", "other", IdGenerator.of(1), null)); + Assert.assertSame(elsewhere, driver.propertyKey("elsewhere", "graph", IdGenerator.of(1), null)); + Assert.assertEquals(5, client.getCalls); + } finally { + SchemaDriver.destroy(); + } + } + + @Test + public void testMetadataKeysAndWatchSubscriptionsUseConfiguredCluster() { + CachingKvClient client = new CachingKvClient(); + SchemaDriver driver = new SchemaDriver(client, "alternate", 10, 60_000L); + this.instance.set(driver); + try { + Assert.assertEquals(4, client.listeners.size()); + for (String key : client.listeners.keySet()) { + Assert.assertTrue(key, key.startsWith("HUGEGRAPH/alternate/EVENT/")); + } + SchemaGraph graph = new SchemaGraph("DEFAULT", "hugegraph", PDConfig.of("127.0.0.1:8686"), + "alternate"); + Assert.assertEquals("HUGEGRAPH/alternate/GRAPHSPACE/DEFAULT/GRAPH_CONF/hugegraph", + client.lastGetKey); + graph.propertyKey(IdGenerator.of(1)); + Assert.assertEquals("HUGEGRAPH/alternate/GRAPHSPACE/DEFAULT/hugegraph/SCHEMA/PROPERTY_KEY/ID/1", + client.lastGetKey); + driver.propertyKeys("DEFAULT", "hugegraph", graph); + Assert.assertEquals("HUGEGRAPH/alternate/GRAPHSPACE/DEFAULT/hugegraph/SCHEMA/PROPERTY_KEY/NAME", + client.lastScanPrefix); + } finally { + SchemaDriver.destroy(); + } + } + + @Test + public void testStoreSupplierMapsRpcNamespaceToSchemaGraphAndConfiguredCluster() throws Exception { + CachingKvClient client = new CachingKvClient(); + this.instance.set(new SchemaDriver(client, "alternate", 10, 60_000L)); + HgStoreEngine engine = HgStoreEngine.getInstance(); + Field optionsField = HgStoreEngine.class.getDeclaredField("options"); + optionsField.setAccessible(true); + Object previousOptions = optionsField.get(engine); + boolean previousClosing = engine.isClosing().getAndSet(false); + Field cacheField = BusinessHandlerImpl.class.getDeclaredField("GRAPH_SUPPLIER_CACHE"); + cacheField.setAccessible(true); + @SuppressWarnings("unchecked") + Map cache = (Map) cacheField.get(null); + Map previousCache = new HashMap<>(cache); + HgStoreEngineOptions options = new HgStoreEngineOptions(); + options.setPdAddress("127.0.0.1:8686"); + options.setPdCluster("alternate"); + optionsField.set(engine, options); + cache.clear(); + try { + HugeGraphSupplier graph = BusinessHandlerImpl.getGraphSupplier("CODEC_TEST/row_graph/g"); + Assert.assertEquals("CODEC_TEST-row_graph", graph.name()); + Assert.assertEquals("HUGEGRAPH/alternate/GRAPHSPACE/CODEC_TEST/GRAPH_CONF/row_graph", + client.lastGetKey); + graph.propertyKey(IdGenerator.of(1)); + Assert.assertEquals("HUGEGRAPH/alternate/GRAPHSPACE/CODEC_TEST/row_graph/SCHEMA/PROPERTY_KEY/ID/1", + client.lastGetKey); + Assert.assertSame(graph, BusinessHandlerImpl.getGraphSupplier("CODEC_TEST/row_graph/g")); + for (String invalid : new String[]{"plain", "/row_graph/g", "CODEC_TEST//g"}) { + try { + BusinessHandlerImpl.getGraphSupplier(invalid); + Assert.fail("Invalid graph namespace must be rejected: " + invalid); + } catch (HgStoreException expected) { + Assert.assertTrue(expected.getMessage().contains("Graph must include")); + } + } + } finally { + BusinessHandlerImpl.closeSchemaResources(); + cache.putAll(previousCache); + optionsField.set(engine, previousOptions); + engine.isClosing().set(previousClosing); + } + } + + @Test + public void testSharedDriverRejectsConflictingClusterAndReusesMatchingCluster() { + CachingKvClient client = new CachingKvClient(); + SchemaDriver driver = new SchemaDriver(client, "alternate", 10, 60_000L); + this.instance.set(driver); + PDConfig config = PDConfig.of("127.0.0.1:8686"); + try { + Assert.assertSame(driver, SchemaDriver.getOrInit(config, "alternate")); + try { + SchemaDriver.getOrInit(config, "hg"); + Assert.fail("Conflicting metadata cluster must fail before reusing the driver"); + } catch (NotAllowException expected) { + Assert.assertTrue(expected.getMessage().contains("alternate")); + } + Assert.assertSame(driver, SchemaDriver.getInstance()); + } finally { + SchemaDriver.destroy(); + } + } + + @SuppressWarnings("unchecked") + private static AtomicReference instanceReference() throws Exception { + Field field = SchemaDriver.class.getDeclaredField("INSTANCE"); + field.setAccessible(true); + return (AtomicReference) field.get(null); + } + + private static class TrackingKvClient extends KvClient { + + protected volatile boolean closed; + + TrackingKvClient() { + super(PDConfig.of("127.0.0.1:8686")); + } + + @Override + public void listen(String key, Consumer consumer) throws PDException { + // Avoid opening a real PD stream while constructing SchemaDriver. + } + + @Override + public void close() { + this.closed = true; + super.close(); + } + } + + private static class CachingKvClient extends TrackingKvClient { + + private final Map> listeners = new HashMap<>(); + private int getCalls; + private String lastGetKey; + private String lastScanPrefix; + + @Override + public void listen(String key, Consumer consumer) { + this.listeners.put(key, consumer); + } + + @Override + public KResponse get(String key) { + this.getCalls++; + this.lastGetKey = key; + return KResponse.newBuilder().setValue("{\"id\":1,\"name\":\"name\"}").build(); + } + + @Override + public ScanPrefixResponse scanPrefix(String prefix) { + this.lastScanPrefix = prefix; + return ScanPrefixResponse.getDefaultInstance(); + } + + private void publish(String key, String graph) { + WatchEvent event = WatchEvent.newBuilder() + .setType(WatchType.Put) + .setCurrent(WatchKv.newBuilder().setValue(graph)) + .build(); + this.listeners.get(key).accept(WatchResponse.newBuilder().addEvents(event).build()); + } + } + + private static class BlockingCloseKvClient extends TrackingKvClient { + + private final CountDownLatch closeStarted = new CountDownLatch(1); + private final CountDownLatch allowClose = new CountDownLatch(1); + + @Override + public void close() { + this.closeStarted.countDown(); + try { + this.allowClose.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Client close was interrupted", e); + } + super.close(); + } + + private void awaitCloseStarted() throws InterruptedException { + Assert.assertTrue(this.closeStarted.await(5L, TimeUnit.SECONDS)); + } + } + + private static class FailingListenKvClient extends TrackingKvClient { + + private int listenCalls; + + @Override + public void listen(String key, Consumer consumer) throws PDException { + if (++this.listenCalls == 2) { + throw new PDException(-1, "listener startup failed"); + } + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ContextClosedListenerTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ContextClosedListenerTest.java new file mode 100644 index 0000000000..d640e56cfe --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ContextClosedListenerTest.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.hugegraph.store.service; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.hugegraph.store.node.grpc.GrpcShutdownBarrier; +import org.apache.hugegraph.store.node.grpc.HgStoreStreamImpl; +import org.apache.hugegraph.store.node.grpc.query.AggregativeQueryService; +import org.apache.hugegraph.store.node.listener.ContextClosedListener; +import org.apache.hugegraph.store.node.task.TTLCleaner; +import org.junit.Test; +import org.lognet.springboot.grpc.context.GRpcServerInitializedEvent; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import io.grpc.Server; + +public class ContextClosedListenerTest { + + + @Test(timeout = 5000) + public void testTtlCleanupBlocksBeanDestructionAfterWorkersTerminate() throws Exception { + CountDownLatch awaitingCleanup = new CountDownLatch(1); + CountDownLatch interruptedCleanup = new CountDownLatch(1); + CountDownLatch releaseCleanup = new CountDownLatch(1); + AtomicBoolean databaseClosed = new AtomicBoolean(); + TTLCleaner cleaner = mock(TTLCleaner.class); + doAnswer(invocation -> { + boolean interrupted = false; + awaitingCleanup.countDown(); + boolean released = false; + while (!released) { + try { + releaseCleanup.await(); + released = true; + } catch (InterruptedException e) { + interrupted = true; + interruptedCleanup.countDown(); + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + return null; + }).when(cleaner).awaitCleanup(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + try { + context.getBeanFactory().registerSingleton("storeStream", mock(HgStoreStreamImpl.class)); + context.getBeanFactory().registerSingleton("queryService", mock(AggregativeQueryService.class)); + context.getBeanFactory().registerSingleton("cleaner", cleaner); + context.getDefaultListableBeanFactory().registerDisposableBean("database", () -> databaseClosed.set(true)); + context.register(ContextClosedListener.class, GrpcShutdownBarrier.class); + context.refresh(); + FutureTask closing = new FutureTask<>(() -> { + context.close(); + return Thread.currentThread().isInterrupted(); + }); + Thread closeThread = new Thread(closing, "test-ttl-context-close"); + closeThread.setDaemon(true); + closeThread.start(); + assertTrue(awaitingCleanup.await(1, TimeUnit.SECONDS)); + closeThread.interrupt(); + assertTrue(interruptedCleanup.await(1, TimeUnit.SECONDS)); + assertFalse("TTL native cleanup must finish before DB destruction", databaseClosed.get()); + assertFalse(closing.isDone()); + releaseCleanup.countDown(); + assertTrue("shutdown must preserve interruption", closing.get(2, TimeUnit.SECONDS)); + assertTrue(databaseClosed.get()); + } finally { + releaseCleanup.countDown(); + context.close(); + } + } + +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/GraphPartitionScanLifecycleTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/GraphPartitionScanLifecycleTest.java new file mode 100644 index 0000000000..4ab3217d11 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/GraphPartitionScanLifecycleTest.java @@ -0,0 +1,561 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.store.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +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.AtomicReference; + +import org.apache.hugegraph.rocksdb.access.ScanIterator; +import org.apache.hugegraph.store.business.BusinessHandler; +import org.apache.hugegraph.store.business.GraphStoreIterator; +import org.apache.hugegraph.store.grpc.Graphpb; +import org.apache.hugegraph.store.grpc.Graphpb.ScanPartitionRequest; +import org.apache.hugegraph.store.grpc.Graphpb.ScanResponse; +import org.apache.hugegraph.store.node.grpc.HgStoreStreamImpl; +import org.apache.hugegraph.store.node.grpc.scan.ScanResponseObserver; +import org.junit.Test; + +import io.grpc.Context; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; + +public class GraphPartitionScanLifecycleTest extends GraphPartitionScanTestSupport { + + @Test + public void testNaturalEndAndLimitCloseIteratorBeforeCompleting() throws Exception { + for (int limit : new int[]{0, 1}) { + ThreadPoolExecutor executor = executor(); + try { + HgStoreStreamImpl service = service(executor); + GraphStoreIterator iterator = mock(GraphStoreIterator.class); + AtomicInteger consumed = new AtomicInteger(); + when(iterator.hasNext()).thenAnswer(call -> consumed.get() < 2); + when(iterator.next()).thenAnswer(call -> { + consumed.incrementAndGet(); + return Graphpb.Vertex.getDefaultInstance(); + }); + BusinessHandler handler = mock(BusinessHandler.class); + doReturn(iterator).when(handler).scan(any()); + StreamObserver response = mock(StreamObserver.class); + AtomicInteger delivered = new AtomicInteger(); + doAnswer(call -> { + delivered.addAndGet(((ScanResponse) call.getArgument(0)).getVertexCount()); + return null; + }).when(response).onNext(any()); + StreamObserver request = service.scanGraphPartition(response, handler); + request.onNext(request(limit)); + verify(response, timeout(2000)).onCompleted(); + service.awaitScanCleanup(); + verify(iterator).close(); + assertEquals(limit == 0 ? 2 : 1, consumed.get()); + assertEquals(consumed.get(), delivered.get()); + assertTrue(registry(service).isEmpty()); + request.onError(new IllegalStateException("late cancellation")); + verify(iterator).close(); + } finally { + executor.shutdown(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + } + } + } + + @Test(timeout = 30000) + public void testFinalBatchPublishedAfterEmptyPollIsDelivered() throws Exception { + ThreadPoolExecutor executor = executor(); + CountDownLatch finalRead = new CountDownLatch(1); + CountDownLatch releaseRead = new CountDownLatch(1); + CountDownLatch emptyPoll = new CountDownLatch(1); + CountDownLatch releasePoll = new CountDownLatch(1); + AtomicInteger consumed = new AtomicInteger(); + GraphStoreIterator iterator = mock(GraphStoreIterator.class); + when(iterator.hasNext()).thenAnswer(call -> consumed.get() < 100001); + when(iterator.next()).thenAnswer(call -> { + if (consumed.get() == 100000) { + finalRead.countDown(); + assertTrue(releaseRead.await(10, TimeUnit.SECONDS)); + } + consumed.incrementAndGet(); + return Graphpb.Vertex.getDefaultInstance(); + }); + BusinessHandler handler = mock(BusinessHandler.class); + doReturn(iterator).when(handler).scan(any()); + StreamObserver response = mock(StreamObserver.class); + AtomicInteger delivered = new AtomicInteger(); + doAnswer(call -> { + delivered.addAndGet(((ScanResponse) call.getArgument(0)).getVertexCount()); + return null; + }).when(response).onNext(any()); + ScanResponseObserver request = + new ScanResponseObserver<>(response, handler, executor); + LinkedBlockingQueue packages = new LinkedBlockingQueue(16) { + @Override + public ScanResponse poll(long timeout, TimeUnit unit) throws InterruptedException { + ScanResponse value = super.poll(timeout, unit); + if (value == null && emptyPoll.getCount() != 0) { + emptyPoll.countDown(); + assertTrue(releasePoll.await(10, TimeUnit.SECONDS)); + } + return value; + } + }; + Field field = ScanResponseObserver.class.getDeclaredField("packages"); + field.setAccessible(true); + field.set(request, packages); + try { + request.onNext(request(0)); + assertTrue(finalRead.await(10, TimeUnit.SECONDS)); + assertTrue("sender must have actually polled an empty queue", + emptyPoll.await(10, TimeUnit.SECONDS)); + assertEquals(100000, delivered.get()); + releaseRead.countDown(); + // The sender occupies one worker; this probe runs only after the real reader + // has published its final batch, closed its iterator and set readOver. + executor.submit(() -> { }).get(10, TimeUnit.SECONDS); + verify(iterator).close(); + assertEquals(1, packages.size()); + releasePoll.countDown(); + verify(response, timeout(5000)).onCompleted(); + assertEquals(100001, consumed.get()); + assertEquals(consumed.get(), delivered.get()); + assertTrue(packages.isEmpty()); + request.onCompleted(); + request.onError(new IllegalStateException("late cancellation")); + verify(response).onCompleted(); + verify(response, org.mockito.Mockito.never()).onError(any()); + verify(iterator).close(); + } finally { + releaseRead.countDown(); + releasePoll.countDown(); + executor.shutdown(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + + @Test + public void testEmptyScanClosesOnceAndReleasesBarrier() throws Exception { + ThreadPoolExecutor executor = executor(); + try { + HgStoreStreamImpl service = service(executor); + GraphStoreIterator iterator = mock(GraphStoreIterator.class); + BusinessHandler handler = mock(BusinessHandler.class); + doReturn(iterator).when(handler).scan(any()); + StreamObserver response = mock(StreamObserver.class); + service.scanGraphPartition(response, handler).onNext(request(0)); + verify(response, timeout(2000)).onCompleted(); + service.awaitScanCleanup(); + verify(iterator).close(); + assertTrue(registry(service).isEmpty()); + } finally { + executor.shutdown(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + } + } + + @Test + public void testRejectedWorkerClosesIteratorAndTerminatesResponse() throws Exception { + ThreadPoolExecutor executor = executor(); + executor.shutdown(); + HgStoreStreamImpl service = service(executor); + GraphStoreIterator iterator = mock(GraphStoreIterator.class); + BusinessHandler handler = mock(BusinessHandler.class); + doReturn(iterator).when(handler).scan(any()); + StreamObserver response = mock(StreamObserver.class); + service.scanGraphPartition(response, handler).onNext(request(0)); + verify(response).onError(any()); + verify(iterator).close(); + assertTrue(registry(service).isEmpty()); + } + + @Test + public void testReadFailureClosesIteratorAndPreservesPrimaryError() throws Exception { + ThreadPoolExecutor executor = executor(); + try { + HgStoreStreamImpl service = service(executor); + GraphStoreIterator iterator = mock(GraphStoreIterator.class); + RuntimeException failure = new IllegalStateException("native read failed"); + when(iterator.hasNext()).thenThrow(failure); + BusinessHandler handler = mock(BusinessHandler.class); + doReturn(iterator).when(handler).scan(any()); + StreamObserver response = mock(StreamObserver.class); + service.scanGraphPartition(response, handler).onNext(request(0)); + verify(response, timeout(2000)).onError(failure); + service.awaitScanCleanup(); + verify(iterator).close(); + } finally { + executor.shutdown(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + } + } + + @Test + public void testClientCompletionBeforeInitializationReleasesBarrier() throws Exception { + ThreadPoolExecutor executor = executor(); + try { + HgStoreStreamImpl service = service(executor); + StreamObserver response = mock(StreamObserver.class); + StreamObserver request = + service.scanGraphPartition(response, mock(BusinessHandler.class)); + request.onCompleted(); + request.onCompleted(); + verify(response).onCompleted(); + assertTrue(registry(service).isEmpty()); + } finally { + executor.shutdown(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + } + } + + @Test + public void testReadFailureKeepsCloseFailureSuppressedAndSticky() throws Exception { + ThreadPoolExecutor executor = executor(); + try { + HgStoreStreamImpl service = service(executor); + GraphStoreIterator iterator = mock(GraphStoreIterator.class); + RuntimeException primary = new IllegalStateException("read"); + RuntimeException cleanup = new IllegalArgumentException("close"); + when(iterator.hasNext()).thenThrow(primary); + doThrow(cleanup).when(iterator).close(); + BusinessHandler handler = mock(BusinessHandler.class); + doReturn(iterator).when(handler).scan(any()); + StreamObserver response = mock(StreamObserver.class); + service.scanGraphPartition(response, handler).onNext(request(0)); + verify(response, timeout(2000)).onError(primary); + assertEquals(1, primary.getSuppressed().length); + assertSame(cleanup, primary.getSuppressed()[0]); + verify(iterator).close(); + assertEquals(1, registry(service).size()); + } finally { + executor.shutdown(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + } + } + + @Test + public void testFullCreditWindowResumesWithOrderedPagesAndKeepsFinalPartialBatch() throws Exception { + checkCreditWindow(false, 7); + } + + @Test(timeout = 5000) + public void testHalfCloseBeforeWorkerStartsKeepsFinalPartialBatch() throws Exception { + ThreadPoolExecutor executor = executor(); + CountDownLatch workersBlocked = new CountDownLatch(2); + CountDownLatch release = new CountDownLatch(1); + List> blockers = new ArrayList<>(); + try { + // Finish service and mock initialization before establishing the worker gate. + HgStoreStreamImpl service = service(executor); + GraphStoreIterator iterator = mock(GraphStoreIterator.class); + AtomicInteger consumed = new AtomicInteger(); + AtomicInteger reads = new AtomicInteger(); + when(iterator.hasNext()).thenAnswer(call -> { + reads.incrementAndGet(); + return consumed.get() < 2; + }); + when(iterator.next()).thenAnswer(call -> { + consumed.incrementAndGet(); + return Graphpb.Vertex.getDefaultInstance(); + }); + BusinessHandler handler = mock(BusinessHandler.class); + doReturn(iterator).when(handler).scan(any()); + StreamObserver response = mock(StreamObserver.class); + AtomicInteger delivered = new AtomicInteger(); + doAnswer(call -> { + delivered.addAndGet(((ScanResponse) call.getArgument(0)).getVertexCount()); + return null; + }).when(response).onNext(any()); + StreamObserver request = service.scanGraphPartition(response, handler); + for (int i = 0; i < 2; i++) { + blockers.add(executor.submit(() -> { + workersBlocked.countDown(); + try { + release.await(); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(failure); + } + })); + } + assertTrue(workersBlocked.await(2, TimeUnit.SECONDS)); + request.onNext(request(0)); + assertEquals("reading must not begin before half-close", 0, reads.get()); + request.onCompleted(); + assertEquals("queued reader must remain gated through half-close", 0, reads.get()); + release.countDown(); + for (Future blocker : blockers) { + blocker.get(2, TimeUnit.SECONDS); + } + verify(response, timeout(2000)).onCompleted(); + service.awaitScanCleanup(); + assertEquals(2, delivered.get()); + verify(iterator).close(); + request.onCompleted(); + verify(response).onCompleted(); + assertTrue(registry(service).isEmpty()); + } finally { + release.countDown(); + executor.shutdown(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + } + } + + @Test(timeout = 5000) + public void testContextCancellationInterruptsBlockedReaderBeforeCleanup() throws Exception { + checkBlockedReaderCancellation(false); + } + + @Test(timeout = 5000) + public void testContextDeadlineInterruptsBlockedReaderBeforeCleanup() throws Exception { + checkBlockedReaderCancellation(true); + } + + private static void checkBlockedReaderCancellation(boolean deadline) throws Exception { + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>()); + ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor(); + HgStoreStreamImpl service = service(executor); + GraphStoreIterator iterator = mock(GraphStoreIterator.class); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch readExited = new CountDownLatch(1); + AtomicBoolean interrupted = new AtomicBoolean(); + when(iterator.hasNext()).thenAnswer(call -> { + reading.countDown(); + try { + new CountDownLatch(1).await(); + throw new AssertionError("blocked reader must exit through interruption"); + } catch (InterruptedException expected) { + interrupted.set(true); + Thread.currentThread().interrupt(); + return false; + } finally { + readExited.countDown(); + } + }); + AtomicBoolean closedAfterRead = new AtomicBoolean(); + doAnswer(call -> { + closedAfterRead.set(readExited.getCount() == 0); + return null; + }).when(iterator).close(); + BusinessHandler handler = mock(BusinessHandler.class); + doReturn(iterator).when(handler).scan(any()); + StreamObserver response = mock(StreamObserver.class); + Context.CancellableContext context = deadline ? + Context.current().withDeadlineAfter(1, TimeUnit.SECONDS, timer) : + Context.current().withCancellation(); + Thread cancellation = null; + try { + StreamObserver request = context.call(() -> + service.scanGraphPartition(response, handler)); + request.onNext(request(0)); + assertTrue("real worker must enter its interruptible read", reading.await(500, TimeUnit.MILLISECONDS)); + if (!deadline) { + FutureTask cancel = new FutureTask<>(() -> { + context.cancel(null); + return null; + }); + cancellation = new Thread(cancel, "test-partition-context-cancel"); + cancellation.setDaemon(true); + cancellation.start(); + cancel.get(2, TimeUnit.SECONDS); + } + assertTrue("context cancellation must interrupt the owned reader", readExited.await(2, TimeUnit.SECONDS)); + assertTrue(interrupted.get()); + verify(response, timeout(2000)).onError(any()); + service.awaitScanCleanup(); + verify(iterator).close(); + assertTrue(closedAfterRead.get()); + assertTrue(registry(service).isEmpty()); + // A released worker must remain usable and free from a cancellation interrupt. + assertFalse(executor.submit(() -> Thread.currentThread().isInterrupted()).get(1, TimeUnit.SECONDS)); + } finally { + // Also release an old implementation's blocked reader when this regression fails. + executor.shutdownNow(); + context.cancel(null); + timer.shutdownNow(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + if (cancellation != null) { + cancellation.join(1000); + } + } + } + + @Test + public void testHalfCloseDeliversEntirePermittedWindowAndCompletes() throws Exception { + checkCreditWindow(true, 0); + } + + @Test + public void testHalfCloseReportsInsufficientCreditAfterDeliveringPermittedPages() throws Exception { + checkCreditWindow(true, 7); + } + + private void checkCreditWindow(boolean halfClose, int extraRows) throws Exception { + ThreadPoolExecutor executor = executor(); + HgStoreStreamImpl service = service(executor); + final int batchSize = 100000; + final int window = 8; + final int total = batchSize * window + extraRows; + AtomicInteger closes = new AtomicInteger(); + Field suppliers = org.apache.hugegraph.store.business.BusinessHandlerImpl.class + .getDeclaredField("GRAPH_SUPPLIER_CACHE"); + suppliers.setAccessible(true); + @SuppressWarnings("unchecked") + Map graphCache = + (Map) suppliers.get(null); + String graphName = "TEST/credit-window"; + org.apache.hugegraph.HugeGraphSupplier previous = + graphCache.put(graphName, mock(org.apache.hugegraph.HugeGraphSupplier.class)); + GraphStoreIterator iterator; + try { + iterator = new GraphStoreIterator( + mock(ScanIterator.class), request(0)) { + private int row; + + @Override + public boolean hasNext() { + return this.row < total; + } + + @Override + public Graphpb.Vertex next() { + return Graphpb.Vertex.newBuilder().setId(Graphpb.Variant.newBuilder() + .setType(Graphpb.VariantType.VT_LONG).setValueInt64(this.row++)).build(); + } + + @Override + public void close() { + closes.incrementAndGet(); + } + }; + } finally { + if (previous == null) { + graphCache.remove(graphName); + } else { + graphCache.put(graphName, previous); + } + } + LinkedBlockingQueue responses = new LinkedBlockingQueue<>(); + AtomicInteger terminals = new AtomicInteger(); + AtomicReference error = new AtomicReference<>(); + CountDownLatch finished = new CountDownLatch(1); + StreamObserver response = new StreamObserver() { + @Override + public void onNext(ScanResponse value) { + responses.add(value); + } + + @Override + public void onError(Throwable failure) { + error.set(failure); + terminals.incrementAndGet(); + finished.countDown(); + } + + @Override + public void onCompleted() { + terminals.incrementAndGet(); + finished.countDown(); + } + }; + try { + BusinessHandler handler = mock(BusinessHandler.class); + doReturn(iterator).when(handler).scan(any()); + StreamObserver request = service.scanGraphPartition(response, handler); + request.onNext(request(0)); + if (halfClose) { + request.onCompleted(); + } + long expectedId = 0; + for (int page = 0; page < window; page++) { + ScanResponse value = responses.poll(20, TimeUnit.SECONDS); + assertTrue("Expected page " + page, value != null); + assertEquals(page, value.getSeqNo()); + assertEquals(batchSize, value.getVertexCount()); + for (Graphpb.Vertex vertex : value.getVertexList()) { + assertEquals(expectedId++, vertex.getId().getValueInt64()); + } + } + assertNull("No ninth page is allowed without credit", responses.poll(100, TimeUnit.MILLISECONDS)); + if (halfClose) { + assertTrue(finished.await(20, TimeUnit.SECONDS)); + if (extraRows == 0) { + assertNull(error.get()); + } else { + assertEquals(Status.Code.FAILED_PRECONDITION, Status.fromThrowable(error.get()).getCode()); + } + service.awaitScanCleanup(); + request.onCompleted(); + assertEquals(1, terminals.get()); + assertEquals(1, closes.get()); + assertTrue(registry(service).isEmpty()); + return; + } + assertEquals(0, terminals.get()); + for (int page = 0; page < window; page++) { + request.onNext(ScanPartitionRequest.newBuilder().setReplyRequest( + ScanPartitionRequest.Reply.newBuilder().setSeqNo(page)).build()); + } + ScanResponse last = responses.poll(20, TimeUnit.SECONDS); + assertTrue("The last partial batch must be delivered", last != null); + assertEquals(window, last.getSeqNo()); + assertEquals(7, last.getVertexCount()); + for (Graphpb.Vertex vertex : last.getVertexList()) { + assertEquals(expectedId++, vertex.getId().getValueInt64()); + } + assertEquals(total, expectedId); + assertTrue(finished.await(20, TimeUnit.SECONDS)); + assertNull(error.get()); + service.awaitScanCleanup(); + request.onCompleted(); + assertEquals(1, terminals.get()); + assertEquals(1, closes.get()); + assertTrue(responses.isEmpty()); + assertTrue(registry(service).isEmpty()); + } finally { + service.shutdownScans(); + executor.shutdown(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/GraphPartitionScanShutdownTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/GraphPartitionScanShutdownTest.java new file mode 100644 index 0000000000..076a981e58 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/GraphPartitionScanShutdownTest.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.hugegraph.store.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.apache.hugegraph.store.business.BusinessHandler; +import org.apache.hugegraph.store.business.GraphStoreIterator; +import org.apache.hugegraph.store.grpc.Graphpb.ScanResponse; +import org.apache.hugegraph.store.node.grpc.HgStoreStreamImpl; +import org.junit.Test; + +import io.grpc.stub.StreamObserver; + +public class GraphPartitionScanShutdownTest extends GraphPartitionScanTestSupport { + + @Test + public void testFailedNativeCloseRemainsInShutdownBarrier() throws Exception { + ThreadPoolExecutor executor = executor(); + try { + HgStoreStreamImpl service = service(executor); + GraphStoreIterator iterator = mock(GraphStoreIterator.class); + RuntimeException failure = new IllegalStateException("native close failed"); + doThrow(failure).when(iterator).close(); + BusinessHandler handler = mock(BusinessHandler.class); + doReturn(iterator).when(handler).scan(any()); + StreamObserver response = mock(StreamObserver.class); + service.scanGraphPartition(response, handler).onNext(request(0)); + verify(response, timeout(2000)).onError(failure); + assertEquals(1, registry(service).size()); + Object lifecycle = registry(service).keySet().iterator().next(); + java.lang.reflect.Method cleanup = lifecycle.getClass().getDeclaredMethod("cleanupFailure"); + cleanup.setAccessible(true); + assertSame(failure, cleanup.invoke(lifecycle)); + verify(iterator).close(); + } finally { + executor.shutdown(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + } + } + + @Test + public void testShutdownWaitsForActiveReadBeforeClosingNativeIterator() throws Exception { + ThreadPoolExecutor executor = executor(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + CountDownLatch stopped = new CountDownLatch(1); + CountDownLatch stoppingStarted = new CountDownLatch(1); + try { + HgStoreStreamImpl service = service(executor); + GraphStoreIterator iterator = mock(GraphStoreIterator.class); + when(iterator.hasNext()).thenAnswer(call -> { + entered.countDown(); + boolean interrupted = false; + try { + // Model native work that cannot exit until the read itself finishes. + while (release.getCount() != 0) { + try { + release.await(); + } catch (InterruptedException cancellation) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + return false; + }); + BusinessHandler handler = mock(BusinessHandler.class); + doReturn(iterator).when(handler).scan(any()); + StreamObserver response = mock(StreamObserver.class); + service.scanGraphPartition(response, handler).onNext(request(0)); + assertTrue(entered.await(2, TimeUnit.SECONDS)); + Thread stopping = new Thread(() -> { + service.stopAcceptingScans(); + stoppingStarted.countDown(); + service.shutdownScans(); + service.awaitScanCleanup(); + stopped.countDown(); + }); + stopping.start(); + assertTrue(stoppingStarted.await(2, TimeUnit.SECONDS)); + assertFalse(stopped.await(100, TimeUnit.MILLISECONDS)); + verify(iterator, never()).close(); + release.countDown(); + assertTrue(stopped.await(2, TimeUnit.SECONDS)); + stopping.join(2000); + verify(iterator).close(); + verify(response).onError(any()); + assertTrue(registry(service).isEmpty()); + } finally { + release.countDown(); + executor.shutdown(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/GraphPartitionScanTestSupport.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/GraphPartitionScanTestSupport.java new file mode 100644 index 0000000000..1742ac64c5 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/GraphPartitionScanTestSupport.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.hugegraph.store.service; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.apache.hugegraph.store.grpc.Graphpb; +import org.apache.hugegraph.store.grpc.Graphpb.ScanPartitionRequest; +import org.apache.hugegraph.store.node.grpc.HgStoreStreamImpl; + +abstract class GraphPartitionScanTestSupport { + + static ScanPartitionRequest request(long limit) { + return ScanPartitionRequest.newBuilder().setScanRequest(Graphpb.ScanPartitionRequest.Request.newBuilder() + .setGraphName("TEST/credit-window") + .setScanType(Graphpb.ScanPartitionRequest.ScanType.SCAN_VERTEX).setLimit(limit)).build(); + } + + static ThreadPoolExecutor executor() { + return new ThreadPoolExecutor(2, 2, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); + } + + static HgStoreStreamImpl service(ThreadPoolExecutor executor) throws Exception { + HgStoreStreamImpl service = new HgStoreStreamImpl(); + Field field = HgStoreStreamImpl.class.getDeclaredField("executor"); + field.setAccessible(true); + field.set(service, executor); + return service; + } + + static Map registry(HgStoreStreamImpl service) throws Exception { + Field field = HgStoreStreamImpl.class.getDeclaredField("scans"); + field.setAccessible(true); + return (Map) field.get(service); + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/GrpcShutdownBarrierTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/GrpcShutdownBarrierTest.java new file mode 100644 index 0000000000..756244a6f9 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/GrpcShutdownBarrierTest.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. + */ + +package org.apache.hugegraph.store.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.hugegraph.store.node.AppConfig; +import org.apache.hugegraph.store.node.grpc.GRpcServerConfig; +import org.apache.hugegraph.store.node.grpc.GrpcShutdownBarrier; +import org.apache.hugegraph.store.node.util.HgExecutorUtil; +import org.junit.Test; + +import io.grpc.CallOptions; +import io.grpc.ClientCall; +import io.grpc.Metadata; +import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor; +import io.grpc.Server; +import io.grpc.ServerCall; +import io.grpc.ServerServiceDefinition; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.stub.ClientCalls; +import io.grpc.stub.ServerCalls; +import io.grpc.stub.StreamObserver; + +public class GrpcShutdownBarrierTest { + + @Test(timeout = 10000) + public void testNettyCancellationSurvivesSaturatedSingleDispatchThread() throws Exception { + CountDownLatch callsStarted = new CountDownLatch(2); + CountDownLatch callbackStarted = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + CountDownLatch cancelled = new CountDownLatch(2); + MethodDescriptor.Marshaller marshaller = new MethodDescriptor.Marshaller() { + @Override + public InputStream stream(byte[] value) { + return new ByteArrayInputStream(value); + } + + @Override + public byte[] parse(InputStream stream) { + return new byte[0]; + } + }; + MethodDescriptor method = MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.BIDI_STREAMING).setFullMethodName("saturation/hold") + .setRequestMarshaller(marshaller).setResponseMarshaller(marshaller).build(); + GrpcShutdownBarrier barrier = new GrpcShutdownBarrier(); + NettyServerBuilder builder = NettyServerBuilder.forPort(0).intercept(barrier) + .addService(ServerServiceDefinition.builder("saturation").addMethod(method, (call, headers) -> { + callsStarted.countDown(); + call.request(1); + return new ServerCall.Listener() { + @Override + public void onMessage(byte[] message) { + callbackStarted.countDown(); + try { + release.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public void onCancel() { + cancelled.countDown(); + } + }; + }).build()); + AppConfig config = mock(AppConfig.class); + AppConfig.ThreadPoolGrpc grpc = mock(AppConfig.ThreadPoolGrpc.class); + when(config.getThreadPoolGrpc()).thenReturn(grpc); + when(grpc.getCore()).thenReturn(1); + when(grpc.getMax()).thenReturn(1); + when(grpc.getQueue()).thenReturn(0); + GRpcServerConfig serverConfig = new GRpcServerConfig(); + Field appConfig = GRpcServerConfig.class.getDeclaredField("appConfig"); + appConfig.setAccessible(true); + appConfig.set(serverConfig, config); + // Isolate the production named-executor cache for this fixture only. + Field poolsField = HgExecutorUtil.class.getDeclaredField("threadPoolMap"); + poolsField.setAccessible(true); + @SuppressWarnings("unchecked") + Map pools = (Map) poolsField.get(null); + ThreadPoolExecutor previous = pools.remove(GRpcServerConfig.EXECUTOR_NAME); + ThreadPoolExecutor executor = null; + Server server = null; + ManagedChannel channel = null; + try { + serverConfig.configure(builder); + executor = HgExecutorUtil.getThreadPoolExecutor(GRpcServerConfig.EXECUTOR_NAME); + assertEquals(1, executor.getCorePoolSize()); + assertEquals(1, executor.getMaximumPoolSize()); + server = builder.build().start(); + channel = NettyChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build(); + ClientCall first = channel.newCall(method, CallOptions.DEFAULT); + ClientCall second = channel.newCall(method, CallOptions.DEFAULT); + first.start(new ClientCall.Listener() { }, new Metadata()); + second.start(new ClientCall.Listener() { }, new Metadata()); + first.request(1); + second.request(1); + assertTrue("both calls must register before saturation", callsStarted.await(2, TimeUnit.SECONDS)); + first.sendMessage(new byte[0]); + assertTrue(callbackStarted.await(2, TimeUnit.SECONDS)); + second.cancel("cancel idle call while dispatch is saturated", null); + first.cancel("cancel busy call", null); + long queuedDeadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); + while (executor.getQueue().isEmpty() && System.nanoTime() < queuedDeadline) { + Thread.yield(); + } + assertFalse("idle call's real cancellation callback must queue behind the busy handler", + executor.getQueue().isEmpty()); + CountDownLatch waiting = new CountDownLatch(1); + FutureTask drained = new FutureTask<>(() -> { + waiting.countDown(); + barrier.awaitCallbacks(); + return null; + }); + Thread waiter = new Thread(drained, "test-netty-saturation-drain"); + waiter.setDaemon(true); + waiter.start(); + assertTrue(waiting.await(1, TimeUnit.SECONDS)); + try { + drained.get(100, TimeUnit.MILLISECONDS); + fail("active callback must still block database destruction"); + } catch (TimeoutException expected) { + // Cancelling the transport cannot discard the application's terminal callback. + } + release.countDown(); + assertTrue("both cancellation callbacks must be delivered", cancelled.await(2, TimeUnit.SECONDS)); + drained.get(2, TimeUnit.SECONDS); + executor.submit(() -> { }).get(1, TimeUnit.SECONDS); + barrier.stopAcceptingCalls(); + server.shutdownNow(); + assertTrue(server.awaitTermination(2, TimeUnit.SECONDS)); + barrier.awaitCallbacks(); + } finally { + release.countDown(); + if (channel != null) { + channel.shutdownNow(); + channel.awaitTermination(2, TimeUnit.SECONDS); + } + if (server != null) { + server.shutdownNow(); + server.awaitTermination(2, TimeUnit.SECONDS); + } + if (executor != null) { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + pools.remove(GRpcServerConfig.EXECUTOR_NAME, executor); + } + if (previous != null) { + pools.put(GRpcServerConfig.EXECUTOR_NAME, previous); + } + } + } + + @Test(timeout = 7000) + public void testTransportTerminationDoesNotReleaseActiveCallback() throws Exception { + CountDownLatch started = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicBoolean finished = new AtomicBoolean(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + MethodDescriptor.Marshaller marshaller = new MethodDescriptor.Marshaller() { + @Override + public InputStream stream(byte[] value) { + return new ByteArrayInputStream(value); + } + @Override + public byte[] parse(InputStream stream) { + return new byte[0]; + } + }; + MethodDescriptor method = MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY).setFullMethodName("probe/hold") + .setRequestMarshaller(marshaller).setResponseMarshaller(marshaller).build(); + String name = InProcessServerBuilder.generateName(); + GrpcShutdownBarrier barrier = new GrpcShutdownBarrier(); + Server server = InProcessServerBuilder.forName(name).executor(executor).intercept(barrier) + .addService(ServerServiceDefinition.builder("probe").addMethod(method, + ServerCalls.asyncUnaryCall((request, response) -> { + started.countDown(); + try { + release.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + finished.set(true); + } + response.onNext(new byte[0]); + response.onCompleted(); + })).build()).build().start(); + ManagedChannel channel = InProcessChannelBuilder.forName(name).directExecutor().build(); + try { + ClientCalls.asyncUnaryCall(channel.newCall(method, CallOptions.DEFAULT), new byte[0], + new StreamObserver() { + @Override + public void onNext(byte[] value) { + } + @Override + public void onError(Throwable error) { + } + @Override + public void onCompleted() { + } + }); + assertTrue("handler must start", started.await(2, TimeUnit.SECONDS)); + barrier.stopAcceptingCalls(); + server.shutdownNow(); + assertTrue(server.awaitTermination(1, TimeUnit.SECONDS)); + assertFalse("transport termination must not be mistaken for callback exit", finished.get()); + CountDownLatch waiting = new CountDownLatch(1); + FutureTask drained = new FutureTask<>(() -> { + waiting.countDown(); + barrier.awaitCallbacks(); + return null; + }); + Thread waiter = new Thread(drained, "test-rpc-drain"); + waiter.setDaemon(true); + waiter.start(); + assertTrue(waiting.await(1, TimeUnit.SECONDS)); + try { + drained.get(100, TimeUnit.MILLISECONDS); + fail("barrier passed while a handler still owns resources"); + } catch (TimeoutException expected) { + // The active handler must prevent database destruction. + } + release.countDown(); + drained.get(2, TimeUnit.SECONDS); + assertTrue(finished.get()); + } finally { + release.countDown(); + channel.shutdownNow(); + server.shutdownNow(); + executor.shutdown(); + assertTrue("cleanup must finish", executor.awaitTermination(2, TimeUnit.SECONDS)); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/PartitionEngineShutdownTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/PartitionEngineShutdownTest.java new file mode 100644 index 0000000000..599d64b682 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/PartitionEngineShutdownTest.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.hugegraph.store.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hugegraph.store.HgStoreEngine; +import org.apache.hugegraph.store.PartitionEngine; +import org.apache.hugegraph.store.meta.ShardGroup; +import org.junit.Test; + +import com.alipay.sofa.jraft.RaftGroupService; + +public class PartitionEngineShutdownTest { + + @Test(timeout = 5000) + public void testInterruptedJoinRetriesBeforeMarkingPartitionStopped() throws Exception { + PartitionEngine engine = new PartitionEngine(mock(HgStoreEngine.class), + mock(ShardGroup.class)) { + @Override + public String toString() { + return "test-partition"; + } + }; + Field started = PartitionEngine.class.getDeclaredField("started"); + started.setAccessible(true); + started.setBoolean(engine, true); + RaftGroupService raft = mock(RaftGroupService.class); + Field service = PartitionEngine.class.getDeclaredField("raftGroupService"); + service.setAccessible(true); + service.set(engine, raft); + AtomicInteger joins = new AtomicInteger(); + doAnswer(invocation -> { + assertTrue("partition must remain started until Raft terminates", + started.getBoolean(engine)); + verify(raft).shutdown(); + if (joins.incrementAndGet() == 1) { + assertTrue("model an interrupt restored by the shutdown listener", + Thread.interrupted()); + throw new InterruptedException("shutdown thread interrupted"); + } + assertFalse("join retry must clear interruption while draining Raft", + Thread.currentThread().isInterrupted()); + return null; + }).when(raft).join(); + try { + Thread.currentThread().interrupt(); + engine.shutdown(); + assertEquals(2, joins.get()); + verify(raft, times(2)).join(); + assertFalse(started.getBoolean(engine)); + assertTrue("shutdown must restore caller interruption after Raft termination", + Thread.currentThread().isInterrupted()); + } finally { + Thread.interrupted(); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ScanContextCleanupTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ScanContextCleanupTest.java new file mode 100644 index 0000000000..4e33faeaed --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ScanContextCleanupTest.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.hugegraph.store.service; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.hugegraph.store.node.grpc.HgStoreStreamImpl; +import org.apache.hugegraph.store.node.grpc.GrpcShutdownBarrier; +import org.apache.hugegraph.store.node.grpc.query.AggregativeQueryService; +import org.apache.hugegraph.store.node.listener.ContextClosedListener; +import org.apache.hugegraph.store.node.task.TTLCleaner; +import org.junit.Test; +import org.lognet.springboot.grpc.context.GRpcServerInitializedEvent; + +import io.grpc.Server; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + + +public class ScanContextCleanupTest { + + @Test(timeout = 5000) + public void testScopedWorkersFinishBeforeBeanDestruction() throws Exception { + ThreadPoolExecutor scan = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + ThreadPoolExecutor ttl = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + CountDownLatch started = new CountDownLatch(3); + CountDownLatch interrupted = new CountDownLatch(3); + CountDownLatch cleanupStarted = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + CountDownLatch cleanupAllowed = new CountDownLatch(1); + Runnable work = () -> { + started.countDown(); + try { + release.await(); + } catch (InterruptedException e) { + interrupted.countDown(); + cleanupStarted.countDown(); + boolean done = false; + while (!done) { + try { + cleanupAllowed.await(); + done = true; + } catch (InterruptedException ignored) { + // Model resource cleanup which must finish before DB destruction. + } + } + Thread.currentThread().interrupt(); + } + }; + AtomicBoolean databaseClosed = new AtomicBoolean(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + try { + HgStoreStreamImpl stream = mock(HgStoreStreamImpl.class); + TTLCleaner cleaner = mock(TTLCleaner.class); + when(stream.getRealExecutor()).thenReturn(scan); + doAnswer(invocation -> { + scan.shutdownNow(); + return null; + }).when(stream).shutdownScans(); + when(cleaner.getExecutor()).thenReturn(ttl); + when(cleaner.getScheduler()).thenReturn(scheduler); + context.getBeanFactory().registerSingleton("storeStream", stream); + context.getBeanFactory().registerSingleton("queryService", mock(AggregativeQueryService.class)); + context.getBeanFactory().registerSingleton("cleaner", cleaner); + context.getDefaultListableBeanFactory().registerDisposableBean( + "database", () -> databaseClosed.set(true)); + context.register(ContextClosedListener.class, GrpcShutdownBarrier.class); + context.refresh(); + scan.execute(work); + ttl.execute(work); + scheduler.execute(work); + assertTrue("all shutdown participants must be active", started.await(1, TimeUnit.SECONDS)); + + FutureTask closing = new FutureTask<>(() -> { + context.close(); + return null; + }); + Thread closeThread = new Thread(closing, "test-context-close"); + closeThread.setDaemon(true); + closeThread.start(); + // Scheduler shutdown must not let DB destruction overtake resource cleanup. + assertTrue(cleanupStarted.await(1, TimeUnit.SECONDS)); + assertFalse("context close must wait for worker cleanup", closing.isDone()); + assertFalse("database must remain open during worker cleanup", databaseClosed.get()); + cleanupAllowed.countDown(); + closing.get(2, TimeUnit.SECONDS); + assertTrue("close must interrupt active workers", interrupted.await(1, TimeUnit.SECONDS)); + assertTrue(scan.awaitTermination(1, TimeUnit.SECONDS)); + assertTrue(ttl.awaitTermination(1, TimeUnit.SECONDS)); + assertTrue(scheduler.awaitTermination(1, TimeUnit.SECONDS)); + assertTrue(databaseClosed.get()); + } finally { + cleanupAllowed.countDown(); + release.countDown(); + scan.shutdownNow(); + ttl.shutdownNow(); + scheduler.shutdownNow(); + context.close(); + } + } + + @Test(timeout = 5000) + public void testGrpcCallbacksFinishBeforeBeanDestructionEvenWhenInterrupted() throws Exception { + CountDownLatch awaitingCallbacks = new CountDownLatch(1); + CountDownLatch releaseCallbacks = new CountDownLatch(1); + AtomicBoolean terminated = new AtomicBoolean(); + AtomicBoolean databaseClosed = new AtomicBoolean(); + Server server = mock(Server.class); + when(server.shutdownNow()).thenReturn(server); + when(server.isTerminated()).thenAnswer(invocation -> terminated.get()); + when(server.awaitTermination(5, TimeUnit.SECONDS)).thenAnswer(invocation -> { + awaitingCallbacks.countDown(); + releaseCallbacks.await(); + terminated.set(true); + return true; + }); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + try { + context.getBeanFactory().registerSingleton("storeStream", mock(HgStoreStreamImpl.class)); + context.getBeanFactory().registerSingleton("queryService", mock(AggregativeQueryService.class)); + context.getBeanFactory().registerSingleton("cleaner", mock(TTLCleaner.class)); + context.getDefaultListableBeanFactory().registerDisposableBean("database", () -> databaseClosed.set(true)); + context.register(ContextClosedListener.class, GrpcShutdownBarrier.class); + context.refresh(); + context.publishEvent(new GRpcServerInitializedEvent(context, server)); + FutureTask closing = new FutureTask<>(() -> { + context.close(); + return Thread.currentThread().isInterrupted(); + }); + Thread closeThread = new Thread(closing, "test-grpc-context-close"); + closeThread.setDaemon(true); + closeThread.start(); + assertTrue(awaitingCallbacks.await(1, TimeUnit.SECONDS)); + closeThread.interrupt(); + assertFalse("DB must remain open while callbacks own its resources", databaseClosed.get()); + releaseCallbacks.countDown(); + assertTrue("shutdown must preserve interruption", closing.get(2, TimeUnit.SECONDS)); + assertTrue(terminated.get()); + assertTrue(databaseClosed.get()); + } finally { + releaseCallbacks.countDown(); + context.close(); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ScanLifecycleTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ScanLifecycleTest.java new file mode 100644 index 0000000000..d6deb27340 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ScanLifecycleTest.java @@ -0,0 +1,1916 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.store.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +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.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.SynchronousQueue; +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.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.hugegraph.pd.common.KVPair; +import org.apache.hugegraph.rocksdb.access.RocksDBSession; +import org.apache.hugegraph.rocksdb.access.ScanIterator; +import org.apache.hugegraph.store.grpc.common.Header; +import org.apache.hugegraph.store.grpc.common.ScanMethod; +import org.apache.hugegraph.store.grpc.common.ScanOrderType; +import org.apache.hugegraph.store.grpc.stream.HgStoreStreamGrpc; +import org.apache.hugegraph.store.grpc.stream.KvPageRes; +import org.apache.hugegraph.store.grpc.stream.KvStream; +import org.apache.hugegraph.store.grpc.stream.ScanQueryRequest; +import org.apache.hugegraph.store.grpc.stream.ScanStreamBatchReq; +import org.apache.hugegraph.store.grpc.stream.ScanStreamReq; +import org.apache.hugegraph.store.node.AppConfig; +import org.apache.hugegraph.store.node.grpc.HgStoreStreamImpl; +import org.apache.hugegraph.store.node.grpc.HgStoreWrapperEx; +import org.apache.hugegraph.store.node.grpc.ParallelScanIterator; +import org.apache.hugegraph.store.node.grpc.QueryCondition; +import org.apache.hugegraph.store.node.grpc.ScanBatchOneShotResponse; +import org.apache.hugegraph.store.node.grpc.ScanBatchResponse; +import org.apache.hugegraph.store.node.grpc.ScanBatchResponseFactory; +import org.apache.hugegraph.store.node.grpc.ScanBatchResponse3; +import org.apache.hugegraph.store.node.grpc.ScanOneShotResponse; +import org.apache.hugegraph.store.node.grpc.ScanStreamResponse; +import org.apache.hugegraph.store.node.util.HgChannel; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import io.grpc.Context; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; + +public class ScanLifecycleTest extends ScanTestSupport { + + @Test(timeout = 10000) + public void testBatchHalfCloseDoesNotBlockTransportCancellation() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + StreamObserver output = mock(StreamObserver.class); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch interrupted = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.hasNext()).thenAnswer(ignored -> { + reading.countDown(); + try { + assertTrue(release.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException expected) { + interrupted.countDown(); + Thread.currentThread().interrupt(); + } + return false; + }); + HgStoreStreamImpl service = scanService(executor, wrapper); + Context.CancellableContext context = Context.current().withCancellation(); + StreamObserver input = context.call(() -> service.scanBatch(output)); + FutureTask halfClosing = new FutureTask<>(() -> { + input.onCompleted(); + return null; + }); + FutureTask cancelling = new FutureTask<>(() -> { + context.cancel(null); + return null; + }); + Thread halfClose = new Thread(halfClosing, "batch-scan-half-close"); + Thread cancel = new Thread(cancelling, "batch-scan-context-cancel"); + try { + input.onNext(batchRequest()); + assertTrue(reading.await(1, TimeUnit.SECONDS)); + halfClose.start(); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + while (halfClose.getState() != Thread.State.BLOCKED && System.nanoTime() < deadline) { + Thread.yield(); + } + assertEquals(Thread.State.BLOCKED, halfClose.getState()); + assertTrue(Arrays.stream(halfClose.getStackTrace()).anyMatch(frame -> + frame.getClassName().endsWith("ScanBatchResponse3$OrderWorker") && + frame.getMethodName().equals("inputCompleted"))); + + cancel.start(); + assertTrue("transport cancellation must interrupt the blocked scan reader", + interrupted.await(1, TimeUnit.SECONDS)); + cancelling.get(1, TimeUnit.SECONDS); + halfClosing.get(1, TimeUnit.SECONDS); + executor.submit(() -> { }).get(1, TimeUnit.SECONDS); + verify(iterator).close(); + assertTrue("the cancelled scan must release its owner", scanRegistry(service).isEmpty()); + assertCancelled(output); + verify(output, never()).onNext(any(KvPageRes.class)); + verify(output, never()).onCompleted(); + } finally { + release.countDown(); + halfClose.join(2000); + cancel.join(2000); + context.cancel(null); + service.shutdownScans(); + executor.shutdownNow(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + assertFalse(halfClose.isAlive()); + assertFalse(cancel.isAlive()); + } + } + + @Test + public void testFailedSelectionCleanupBlocksLifecycleCompletion() throws Exception { + ScanIterator iterator = mock(ScanIterator.class); + IllegalStateException closing = new IllegalStateException("native release failed"); + doThrow(closing).when(iterator).close(); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + Class lifecycleType = Class.forName("org.apache.hugegraph.store.node.grpc.ScanLifecycle"); + java.lang.reflect.Constructor constructor = lifecycleType.getDeclaredConstructor(); + constructor.setAccessible(true); + Object lifecycle = constructor.newInstance(); + Method failedCleanup = lifecycleType.getDeclaredMethod("failedCleanup", Throwable.class); + failedCleanup.setAccessible(true); + AtomicBoolean finished = new AtomicBoolean(); + Method onFinished = lifecycleType.getDeclaredMethod("onFinished", Runnable.class); + onFinished.setAccessible(true); + onFinished.invoke(lifecycle, (Runnable) () -> finished.set(true)); + java.util.function.Consumer cleanup = failure -> { + try { + failedCleanup.invoke(lifecycle, failure); + } catch (ReflectiveOperationException error) { + throw new AssertionError(error); + } + }; + Class selector = Class.forName("org.apache.hugegraph.store.node.grpc.ScanUtil"); + Method getIterator = selector.getDeclaredMethod("getIterator", ScanStreamReq.class, + HgStoreWrapperEx.class, + java.util.function.Consumer.class); + getIterator.setAccessible(true); + ScanStreamReq request = ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setHeader(Header.newBuilder().setGraph("selection-cleanup")) + .setTable("unsupported-property-table") + .setSelects(org.apache.hugegraph.store.grpc.stream.SelectParam.newBuilder().addProperties(1)).build(); + try { + getIterator.invoke(null, request, wrapper, cleanup); + fail("Invalid property selection must fail"); + } catch (java.lang.reflect.InvocationTargetException error) { + assertTrue(error.getCause() instanceof IllegalArgumentException); + assertEquals(Collections.singletonList(closing), Arrays.asList(error.getCause().getSuppressed())); + } + Method finish = lifecycleType.getDeclaredMethod("finishWithoutResponse"); + finish.setAccessible(true); + finish.invoke(lifecycle); + assertFalse("Failed native release must remain a shutdown blocker", finished.get()); + verify(iterator).close(); + } + + @Test(timeout = 5000) + public void testInFlightReceiptDoesNotPublishOverAfterErrorCancellation() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + StreamObserver output = mock(StreamObserver.class); + ScanBatchResponse response = new ScanBatchResponse(output, mock(HgStoreWrapperEx.class), executor); + Field lockField = ScanBatchResponse.class.getDeclaredField("stateLock"); + lockField.setAccessible(true); + Field state = ScanBatchResponse.class.getDeclaredField("state"); + state.setAccessible(true); + Field cancelled = ScanBatchResponse.class.getDeclaredField("cancelled"); + cancelled.setAccessible(true); + AtomicBoolean cancelling = (AtomicBoolean) cancelled.get(response); + Object done = Arrays.stream(state.getType().getEnumConstants()) + .filter(value -> value.toString().equals("DONE")).findFirst().get(); + FutureTask receipt = new FutureTask<>(() -> { + response.onNext(ScanStreamBatchReq.newBuilder().setReceiptRequest( + org.apache.hugegraph.store.grpc.stream.ScanReceiptRequest.newBuilder().setTimes(1)).build()); + return null; + }); + FutureTask failing = new FutureTask<>(() -> { + response.onError(Status.INTERNAL.asRuntimeException()); + return null; + }); + Thread reader = new Thread(receipt, "receipt-at-terminal-transition"); + Thread closer = new Thread(failing, "error-at-terminal-transition"); + try { + synchronized (lockField.get(response)) { + reader.start(); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + while (reader.getState() != Thread.State.BLOCKED && System.nanoTime() < deadline) { + Thread.yield(); + } + assertEquals(Thread.State.BLOCKED, reader.getState()); + closer.start(); + deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + while (!cancelling.get() && System.nanoTime() < deadline) { + Thread.yield(); + } + assertTrue(cancelling.get()); + state.set(response, done); + } + receipt.get(2, TimeUnit.SECONDS); + failing.get(2, TimeUnit.SECONDS); + verify(output, never()).onNext(any(KvStream.class)); + verify(output).onError(any(Throwable.class)); + } finally { + executor.shutdownNow(); + reader.join(1000); + closer.join(1000); + } + } + + @Test(timeout = 10000) + public void testOrderedVertexRemainsConsecutiveAcrossBackpressureWithTwoScanners() throws Exception { + AtomicBoolean defer = new AtomicBoolean(true); + List initial = new ArrayList<>(); + ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 0, TimeUnit.SECONDS, + new java.util.concurrent.LinkedBlockingQueue<>()) { + @Override + public void execute(Runnable command) { + if (defer.get()) { + initial.add(command); + } else { + super.execute(command); + } + } + }; + ScanIterator first = mock(ScanIterator.class); + ScanIterator second = mock(ScanIterator.class); + AtomicInteger firstRows = new AtomicInteger(); + AtomicInteger secondRows = new AtomicInteger(); + CountDownLatch firstBackpressure = new CountDownLatch(1); + CountDownLatch secondReading = new CountDownLatch(1); + when(first.hasNext()).thenAnswer(ignored -> firstRows.get() < 8); + when(first.next()).thenAnswer(ignored -> { + if (firstRows.incrementAndGet() == 5) { + firstBackpressure.countDown(); + } + return RocksDBSession.BackendColumn.of(new byte[]{1, 0, 0, 0}, new byte[16]); + }); + when(second.hasNext()).thenAnswer(ignored -> secondRows.get() < 2); + when(second.next()).thenAnswer(ignored -> { + secondRows.incrementAndGet(); + secondReading.countDown(); + return RocksDBSession.BackendColumn.of(new byte[]{2, 0, 0, 0}, new byte[16]); + }); + AtomicInteger supplies = new AtomicInteger(); + Field bodySize = ParallelScanIterator.class.getDeclaredField("maxBodySize"); + bodySize.setAccessible(true); + int previous = bodySize.getInt(null); + bodySize.setInt(null, 16); + ParallelScanIterator scan = null; + try { + scan = ParallelScanIterator.of(() -> { + int index = supplies.incrementAndGet(); + return new KVPair<>(mock(QueryCondition.class), index == 1 ? first : index == 2 ? second : null); + }, () -> Long.MAX_VALUE, + ScanQueryRequest.newBuilder().setOrderType(ScanOrderType.ORDER_WITHIN_VERTEX).build(), executor); + // Force two owned scanners independently of the container's CPU-derived default. + Field registry = ParallelScanIterator.class.getDeclaredField("scanners"); + registry.setAccessible(true); + java.util.Queue scanners = (java.util.Queue) registry.get(scan); + Class scannerType = scanners.peek().getClass(); + java.lang.reflect.Constructor constructor = + scannerType.getDeclaredConstructor(ParallelScanIterator.class); + constructor.setAccessible(true); + Object other = constructor.newInstance(scan); + scanners.add(other); + Method run = scannerType.getDeclaredMethod("scanKV"); + run.setAccessible(true); + defer.set(false); + executor.execute(initial.get(0)); + assertTrue(firstBackpressure.await(2, TimeUnit.SECONDS)); + executor.execute(() -> { + try { + run.invoke(other); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + }); + assertTrue(secondReading.await(2, TimeUnit.SECONDS)); + List vertices = new ArrayList<>(); + while (scan.hasNext()) { + for (Object row : scan.next()) { + Field key = row.getClass().getField("key"); + key.setAccessible(true); + vertices.add((int) ((byte[]) key.get(row))[0]); + } + } + assertEquals(Arrays.asList(1, 1, 1, 1, 1, 1, 1, 1, 2, 2), vertices); + verify(first).close(); + verify(second).close(); + } finally { + if (scan != null) { + scan.close(); + } + executor.shutdownNow(); + bodySize.setInt(null, previous); + } + } + + @Test(timeout = 5000) + public void testUnsupportedPausedBatchClosesOwnedIteratorAndRetainsFailure() throws Exception { + for (boolean fails : new boolean[]{false, true}) { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreStreamImpl service = scanService(executor, mock(HgStoreWrapperEx.class)); + StreamObserver output = mock(StreamObserver.class); + doThrow(new AssertionError("injected response callback failure")) + .when(output).onError(any(Throwable.class)); + StreamObserver input = service.scanBatch2(output); + ScanBatchResponse response = null; + for (Field field : input.getClass().getDeclaredFields()) { + field.setAccessible(true); + Object captured = field.get(input); + if (captured instanceof ScanBatchResponse) { + response = (ScanBatchResponse) captured; + } + } + org.junit.Assert.assertNotNull(response); + ScanIterator iterator = mock(ScanIterator.class); + if (fails) { + doThrow(new IllegalStateException("paused iterator close failed")).when(iterator).close(); + } + Field resource = ScanBatchResponse.class.getDeclaredField("iterator"); + resource.setAccessible(true); + resource.set(response, iterator); + try { + // An idle response can retain a native iterator while waiting for receipts. + input.onNext(ScanStreamBatchReq.getDefaultInstance()); + verify(iterator).close(); + verify(output).onError(any(Throwable.class)); + verify(output, never()).onCompleted(); + assertEquals(fails ? 1 : 0, scanRegistry(service).size()); + service.shutdownScans(); + input.onCompleted(); + verify(iterator).close(); + } finally { + scanRegistry(service).clear(); + executor.shutdownNow(); + } + } + } + + @Test(timeout = 5000) + public void testFailedCleanupAndResponseCallbackStillClearWorkerOwnership() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1, task -> { + Thread thread = new Thread(task, "scan-close-callback-failure"); + thread.setUncaughtExceptionHandler((ignored, failure) -> { }); + return thread; + }); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + StreamObserver output = mock(StreamObserver.class); + CountDownLatch sending = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + doAnswer(invocation -> { + // Exercise cleanup failure while the response callback is already in flight. + assertTrue(sending.await(2, TimeUnit.SECONDS)); + throw new IllegalStateException("native close failed"); + }).when(iterator).close(); + doAnswer(invocation -> { + sending.countDown(); + assertTrue(release.await(2, TimeUnit.SECONDS)); + return null; + }).when(output).onNext(any(KvPageRes.class)); + doThrow(new AssertionError("error callback failed")).when(output).onError(any(Throwable.class)); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(5); + ScanStreamResponse response = ScanStreamResponse.of(output, wrapper, executor, config); + FutureTask request = new FutureTask<>(() -> { + response.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build()); + return null; + }); + Thread caller = new Thread(request, "scan-close-callback-request"); + try { + caller.start(); + assertTrue(sending.await(1, TimeUnit.SECONDS)); + Field finished = ScanStreamResponse.class.getDeclaredField("finishFlag"); + finished.setAccessible(true); + AtomicBoolean terminal = (AtomicBoolean) finished.get(response); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + while (!terminal.get() && System.nanoTime() < deadline) { + Thread.yield(); + } + assertTrue("cleanup failure must claim error before normal completion", terminal.get()); + release.countDown(); + request.get(2, TimeUnit.SECONDS); + executor.shutdown(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + Field worker = ScanStreamResponse.class.getDeclaredField("worker"); + worker.setAccessible(true); + assertNull(worker.get(response)); + verify(iterator).close(); + verify(output).onError(any(Throwable.class)); + verify(output, never()).onCompleted(); + } finally { + release.countDown(); + executor.shutdownNow(); + caller.join(1000); + } + } + + @Test(timeout = 5000) + public void testTerminalAdmissionRejectsRequestWaitingForLifecycleLock() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + HgStoreStreamImpl service = scanService(executor, wrapper); + StreamObserver input = service.scanBatch(mock(StreamObserver.class)); + Object lifecycle = scanRegistry(service).keySet().iterator().next(); + Method finished = lifecycle.getClass().getDeclaredMethod("finishWithoutResponse"); + finished.setAccessible(true); + FutureTask request = new FutureTask<>(() -> { + input.onNext(batchRequest()); + return null; + }); + Thread late = new Thread(request, "late-scan-admission"); + try { + synchronized (lifecycle) { + late.start(); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + while (late.getState() != Thread.State.BLOCKED && System.nanoTime() < deadline) { + Thread.yield(); + } + assertEquals(Thread.State.BLOCKED, late.getState()); + // Exercise the atomic admission invariant directly, without claiming a native fault. + finished.invoke(lifecycle); + assertTrue(scanRegistry(service).isEmpty()); + } + request.get(1, TimeUnit.SECONDS); + service.shutdownScans(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + verifyNoInteractions(wrapper); + assertEquals(0, executor.getTaskCount()); + } finally { + executor.shutdownNow(); + late.join(1000); + } + } + + @Test(timeout = 10000) + public void testInProcessNormalCompletionDoesNotCancelCompletedScan() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + HgStoreStreamImpl service = scanService(executor, wrapper); + String name = InProcessServerBuilder.generateName(); + Server server = InProcessServerBuilder.forName(name) + .directExecutor().addService(service).build().start(); + ManagedChannel channel = InProcessChannelBuilder.forName(name) + .directExecutor().build(); + org.apache.logging.log4j.core.Logger logger = (org.apache.logging.log4j.core.Logger) + org.apache.logging.log4j.LogManager.getLogger(ScanStreamResponse.class); + List messages = new java.util.concurrent.CopyOnWriteArrayList<>(); + org.apache.logging.log4j.core.appender.AbstractAppender logs = + new org.apache.logging.log4j.core.appender.AbstractAppender( + "scan-normal-completion", null, null, false, + org.apache.logging.log4j.core.config.Property.EMPTY_ARRAY) { + @Override + public void append(org.apache.logging.log4j.core.LogEvent event) { + messages.add(event.getMessage().getFormattedMessage()); + } + }; + logs.start(); + logger.addAppender(logs); + CountDownLatch completed = new CountDownLatch(1); + AtomicInteger errors = new AtomicInteger(); + try { + StreamObserver request = + HgStoreStreamGrpc.newStub(channel) + .scan(new StreamObserver() { + @Override + public void onNext(KvPageRes value) { } + + @Override + public void onError(Throwable failure) { + errors.incrementAndGet(); + completed.countDown(); + } + + @Override + public void onCompleted() { + completed.countDown(); + } + }); + request.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build()); + assertTrue(completed.await(2, TimeUnit.SECONDS)); + service.shutdownScans(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + service.awaitScanCleanup(); + assertEquals(0, errors.get()); + assertTrue(messages.stream().noneMatch(message -> message.contains("onError from client"))); + verify(iterator).close(); + assertTrue(scanRegistry(service).isEmpty()); + } finally { + logger.removeAppender(logs); + logs.stop(); + channel.shutdownNow(); + server.shutdownNow(); + executor.shutdownNow(); + } + } + + @Test(timeout = 10000) + public void testInProcessClientCancellationDrainsIterator() throws Exception { + assertInProcessCancellation(false); + assertInProcessCancellation(true); + } + + private static void assertInProcessCancellation(boolean completionRace) throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch releaseRead = new CountDownLatch(1); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.hasNext()).thenAnswer(invocation -> { + reading.countDown(); + try { + releaseRead.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return false; + }); + HgStoreStreamImpl service = scanService(executor, wrapper); + // gRPC 1.39 dispatches context cancellation through the application executor too. + // Keep a slot for cancellation while scan.onNext waits for its worker's first page. + ExecutorService callbacks = Executors.newFixedThreadPool(2); + String name = InProcessServerBuilder.generateName(); + Server server = InProcessServerBuilder.forName(name) + .executor(callbacks).addService(service).build().start(); + ManagedChannel channel = InProcessChannelBuilder.forName(name) + .directExecutor().build(); + Context.CancellableContext context = Context.current().withCancellation(); + CountDownLatch ended = new CountDownLatch(1); + AtomicInteger terminals = new AtomicInteger(); + AtomicReference failure = new AtomicReference<>(); + try { + context.run(() -> { + StreamObserver request = + HgStoreStreamGrpc.newStub(channel) + .scan(new StreamObserver() { + @Override + public void onNext(KvPageRes value) { } + + @Override + public void onError(Throwable error) { + failure.set(error); + terminals.incrementAndGet(); + ended.countDown(); + } + + @Override + public void onCompleted() { + terminals.incrementAndGet(); + ended.countDown(); + } + }); + request.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build()); + }); + assertTrue(reading.await(2, TimeUnit.SECONDS)); + if (completionRace) { + CountDownLatch start = new CountDownLatch(1); + Thread cancel = new Thread(() -> { + try { + start.await(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + context.cancel(null); + }, "scan-completion-cancel-race"); + cancel.start(); + start.countDown(); + releaseRead.countDown(); + cancel.join(2000); + assertFalse(cancel.isAlive()); + } else { + context.cancel(null); + } + assertTrue(ended.await(2, TimeUnit.SECONDS)); + verify(iterator, timeout(2000)).close(); + service.shutdownScans(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + service.awaitScanCleanup(); + assertEquals(1, terminals.get()); + if (!completionRace || failure.get() != null) { + assertEquals(Status.Code.CANCELLED, Status.fromThrowable(failure.get()).getCode()); + } + verify(iterator).close(); + assertTrue(scanRegistry(service).isEmpty()); + } finally { + releaseRead.countDown(); + context.cancel(null); + channel.shutdownNow(); + server.shutdownNow(); + executor.shutdownNow(); + callbacks.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testBatchRejectionReportsStatusAndClosesUnstartedIterator() throws Exception { + for (boolean stopped : new boolean[]{false, true}) { + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 1, TimeUnit.SECONDS, + new SynchronousQueue<>()); + CountDownLatch release = new CountDownLatch(1); + if (stopped) { + executor.shutdown(); + } else { + executor.execute(() -> { + try { + release.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + HgStoreStreamImpl service = scanService(executor, wrapper); + StreamObserver output = mock(StreamObserver.class); + try { + service.scanBatch(output).onNext(batchRequest()); + ArgumentCaptor error = ArgumentCaptor.forClass(Throwable.class); + verify(output).onError(error.capture()); + assertEquals(stopped ? Status.Code.UNAVAILABLE : Status.Code.RESOURCE_EXHAUSTED, + Status.fromThrowable(error.getValue()).getCode()); + verify(output, never()).onCompleted(); + assertTrue(scanRegistry(service).isEmpty()); + verifyNoInteractions(wrapper); + } finally { + release.countDown(); + executor.shutdownNow(); + } + } + } + + @Test(timeout = 5000) + public void testBatchTimeoutCleanupDoesNotHoldAdmissionMonitor() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreStreamImpl service = scanService(executor, mock(HgStoreWrapperEx.class)); + ScanBatchResponse expired = mock(ScanBatchResponse.class); + CountDownLatch checking = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + ScanBatchResponseFactory factory = ScanBatchResponseFactory.getInstance(); + doAnswer(invocation -> { + assertFalse(Thread.holdsLock(service)); + checking.countDown(); + assertTrue(release.await(2, TimeUnit.SECONDS)); + return null; + }).when(expired).checkActiveTimeout(); + FutureTask> opening = new FutureTask<>(() -> + service.scanBatch2(mock(StreamObserver.class))); + Thread caller = new Thread(opening, "test-batch-timeout-cleanup"); + try { + factory.addStreamObserver(expired); + caller.start(); + assertTrue(checking.await(1, TimeUnit.SECONDS)); + FutureTask stopping = new FutureTask<>(() -> { + service.stopAcceptingScans(); + return null; + }); + Thread stopper = new Thread(stopping, "test-batch-stop-admission"); + stopper.start(); + stopping.get(1, TimeUnit.SECONDS); + assertUnavailable(() -> service.scan(mock(StreamObserver.class))); + assertFalse(opening.isDone()); + release.countDown(); + opening.get(1, TimeUnit.SECONDS).onError(Status.CANCELLED.asRuntimeException()); + assertTrue(scanRegistry(service).isEmpty()); + } finally { + release.countDown(); + factory.removeStreamObserver(expired); + caller.join(2000); + service.shutdownScans(); + executor.shutdownNow(); + } + } + + @Test(timeout = 10000) + public void testOneShotCancellationInterruptsCallerAndClosesIterator() throws Exception { + for (boolean batch : new boolean[]{false, true}) { + for (boolean transport : new boolean[]{false, true}) { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + StreamObserver output = mock(StreamObserver.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch interrupted = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + when(iterator.hasNext()).thenAnswer(invocation -> { + reading.countDown(); + try { + assertTrue(release.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException expected) { + interrupted.countDown(); + Thread.currentThread().interrupt(); + } + return false; + }); + HgStoreStreamImpl service = scanService(executor, wrapper); + Context.CancellableContext context = Context.current().withCancellation(); + FutureTask scanning = new FutureTask<>(() -> { + context.run(() -> { + if (batch) { + service.scanBatchOneShot(batchRequest(), output); + } else { + service.scanOneShot(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setLimit(10).build(), output); + } + }); + assertFalse("a cancelled call must not interrupt the next call", + Thread.currentThread().isInterrupted()); + doReturn(false).when(iterator).hasNext(); + StreamObserver nextOutput = mock(StreamObserver.class); + HgStoreStreamImpl nextService = scanService(executor, wrapper); + if (batch) { + nextService.scanBatchOneShot(batchRequest(), nextOutput); + } else { + nextService.scanOneShot(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setLimit(10).build(), nextOutput); + } + verify(nextOutput).onNext(any(KvPageRes.class)); + verify(nextOutput).onCompleted(); + verify(nextOutput, never()).onError(any(Throwable.class)); + return null; + }); + Thread caller = new Thread(scanning, "test-one-shot-blocked-read"); + try { + caller.start(); + assertTrue(reading.await(1, TimeUnit.SECONDS)); + if (transport) { + context.cancel(new IllegalStateException("client canceled")); + } else { + service.shutdownScans(); + } + assertTrue(interrupted.await(1, TimeUnit.SECONDS)); + scanning.get(1, TimeUnit.SECONDS); + assertCancelled(output); + verify(output, never()).onNext(any(KvPageRes.class)); + verify(output, never()).onCompleted(); + verify(iterator, times(2)).close(); + assertTrue(scanRegistry(service).isEmpty()); + } finally { + release.countDown(); + context.cancel(null); + caller.join(2000); + service.shutdownScans(); + executor.shutdownNow(); + assertFalse(caller.isAlive()); + } + } + } + } + + private static void awaitCloseRelease(CountDownLatch release) { + boolean interrupted = false; + try { + for (;;) { + try { + assertTrue(release.await(5, TimeUnit.SECONDS)); + return; + } catch (InterruptedException ignored) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + @Test(timeout = 10000) + public void testInProcessOneShotCancellationDuringCloseReportsCancelled() throws Exception { + for (boolean batch : new boolean[]{false, true}) { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.hasNext()).thenReturn(false); + CountDownLatch closing = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + doAnswer(invocation -> { + closing.countDown(); + awaitCloseRelease(release); + return null; + }).when(iterator).close(); + HgStoreStreamImpl service = scanService(executor, wrapper); + String name = InProcessServerBuilder.generateName(); + Server server = InProcessServerBuilder.forName(name).directExecutor() + .addService(service).build().start(); + ManagedChannel channel = InProcessChannelBuilder.forName(name).directExecutor().build(); + FutureTask call = new FutureTask<>(() -> { + try { + HgStoreStreamGrpc.HgStoreStreamBlockingStub stub = + HgStoreStreamGrpc.newBlockingStub(channel).withDeadlineAfter(3, TimeUnit.SECONDS); + if (batch) { + stub.scanBatchOneShot(batchRequest()); + } else { + stub.scanOneShot(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setLimit(10).build()); + } + fail("cancelled unary scan reported success"); + return Status.Code.OK; + } catch (StatusRuntimeException error) { + return error.getStatus().getCode(); + } + }); + Thread caller = new Thread(call, "one-shot-cancellation-client"); + try { + caller.start(); + assertTrue(closing.await(2, TimeUnit.SECONDS)); + service.shutdownScans(); + assertEquals(1, scanRegistry(service).size()); + assertFalse(call.isDone()); + release.countDown(); + assertEquals(Status.Code.CANCELLED, call.get(2, TimeUnit.SECONDS)); + service.awaitScanCleanup(); + assertTrue(scanRegistry(service).isEmpty()); + verify(iterator).close(); + } finally { + release.countDown(); + channel.shutdownNow(); + server.shutdownNow(); + executor.shutdownNow(); + caller.join(2000); + assertFalse(caller.isAlive()); + assertTrue(channel.awaitTermination(2, TimeUnit.SECONDS)); + assertTrue(server.awaitTermination(2, TimeUnit.SECONDS)); + } + } + } + + @Test(timeout = 10000) + public void testOneShotCancelledDuringCloseDoesNotCompleteSuccessfully() throws Exception { + for (boolean batch : new boolean[]{false, true}) { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + StreamObserver output = mock(StreamObserver.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.hasNext()).thenReturn(false); + CountDownLatch closing = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + doAnswer(invocation -> { + closing.countDown(); + awaitCloseRelease(release); + return null; + }).when(iterator).close(); + HgStoreStreamImpl service = scanService(executor, wrapper); + FutureTask scanning = new FutureTask<>(() -> { + if (batch) { + service.scanBatchOneShot(batchRequest(), output); + } else { + service.scanOneShot(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setLimit(10).build(), output); + } + return null; + }); + Thread caller = new Thread(scanning, "one-shot-close-cancellation"); + try { + caller.start(); + assertTrue(closing.await(2, TimeUnit.SECONDS)); + service.shutdownScans(); + assertEquals(1, scanRegistry(service).size()); + assertFalse(scanning.isDone()); + verifyNoInteractions(output); + release.countDown(); + scanning.get(2, TimeUnit.SECONDS); + ArgumentCaptor error = ArgumentCaptor.forClass(Throwable.class); + verify(output).onError(error.capture()); + assertEquals(Status.Code.CANCELLED, Status.fromThrowable(error.getValue()).getCode()); + verify(output, never()).onNext(any(KvPageRes.class)); + verify(output, never()).onCompleted(); + verify(iterator).close(); + assertTrue(scanRegistry(service).isEmpty()); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + } finally { + release.countDown(); + caller.join(2000); + executor.shutdownNow(); + assertFalse(caller.isAlive()); + } + } + } + + @Test(timeout = 5000) + public void testBatchShutdownInterruptsBlockedReaderBeforeIteratorClose() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch interrupted = new CountDownLatch(1); + CountDownLatch readExited = new CountDownLatch(1); + AtomicBoolean closedAfterRead = new AtomicBoolean(); + when(iterator.hasNext()).thenAnswer(invocation -> { + reading.countDown(); + try { + new CountDownLatch(1).await(); + return true; + } catch (InterruptedException expected) { + interrupted.countDown(); + return false; + } finally { + readExited.countDown(); + } + }); + doAnswer(invocation -> { + closedAfterRead.set(readExited.getCount() == 0); + return null; + }).when(iterator).close(); + + HgStoreStreamImpl service = scanService(executor, wrapper); + StreamObserver output = mock(StreamObserver.class); + StreamObserver input = service.scanBatch(output); + FutureTask shutdown = new FutureTask<>(() -> { + service.shutdownScans(); + return null; + }); + Thread shutdownThread = new Thread(shutdown, "batch-scan-shutdown-test"); + try { + input.onNext(batchRequest()); + assertTrue("batch reader must enter its blocking iterator read", + reading.await(1, TimeUnit.SECONDS)); + shutdownThread.start(); + shutdown.get(2, TimeUnit.SECONDS); + assertTrue(interrupted.await(1, TimeUnit.SECONDS)); + service.awaitScanCleanup(); + assertTrue(closedAfterRead.get()); + assertTrue(scanRegistry(service).isEmpty()); + assertCancelled(output); + verify(output, never()).onCompleted(); + verify(iterator).close(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + } finally { + executor.shutdownNow(); + shutdownThread.interrupt(); + shutdownThread.join(1000); + } + } + + @Test(timeout = 5000) + public void testEncodedBatchShutdownInterruptsBlockedScannerBeforeIteratorClose() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + CountDownLatch reading = new CountDownLatch(1); + CountDownLatch interrupted = new CountDownLatch(1); + CountDownLatch readExited = new CountDownLatch(1); + AtomicBoolean closedAfterRead = new AtomicBoolean(); + when(iterator.hasNext()).thenAnswer(invocation -> { + reading.countDown(); + try { + new CountDownLatch(1).await(); + return true; + } catch (InterruptedException expected) { + interrupted.countDown(); + return false; + } finally { + readExited.countDown(); + } + }); + doAnswer(invocation -> { + closedAfterRead.set(readExited.getCount() == 0); + return null; + }).when(iterator).close(); + + HgStoreStreamImpl service = scanService(executor, wrapper); + StreamObserver output = mock(StreamObserver.class); + StreamObserver input = service.scanBatch2(output); + FutureTask shutdown = new FutureTask<>(() -> { + service.shutdownScans(); + return null; + }); + Thread shutdownThread = new Thread(shutdown, "encoded-batch-scan-shutdown-test"); + try { + input.onNext(batchRequest()); + assertTrue("parallel scanner must enter its blocking iterator read", + reading.await(1, TimeUnit.SECONDS)); + shutdownThread.start(); + shutdown.get(2, TimeUnit.SECONDS); + assertTrue(interrupted.await(1, TimeUnit.SECONDS)); + service.awaitScanCleanup(); + assertTrue(closedAfterRead.get()); + assertTrue(scanRegistry(service).isEmpty()); + ArgumentCaptor failure = ArgumentCaptor.forClass(Throwable.class); + verify(output).onError(failure.capture()); + assertEquals(Status.Code.CANCELLED, Status.fromThrowable(failure.getValue()).getCode()); + verify(output, never()).onCompleted(); + verify(iterator).close(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + } finally { + executor.shutdownNow(); + shutdownThread.interrupt(); + shutdownThread.join(1000); + } + } + + @Test(timeout = 5000) + public void testOneShotObservesContextCancellationAndClosesIterator() { + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + StreamObserver output = mock(StreamObserver.class); + Context.CancellableContext context = Context.current().withCancellation(); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.hasNext()).thenAnswer(invocation -> { + context.cancel(null); + return true; + }); + when(iterator.next()).thenReturn(RocksDBSession.BackendColumn.of(new byte[4], new byte[0])); + when(iterator.position()).thenReturn(new byte[4]); + try { + context.run(() -> ScanOneShotResponse.scanOneShot( + ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL).setLimit(100).build(), + output, wrapper)); + verify(iterator).close(); + verifyNoInteractions(output); + } finally { + context.cancel(null); + } + } + + @Test(timeout = 5000) + public void testBatchDoneRejectsSecondQueryBeforeAllocatingIterator() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator source = mock(ScanIterator.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(source); + StreamObserver output = mock(StreamObserver.class); + ScanBatchResponse response = new ScanBatchResponse(output, wrapper, executor); + Field state = ScanBatchResponse.class.getDeclaredField("state"); + state.setAccessible(true); + Field iterator = ScanBatchResponse.class.getDeclaredField("iterator"); + iterator.setAccessible(true); + try { + response.onNext(batchRequest()); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); + while (!"DONE".equals(state.get(response).toString()) && System.nanoTime() < deadline) { + Thread.yield(); + } + assertEquals("DONE", state.get(response).toString()); + verify(source).close(); + assertNull(iterator.get(response)); + response.onNext(batchRequest()); + // The single worker processes the queued request before this barrier. + executor.submit(() -> { }).get(2, TimeUnit.SECONDS); + assertNull("a completed query must reject another request before allocating", iterator.get(response)); + response.onCompleted(); + response.onCompleted(); + verify(wrapper).scanAll(anyString(), anyString(), any(byte[].class)); + verify(source).close(); + verify(output).onCompleted(); + verify(output, never()).onError(any(Throwable.class)); + } finally { + // Preserve cleanup even when the old implementation allocated a second producer. + Object remaining = iterator.get(response); + response.onCompleted(); + if (remaining instanceof ScanIterator) { + ((ScanIterator) remaining).close(); + } + executor.shutdownNow(); + } + } + + @Test + public void testPausedBatchClosesIteratorAndRejectsLaterQuery() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + StreamObserver output = mock(StreamObserver.class); + ScanBatchResponse response = new ScanBatchResponse(output, wrapper, executor); + Field field = ScanBatchResponse.class.getDeclaredField("iterator"); + field.setAccessible(true); + field.set(response, iterator); + try { + response.onCompleted(); + response.onNext(ScanStreamBatchReq.newBuilder() + .setQueryRequest(ScanQueryRequest.getDefaultInstance()) + .build()); + response.onCompleted(); + verify(iterator).close(); + verifyNoInteractions(wrapper); + assertNull(field.get(response)); + assertFalse(executor.isShutdown()); + } finally { + executor.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testRejectedStreamReportsOnlyFailure() { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + executor.shutdown(); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(60); + StreamObserver output = mock(StreamObserver.class); + ScanStreamResponse response = ScanStreamResponse.of(output, wrapper, executor, config); + ScanStreamReq request = ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build(); + + response.onNext(request); + response.onNext(request); + response.onCompleted(); + + verify(output).onError(any(Throwable.class)); + verify(output, never()).onNext(any(KvPageRes.class)); + verify(output, never()).onCompleted(); + verifyNoInteractions(wrapper); + } + + @Test(timeout = 5000) + public void testStreamIteratorFailureReportsOnlyFailure() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))) + .thenThrow(new IllegalStateException("iterator failed")); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(60); + StreamObserver output = mock(StreamObserver.class); + ScanStreamResponse response = ScanStreamResponse.of(output, wrapper, executor, config); + FutureTask request = new FutureTask<>(() -> { + response.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build()); + return null; + }); + Thread caller = new Thread(request, "scan-failure-test"); + try { + caller.start(); + request.get(1, TimeUnit.SECONDS); + executor.shutdown(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + response.onCompleted(); + verify(output).onError(any(Throwable.class)); + verify(output, never()).onNext(any(KvPageRes.class)); + verify(output, never()).onCompleted(); + } finally { + response.onCompleted(); + executor.shutdownNow(); + caller.join(1000); + } + } + + @Test(timeout = 5000) + public void testReceiveFailureDoesNotInterruptItsCallbackThread() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + CountDownLatch occupied = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + executor.execute(() -> { + occupied.countDown(); + try { + release.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(0); + StreamObserver output = mock(StreamObserver.class); + ScanStreamResponse response = ScanStreamResponse.of(output, wrapper, executor, config); + FutureTask request = new FutureTask<>(() -> { + response.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build()); + return Thread.currentThread().isInterrupted(); + }); + Thread caller = new Thread(request, "scan-receive-failure-test"); + try { + assertTrue(occupied.await(1, TimeUnit.SECONDS)); + caller.start(); + assertFalse(request.get(1, TimeUnit.SECONDS)); + release.countDown(); + executor.shutdown(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + verify(output).onError(any(Throwable.class)); + verify(output, never()).onNext(any(KvPageRes.class)); + verify(output, never()).onCompleted(); + verifyNoInteractions(wrapper); + } finally { + release.countDown(); + response.onCompleted(); + executor.shutdownNow(); + caller.join(1000); + } + } + + @Test(timeout = 10000) + public void testStreamFailureWaitsForPageCallback() throws Exception { + AtomicReference scanWorker = new AtomicReference<>(); + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1, task -> { + Thread thread = new Thread(task, "scan-page-failure-worker"); + scanWorker.set(thread); + return thread; + }); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.next()).thenReturn(RocksDBSession.BackendColumn.of(new byte[4], new byte[0])); + when(iterator.position()).thenReturn(new byte[4]); + CountDownLatch pageEntered = new CountDownLatch(1); + CountDownLatch releasePage = new CountDownLatch(1); + CountDownLatch iteratorFailed = new CountDownLatch(1); + CountDownLatch errorReceived = new CountDownLatch(1); + AtomicInteger advances = new AtomicInteger(); + when(iterator.hasNext()).thenAnswer(invocation -> { + if (advances.incrementAndGet() <= 2) { + return true; + } + assertTrue(pageEntered.await(1, TimeUnit.SECONDS)); + iteratorFailed.countDown(); + throw new IllegalStateException("failure after first page"); + }); + StreamObserver output = mock(StreamObserver.class); + List signals = Collections.synchronizedList(new ArrayList<>()); + doAnswer(invocation -> { + signals.add("page-start"); + pageEntered.countDown(); + assertTrue(releasePage.await(2, TimeUnit.SECONDS)); + signals.add("page-end"); + return null; + }).when(output).onNext(any(KvPageRes.class)); + doAnswer(invocation -> { + signals.add("error"); + errorReceived.countDown(); + return null; + }).when(output).onError(any(Throwable.class)); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(60); + ScanStreamResponse response = ScanStreamResponse.of(output, wrapper, executor, config); + FutureTask request = new FutureTask<>(() -> { + response.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build()); + return null; + }); + Thread caller = new Thread(request, "scan-page-failure-test"); + try { + caller.start(); + assertTrue(pageEntered.await(1, TimeUnit.SECONDS)); + assertTrue(iteratorFailed.await(1, TimeUnit.SECONDS)); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + while (scanWorker.get().getState() != Thread.State.BLOCKED && + errorReceived.getCount() != 0 && System.nanoTime() < deadline) { + Thread.yield(); + } + assertEquals("worker must reach the response serialization point", + Thread.State.BLOCKED, scanWorker.get().getState()); + assertEquals(1L, errorReceived.getCount()); + releasePage.countDown(); + request.get(1, TimeUnit.SECONDS); + executor.shutdown(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + assertEquals(Arrays.asList("page-start", "page-end", "error"), signals); + verify(output).onError(any(Throwable.class)); + verify(output, never()).onCompleted(); + verify(iterator).close(); + } finally { + releasePage.countDown(); + response.onCompleted(); + executor.shutdownNow(); + caller.join(1000); + } + } + + @Test(timeout = 10000) + public void testNormalStreamCancellationClaimsTerminalBeforeInterruptingWorker() throws Exception { + for (boolean closeRequest : new boolean[]{false, true}) { + CountDownLatch workerEntered = new CountDownLatch(1); + CountDownLatch iteratorClosed = new CountDownLatch(1); + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.next()).thenReturn(RocksDBSession.BackendColumn.of(new byte[4], new byte[0])); + when(iterator.position()).thenReturn(new byte[4]); + AtomicInteger advances = new AtomicInteger(); + when(iterator.hasNext()).thenAnswer(invocation -> { + if (advances.incrementAndGet() <= 2) { + return true; + } + workerEntered.countDown(); + new CountDownLatch(1).await(); + return false; + }); + doAnswer(invocation -> { + iteratorClosed.countDown(); + return null; + }).when(iterator).close(); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(60); + StreamObserver output = mock(StreamObserver.class); + ScanStreamResponse response = ScanStreamResponse.of(output, wrapper, executor, config); + try { + response.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build()); + assertTrue(workerEntered.await(1, TimeUnit.SECONDS)); + if (closeRequest) { + response.onNext(ScanStreamReq.newBuilder().setCloseFlag(1).build()); + } else { + response.onCompleted(); + } + executor.shutdown(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + verify(output, never()).onError(any(Throwable.class)); + verify(output).onCompleted(); + verify(iterator).close(); + } finally { + response.onCompleted(); + executor.shutdownNow(); + } + } + } + + @Test + public void testRejectedBatchReportsFailureWithoutSuccessfulCompletion() { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + executor.shutdown(); + StreamObserver output = mock(StreamObserver.class); + ScanBatchResponse response = new ScanBatchResponse(output, mock(HgStoreWrapperEx.class), executor); + response.onNext(ScanStreamBatchReq.newBuilder() + .setQueryRequest(ScanQueryRequest.getDefaultInstance()).build()); + verify(output).onError(any(Throwable.class)); + verify(output, never()).onCompleted(); + } + + @Test + public void testBatchIteratorFailureClosesResourcesWithoutSuccessfulCompletion() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + StreamObserver output = mock(StreamObserver.class); + ScanIterator iterator = mock(ScanIterator.class); + IllegalStateException failure = new IllegalStateException("iterator failed"); + when(iterator.hasNext()).thenThrow(failure); + ScanBatchResponse response = new ScanBatchResponse(output, mock(HgStoreWrapperEx.class), executor); + Field field = ScanBatchResponse.class.getDeclaredField("iterator"); + field.setAccessible(true); + field.set(response, iterator); + Method send = ScanBatchResponse.class.getDeclaredMethod("sendEntries"); + send.setAccessible(true); + try { + send.invoke(response); + verify(iterator).close(); + verify(output).onError(failure); + verify(output, never()).onCompleted(); + } finally { + response.onCompleted(); + executor.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testActiveStreamCancellationReleasesBlockedProducer() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.hasNext()).thenReturn(true); + when(iterator.next()).thenReturn(RocksDBSession.BackendColumn.of(new byte[4], new byte[0])); + when(iterator.position()).thenReturn(new byte[4]); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(60); + StreamObserver output = mock(StreamObserver.class); + ScanStreamResponse response = ScanStreamResponse.of(output, wrapper, executor, config); + try { + // Consume just one page, then leave the producer waiting for its consumer. + response.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(1000).build()); + response.onError(Status.CANCELLED.asRuntimeException()); + executor.shutdown(); + assertTrue("cancel must release a producer without its 60-second timeout", + executor.awaitTermination(1, TimeUnit.SECONDS)); + verify(iterator).close(); + } finally { + response.onCompleted(); + executor.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testParallelPausedScannerReleasesIterator() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + ScanIterator source = mock(ScanIterator.class); + when(source.hasNext()).thenReturn(true); + when(source.next()).thenReturn(RocksDBSession.BackendColumn.of(new byte[4], new byte[0])); + Field bodySize = ParallelScanIterator.class.getDeclaredField("maxBodySize"); + bodySize.setAccessible(true); + int originalBodySize = bodySize.getInt(null); + bodySize.setInt(null, 2); + ParallelScanIterator scan = null; + try { + scan = ParallelScanIterator.of( + () -> new KVPair<>(mock(QueryCondition.class), source), () -> Long.MAX_VALUE, + ScanQueryRequest.getDefaultInstance(), executor); + // One worker fills its output allowance and pauses while retaining its iterator. + executor.submit(() -> { }).get(2, TimeUnit.SECONDS); + verify(source, atLeastOnce()).next(); + verify(source, never()).close(); + scan.close(); + scan.close(); + verify(source).close(); + executor.shutdown(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + } finally { + if (scan != null) { + scan.close(); + } + bodySize.setInt(null, originalBodySize); + executor.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testParallelOrderedScannerReleasesQueueLockOnEmptyIterator() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + ScanIterator first = mock(ScanIterator.class); + ScanIterator empty = mock(ScanIterator.class); + when(first.hasNext()).thenReturn(true); + when(first.next()).thenReturn(RocksDBSession.BackendColumn.of(new byte[4], new byte[0])); + when(empty.hasNext()).thenReturn(false); + + AtomicInteger supplies = new AtomicInteger(); + Field bodySize = ParallelScanIterator.class.getDeclaredField("maxBodySize"); + bodySize.setAccessible(true); + int originalBodySize = bodySize.getInt(null); + bodySize.setInt(null, 2); + ParallelScanIterator scan = null; + try { + scan = ParallelScanIterator.of( + () -> { + int supply = supplies.incrementAndGet(); + if (supply == 1) { + return new KVPair<>(mock(QueryCondition.class), first); + } + if (supply == 2) { + return new KVPair<>(mock(QueryCondition.class), empty); + } + return new KVPair<>(mock(QueryCondition.class), null); + }, + () -> 1L, + ScanQueryRequest.newBuilder().setOrderType(ScanOrderType.ORDER_WITHIN_VERTEX).build(), + executor); + + // Wait for the scanner task to finish before checking the lock owner. + executor.submit(() -> { }).get(2, TimeUnit.SECONDS); + Field queueLock = ParallelScanIterator.class.getDeclaredField("queueLock"); + queueLock.setAccessible(true); + assertFalse(((ReentrantLock) queueLock.get(scan)).isLocked()); + } finally { + if (scan != null) { + scan.close(); + } + bodySize.setInt(null, originalBodySize); + executor.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testParallelOrderedProducerCancelsWithFullOutputQueue() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + ScanIterator source = mock(ScanIterator.class); + Field bodySize = ParallelScanIterator.class.getDeclaredField("maxBodySize"); + bodySize.setAccessible(true); + byte[] value = new byte[bodySize.getInt(null)]; + CountDownLatch full = new CountDownLatch(1); + AtomicInteger rows = new AtomicInteger(); + when(source.hasNext()).thenReturn(true); + when(source.next()).thenAnswer(invocation -> { + if (rows.incrementAndGet() == 5) { + full.countDown(); + } + return RocksDBSession.BackendColumn.of(new byte[4], value); + }); + ParallelScanIterator scan = ParallelScanIterator.of( + () -> new KVPair<>(mock(QueryCondition.class), source), () -> Long.MAX_VALUE, + ScanQueryRequest.newBuilder().setOrderType(ScanOrderType.ORDER_WITHIN_VERTEX).build(), + executor); + try { + assertTrue("four queued batches must fill the single-scanner queue", + full.await(2, TimeUnit.SECONDS)); + scan.close(); + executor.shutdown(); + assertTrue("cancellation must release the blocked ordered producer", + executor.awaitTermination(1, TimeUnit.SECONDS)); + verify(source).close(); + } finally { + scan.close(); + executor.shutdownNow(); + } + } + + @Test + public void testInterruptedOneShotDoesNotSendPartialSuccess() throws Exception { + for (boolean batch : new boolean[]{false, true}) { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + StreamObserver output = mock(StreamObserver.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.hasNext()).thenReturn(true); + when(iterator.position()).thenReturn(new byte[4]); + when(iterator.next()).thenAnswer(invocation -> { + Thread.currentThread().interrupt(); + return RocksDBSession.BackendColumn.of(new byte[4], new byte[0]); + }); + try { + HgStoreStreamImpl service = scanService(executor, wrapper); + if (batch) { + service.scanBatchOneShot(batchRequest(), output); + } else { + service.scanOneShot(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL).setLimit(10).build(), + output); + } + verify(iterator).next(); + assertTrue("scan must preserve an external interrupt", + Thread.currentThread().isInterrupted()); + verify(output).onError(any(Throwable.class)); + verify(output, never()).onNext(any(KvPageRes.class)); + verify(output, never()).onCompleted(); + verify(iterator).close(); + StreamObserver nextOutput = mock(StreamObserver.class); + if (batch) { + service.scanBatchOneShot(batchRequest(), nextOutput); + } else { + service.scanOneShot(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL).setLimit(10).build(), + nextOutput); + } + assertTrue("an interrupt present at entry must be preserved", Thread.currentThread().isInterrupted()); + verify(nextOutput).onError(any(Throwable.class)); + verify(nextOutput, never()).onNext(any(KvPageRes.class)); + verify(nextOutput, never()).onCompleted(); + } finally { + Thread.interrupted(); + executor.shutdownNow(); + } + } + } + + @Test(timeout = 5000) + public void testInterruptedBatchWorkerDoesNotCompletePartialPage() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + StreamObserver output = mock(StreamObserver.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.hasNext()).thenReturn(true); + when(iterator.position()).thenReturn(new byte[4]); + when(iterator.next()).thenAnswer(invocation -> { + Thread.currentThread().interrupt(); + return RocksDBSession.BackendColumn.of(new byte[4], new byte[0]); + }); + StreamObserver response = ScanBatchResponse3.of(output, wrapper, executor); + try { + response.onNext(batchRequest()); + executor.shutdown(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + verify(iterator).next(); + assertCancelled(output); + verify(output, never()).onNext(any(KvPageRes.class)); + verify(output, never()).onCompleted(); + verify(iterator).close(); + } finally { + response.onCompleted(); + executor.shutdownNow(); + } + } + + @Test(timeout = 10000) + public void testCancellationDoesNotInterruptReceiverAfterHandoff() throws Exception { + assertReceiverInterruptAfterHandoff(false); + } + + @Test(timeout = 10000) + public void testCancellationPreservesExternalReceiverInterruptAfterHandoff() throws Exception { + assertReceiverInterruptAfterHandoff(true); + } + + @Test(timeout = 5000) + public void testReceivePreservesExternalInterrupt() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(60); + StreamObserver output = mock(StreamObserver.class); + ScanStreamResponse response = ScanStreamResponse.of(output, wrapper, executor, config); + FutureTask request = new FutureTask<>(() -> { + Thread.currentThread().interrupt(); + response.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build()); + return Thread.currentThread().isInterrupted(); + }); + Thread caller = new Thread(request, "scan-external-interrupt-test"); + try { + caller.start(); + assertTrue(request.get(1, TimeUnit.SECONDS)); + assertCancelled(output); + verify(output, never()).onCompleted(); + executor.shutdown(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + } finally { + response.onCompleted(); + executor.shutdownNow(); + caller.join(1000); + } + } + + private static void assertReceiverInterruptAfterHandoff(boolean externalInterrupt) throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.next()).thenReturn(RocksDBSession.BackendColumn.of(new byte[4], new byte[0])); + when(iterator.position()).thenReturn(new byte[4]); + CountDownLatch beforePage = new CountDownLatch(1); + CountDownLatch releasePage = new CountDownLatch(1); + AtomicInteger advances = new AtomicInteger(); + when(iterator.hasNext()).thenAnswer(invocation -> { + int advance = advances.incrementAndGet(); + if (advance == 2) { + beforePage.countDown(); + assertTrue(releasePage.await(2, TimeUnit.SECONDS)); + } + return advance <= 2; + }); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(60); + StreamObserver output = mock(StreamObserver.class); + ScanStreamResponse response = ScanStreamResponse.of(output, wrapper, executor, config); + Field responseLock = ScanStreamResponse.class.getDeclaredField("responseLock"); + responseLock.setAccessible(true); + FutureTask request = new FutureTask<>(() -> { + response.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build()); + return Thread.currentThread().isInterrupted(); + }); + Thread caller = new Thread(request, "scan-late-cancellation-test"); + try { + caller.start(); + assertTrue(beforePage.await(1, TimeUnit.SECONDS)); + synchronized (responseLock.get(response)) { + // The real channel hands off a page before its callback reaches responseLock. + releasePage.countDown(); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + while (caller.getState() != Thread.State.BLOCKED && System.nanoTime() < deadline) { + Thread.yield(); + } + assertEquals(Thread.State.BLOCKED, caller.getState()); + if (externalInterrupt) { + caller.interrupt(); + } + response.onCompleted(); + } + assertEquals(externalInterrupt, request.get(1, TimeUnit.SECONDS)); + executor.shutdown(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + verify(output).onCompleted(); + verify(output, never()).onError(any(Throwable.class)); + verify(output, never()).onNext(any(KvPageRes.class)); + verify(iterator).close(); + } finally { + releasePage.countDown(); + response.onCompleted(); + executor.shutdownNow(); + caller.join(1000); + } + } + + @Test(timeout = 5000) + public void testInterruptedStreamWorkerDoesNotCompletePartialPage() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.hasNext()).thenReturn(true); + when(iterator.position()).thenReturn(new byte[4]); + when(iterator.next()).thenAnswer(invocation -> { + Thread.currentThread().interrupt(); + return RocksDBSession.BackendColumn.of(new byte[4], new byte[0]); + }); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(60); + StreamObserver output = mock(StreamObserver.class); + ScanStreamResponse response = ScanStreamResponse.of(output, wrapper, executor, config); + try { + response.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build()); + executor.shutdown(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + verify(iterator).next(); + assertCancelled(output); + verify(output, never()).onNext(any(KvPageRes.class)); + verify(output, never()).onCompleted(); + verify(iterator).close(); + } finally { + response.onCompleted(); + executor.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testInterruptedStreamSenderReportsCancellation() throws Exception { + AtomicReference worker = new AtomicReference<>(); + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1, task -> { + Thread thread = new Thread(task, "scan-interrupted-sender-test"); + worker.set(thread); + return thread; + }); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator iterator = mock(ScanIterator.class); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(iterator); + when(iterator.hasNext()).thenReturn(true); + when(iterator.next()).thenReturn(RocksDBSession.BackendColumn.of(new byte[4], new byte[0])); + when(iterator.position()).thenReturn(new byte[4]); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(60); + StreamObserver output = mock(StreamObserver.class); + CountDownLatch errorReceived = new CountDownLatch(1); + doAnswer(invocation -> { + errorReceived.countDown(); + return null; + }).when(output).onError(any(Throwable.class)); + ScanStreamResponse response = ScanStreamResponse.of(output, wrapper, executor, config); + try { + // Consume one page, then interrupt the actual worker waiting to hand off its next page. + response.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build()); + awaitTimedWaiting(worker.get()); + worker.get().interrupt(); + assertTrue(errorReceived.await(1, TimeUnit.SECONDS)); + executor.shutdown(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + assertCancelled(output); + verify(output).onNext(any(KvPageRes.class)); + verify(output, never()).onCompleted(); + verify(iterator).close(); + } finally { + response.onCompleted(); + executor.shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testChannelCloseReleasesWaitingReceiverWithoutInterrupt() throws Exception { + HgChannel channel = HgChannel.of(60); + FutureTask receive = new FutureTask<>(() -> { + assertNull(channel.receive()); + return Thread.currentThread().isInterrupted(); + }); + Thread caller = new Thread(receive, "channel-close-receiver-test"); + try { + caller.start(); + awaitTimedWaiting(caller); + channel.close(); + assertFalse(receive.get(1, TimeUnit.SECONDS)); + assertNull(channel.receive()); + assertFalse(channel.send("after-close")); + } finally { + channel.close(); + caller.join(1000); + } + } + + @Test(timeout = 5000) + public void testChannelCloseReleasesWaitingProducer() throws Exception { + HgChannel channel = HgChannel.of(60); + FutureTask send = new FutureTask<>(() -> channel.send("page")); + Thread producer = new Thread(send, "channel-close-producer-test"); + try { + producer.start(); + awaitTimedWaiting(producer); + channel.close(); + assertFalse(send.get(1, TimeUnit.SECONDS)); + } finally { + channel.close(); + producer.join(1000); + } + } + + @Test(timeout = 5000) + public void testChannelRetainsTimeoutAndNormalHandoff() throws Exception { + HgChannel channel = HgChannel.of(1); + AtomicInteger timeouts = new AtomicInteger(); + long started = System.nanoTime(); + assertNull(channel.receive(timeout -> { + assertEquals(Long.valueOf(1L), timeout); + timeouts.incrementAndGet(); + })); + assertEquals(1, timeouts.get()); + assertTrue("short close polling must not shorten the configured timeout", + System.nanoTime() - started >= TimeUnit.MILLISECONDS.toNanos(900)); + FutureTask send = new FutureTask<>(() -> channel.send("page")); + Thread producer = new Thread(send, "channel-handoff-test"); + try { + producer.start(); + assertEquals("page", channel.receive()); + assertTrue(send.get(1, TimeUnit.SECONDS)); + } finally { + channel.close(); + producer.join(1000); + } + } + + @Test + public void testClosingPreventsLazyExecutorAndStateCreation() { + HgStoreStreamImpl service = new HgStoreStreamImpl(); + service.stopAcceptingScans(); + assertUnavailable(service::getExecutor); + assertUnavailable(service::getState); + assertUnavailable(() -> service.scan(mock(StreamObserver.class))); + service.shutdownScans(); + assertNull(service.getRealExecutor()); + } + + @Test(timeout = 5000) + public void testQueuedStreamCancellationDrainsWithoutOpeningIterator() throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + CountDownLatch release = new CountDownLatch(1); + CountDownLatch occupied = new CountDownLatch(1); + executor.execute(() -> { + occupied.countDown(); + try { + release.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(60); + StreamObserver output = mock(StreamObserver.class); + ScanStreamResponse response = ScanStreamResponse.of(output, wrapper, executor, config); + FutureTask request = new FutureTask<>(() -> { + response.onNext(ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build()); + return null; + }); + Thread caller = new Thread(request, "scan-shutdown-test"); + try { + assertTrue(occupied.await(1, TimeUnit.SECONDS)); + caller.start(); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + while (executor.getQueue().isEmpty() && System.nanoTime() < deadline) { + Thread.yield(); + } + assertEquals(1, executor.getQueue().size()); + response.onError(Status.CANCELLED.asRuntimeException()); + executor.shutdown(); + release.countDown(); + request.get(1, TimeUnit.SECONDS); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + verifyNoInteractions(wrapper); + assertEquals(2L, executor.getCompletedTaskCount()); + } finally { + response.onCompleted(); + release.countDown(); + executor.shutdownNow(); + caller.join(1000); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ScanShutdownTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ScanShutdownTest.java new file mode 100644 index 0000000000..0a1003ceb0 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ScanShutdownTest.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.hugegraph.store.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +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.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.hugegraph.rocksdb.access.ScanIterator; +import org.apache.hugegraph.store.grpc.common.ScanMethod; +import org.apache.hugegraph.store.grpc.stream.KvPageRes; +import org.apache.hugegraph.store.grpc.stream.ScanStreamReq; +import org.apache.hugegraph.store.node.AppConfig; +import org.apache.hugegraph.store.node.grpc.GrpcShutdownBarrier; +import org.apache.hugegraph.store.node.grpc.HgStoreStreamImpl; +import org.apache.hugegraph.store.node.grpc.HgStoreWrapperEx; +import org.apache.hugegraph.store.node.grpc.ScanStreamResponse; +import org.apache.hugegraph.store.node.grpc.query.AggregativeQueryService; +import org.apache.hugegraph.store.node.listener.ContextClosedListener; +import org.apache.hugegraph.store.node.task.TTLCleaner; +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import io.grpc.Status; +import io.grpc.stub.StreamObserver; + +public class ScanShutdownTest extends ScanTestSupport { + + @Test(timeout = 20000) + public void testOrdinaryScanCleanupFailureBlocksSpringDestruction() throws Exception { + for (int mode = 0; mode < 5; mode++) { + assertCleanupBlocksDestruction(mode); + } + } + + private static void assertCleanupBlocksDestruction(int mode) throws Exception { + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(4); + HgStoreWrapperEx wrapper = mock(HgStoreWrapperEx.class); + ScanIterator broken = mock(ScanIterator.class); + ScanIterator healthy = mock(ScanIterator.class); + IllegalStateException failure = new IllegalStateException("injected native release failure"); + doThrow(failure).when(broken).close(); + when(wrapper.scanAll(anyString(), anyString(), any(byte[].class))).thenReturn(broken, healthy); + HgStoreStreamImpl service = scanService(executor, wrapper); + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(); + AtomicBoolean destroyed = new AtomicBoolean(); + FutureTask closing = new FutureTask<>(() -> { + context.close(); + return null; + }); + Thread closer = new Thread(closing, "scan-failed-cleanup-context-close"); + try { + context.getBeanFactory().registerSingleton("storeStream", service); + context.getBeanFactory().registerSingleton("queryService", + mock(AggregativeQueryService.class)); + context.getBeanFactory().registerSingleton("cleaner", + mock(TTLCleaner.class)); + context.getDefaultListableBeanFactory().registerDisposableBean("database", () -> destroyed.set(true)); + context.register(ContextClosedListener.class, + GrpcShutdownBarrier.class); + context.refresh(); + for (int i = 0; i < 2; i++) { + ScanStreamReq request = ScanStreamReq.newBuilder().setMethod(ScanMethod.ALL) + .setPageSize(1).setLimit(10).build(); + switch (mode) { + case 0: + service.scan(mock(StreamObserver.class)).onNext(request); + break; + case 1: + service.scanBatch2(mock(StreamObserver.class)).onNext(batchRequest()); + break; + case 2: + service.scanBatch(mock(StreamObserver.class)).onNext(batchRequest()); + break; + case 3: + service.scanOneShot(request, mock(StreamObserver.class)); + break; + default: + service.scanBatchOneShot(batchRequest(), mock(StreamObserver.class)); + } + } + verify(broken, timeout(2000)).close(); + verify(healthy, timeout(2000)).close(); + closer.start(); + assertTrue(executor.awaitTermination(2, TimeUnit.SECONDS)); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); + Thread.State observed = closer.getState(); + while (observed != Thread.State.TIMED_WAITING && System.nanoTime() < deadline) { + Thread.yield(); + observed = closer.getState(); + } + // Assert the captured wait observation: a second state read can see a + // notification wakeup even though database destruction remains blocked. + assertEquals(Thread.State.TIMED_WAITING, observed); + assertFalse("terminated workers do not confirm native cleanup", closing.isDone()); + assertFalse(destroyed.get()); + verify(broken).close(); + verify(healthy).close(); + Map pending = scanRegistry(service); + assertEquals(1, pending.size()); + Object retained = pending.keySet().iterator().next(); + Field cleanupFailure = retained.getClass().getDeclaredField("cleanupFailure"); + cleanupFailure.setAccessible(true); + assertSame(failure, cleanupFailure.get(retained)); + } finally { + // Test-only teardown: production never clears a failed native release. + Map pending = scanRegistry(service); + synchronized (pending) { + pending.clear(); + pending.notifyAll(); + } + executor.shutdownNow(); + if (closer.isAlive()) { + closing.get(2, TimeUnit.SECONDS); + } + context.close(); + } + } + + + + +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ScanTestSupport.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ScanTestSupport.java new file mode 100644 index 0000000000..c3755a10d5 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ScanTestSupport.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.hugegraph.store.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.apache.hugegraph.store.grpc.common.Header; +import org.apache.hugegraph.store.grpc.common.ScanMethod; +import org.apache.hugegraph.store.grpc.stream.KvPageRes; +import org.apache.hugegraph.store.grpc.stream.ScanQueryRequest; +import org.apache.hugegraph.store.grpc.stream.ScanStreamBatchReq; +import org.apache.hugegraph.store.node.AppConfig; +import org.apache.hugegraph.store.node.grpc.HgStoreStreamImpl; +import org.apache.hugegraph.store.node.grpc.HgStoreWrapperEx; +import org.mockito.ArgumentCaptor; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; + +abstract class ScanTestSupport { + + static HgStoreStreamImpl scanService(ThreadPoolExecutor executor, + HgStoreWrapperEx wrapper) throws Exception { + HgStoreStreamImpl service = new HgStoreStreamImpl(); + AppConfig config = mock(AppConfig.class); + when(config.getServerWaitTime()).thenReturn(5); + for (String name : new String[]{"executor", "wrapper", "appConfig"}) { + Field field = HgStoreStreamImpl.class.getDeclaredField(name); + field.setAccessible(true); + field.set(service, name.equals("executor") ? executor : name.equals("wrapper") ? wrapper : config); + } + return service; + } + + static Map scanRegistry(HgStoreStreamImpl service) throws Exception { + Field scans = HgStoreStreamImpl.class.getDeclaredField("scans"); + scans.setAccessible(true); + return (Map) scans.get(service); + } + + static void awaitTimedWaiting(Thread thread) { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + Thread.State observed = thread.getState(); + while (observed != Thread.State.TIMED_WAITING && System.nanoTime() < deadline) { + Thread.yield(); + observed = thread.getState(); + } + assertEquals(Thread.State.TIMED_WAITING, observed); + } + + static ScanStreamBatchReq batchRequest() { + return ScanStreamBatchReq.newBuilder().setHeader(Header.newBuilder().setGraph("g")) + .setQueryRequest(ScanQueryRequest.newBuilder().setMethod(ScanMethod.ALL) + .setTable("t").setLimit(10) + .setPerKeyMax(Long.MAX_VALUE) + .setPageSize(1)).build(); + } + + static void assertCancelled(StreamObserver output) { + ArgumentCaptor failure = ArgumentCaptor.forClass(Throwable.class); + verify(output).onError(failure.capture()); + assertEquals(Status.Code.CANCELLED, Status.fromThrowable(failure.getValue()).getCode()); + } + + static void assertUnavailable(Runnable action) { + try { + action.run(); + fail("Scan admission must be closed"); + } catch (StatusRuntimeException e) { + assertEquals(Status.Code.UNAVAILABLE, e.getStatus().getCode()); + } + } +} diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ServerSuiteTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ServerSuiteTest.java index 2d115e78ed..082f9d30ba 100644 --- a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ServerSuiteTest.java +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/ServerSuiteTest.java @@ -17,7 +17,12 @@ package org.apache.hugegraph.store.service; +import org.apache.hugegraph.rocksdb.access.RocksDBScanIteratorCloseTest; import org.apache.hugegraph.store.business.OrderedMultiPartitionIteratorTest; +import org.apache.hugegraph.store.node.grpc.query.AggregativeQueryLifecycleTest; +import org.apache.hugegraph.store.node.grpc.query.AggregativeQueryShutdownTest; +import org.apache.hugegraph.store.node.grpc.query.UnaryQueryLifecycleTest; +import org.apache.hugegraph.store.node.grpc.BatchScanHalfCloseTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -25,7 +30,21 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ - OrderedMultiPartitionIteratorTest.class + RocksDBScanIteratorCloseTest.class, + OrderedMultiPartitionIteratorTest.class, + ScanContextCleanupTest.class, + ContextClosedListenerTest.class, + TTLCleanerCleanupTest.class, + GrpcShutdownBarrierTest.class, + PartitionEngineShutdownTest.class, + AggregativeQueryLifecycleTest.class, + AggregativeQueryShutdownTest.class, + UnaryQueryLifecycleTest.class, + BatchScanHalfCloseTest.class, + ScanLifecycleTest.class, + ScanShutdownTest.class, + GraphPartitionScanLifecycleTest.class, + GraphPartitionScanShutdownTest.class }) @Slf4j diff --git a/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/TTLCleanerCleanupTest.java b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/TTLCleanerCleanupTest.java new file mode 100644 index 0000000000..489eff98e9 --- /dev/null +++ b/hugegraph-store/hg-store-test/src/main/java/org/apache/hugegraph/store/service/TTLCleanerCleanupTest.java @@ -0,0 +1,374 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.store.service; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; + +import org.apache.commons.configuration2.MapConfiguration; +import org.apache.commons.lang3.tuple.Triple; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.config.OptionSpace; +import org.apache.hugegraph.rocksdb.access.RocksDBOptions; +import org.apache.hugegraph.rocksdb.access.RocksDBSession; +import org.apache.hugegraph.rocksdb.access.ScanIterator; +import org.apache.hugegraph.rocksdb.access.SessionOperator; +import org.apache.hugegraph.store.business.BusinessHandlerImpl; +import org.apache.hugegraph.store.business.InnerKeyCreator; +import org.apache.hugegraph.store.constant.HugeServerTables; +import org.apache.hugegraph.store.node.AppConfig; +import org.apache.hugegraph.store.node.grpc.GrpcShutdownBarrier; +import org.apache.hugegraph.store.node.grpc.HgStoreStreamImpl; +import org.apache.hugegraph.store.node.grpc.query.AggregativeQueryService; +import org.apache.hugegraph.store.node.listener.ContextClosedListener; +import org.apache.hugegraph.store.node.task.TTLCleaner; +import org.apache.hugegraph.store.node.task.ttl.TaskInfo; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.InOrder; +import org.rocksdb.RocksDB; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +public class TTLCleanerCleanupTest { + + @Rule + public TemporaryFolder directory = new TemporaryFolder(); + + @Test(timeout = 5000) + public void testCompletionWaitsForNativeClose() throws Exception { + TTLCleaner cleaner = newCleaner(); + CountDownLatch completed = new CountDownLatch(1); + CountDownLatch closing = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + ScanIterator scan = mock(ScanIterator.class); + doAnswer(invocation -> { + closing.countDown(); + release.await(); + return null; + }).when(scan).close(); + RocksDBSession session = mock(RocksDBSession.class); + FutureTask task = new FutureTask<>(task(cleaner, scan, session, completed), null); + Thread worker = new Thread(task, "test-ttl-close"); + worker.setDaemon(true); + try { + worker.start(); + assertTrue(closing.await(1, TimeUnit.SECONDS)); + assertEquals("TTL must not compact before native close finishes", 1L, + completed.getCount()); + release.countDown(); + task.get(1, TimeUnit.SECONDS); + assertEquals(0L, completed.getCount()); + InOrder order = inOrder(scan, session); + order.verify(scan).close(); + order.verify(session).close(); + cleaner.awaitCleanup(); + } finally { + release.countDown(); + cleaner.getScheduler().shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testFailedCloseStaysBlockedAfterInterruption() throws Exception { + assertFailedCloseStaysBlocked(true, false); + assertFailedCloseStaysBlocked(false, true); + assertFailedCloseStaysBlocked(true, true); + } + + private static void assertFailedCloseStaysBlocked(boolean scanFails, boolean sessionFails) throws Exception { + TTLCleaner cleaner = newCleaner(); + RuntimeException failure = new IllegalStateException("native release failed"); + ScanIterator scan = mock(ScanIterator.class); + RocksDBSession session = mock(RocksDBSession.class); + RuntimeException sessionFailure = scanFails ? new IllegalStateException("session release failed") : failure; + if (scanFails) { + doAnswer(invocation -> { throw failure; }).when(scan).close(); + } + if (sessionFails) { + doAnswer(invocation -> { throw sessionFailure; }).when(session).close(); + } + CountDownLatch completed = new CountDownLatch(1); + CountDownLatch awaiting = new CountDownLatch(1); + FutureTask cleanup = new FutureTask<>(() -> { + awaiting.countDown(); + cleaner.awaitCleanup(); + return Thread.currentThread().isInterrupted(); + }); + Thread shutdown = new Thread(cleanup, "test-ttl-cleanup-failure"); + shutdown.setDaemon(true); + Field field = TTLCleaner.class.getDeclaredField("cleanupFailure"); + field.setAccessible(true); + @SuppressWarnings("unchecked") + AtomicReference retained = (AtomicReference) field.get(cleaner); + try { + task(cleaner, scan, session, completed).run(); + InOrder order = inOrder(scan, session); + order.verify(scan).close(); + order.verify(session).close(); + if (scanFails && sessionFails) { + assertEquals(1, failure.getSuppressed().length); + assertSame(sessionFailure, failure.getSuppressed()[0]); + } + assertEquals(0L, completed.getCount()); + assertSame(failure, retained.get()); + shutdown.start(); + assertTrue(awaiting.await(1, TimeUnit.SECONDS)); + shutdown.interrupt(); + assertFalse("interruption cannot authorize native storage destruction", + cleanup.isDone()); + assertSame(failure, retained.get()); + } finally { + // Production never clears this failure. Release only this test's daemon waiter. + synchronized (retained) { + retained.set(null); + retained.notifyAll(); + } + shutdown.interrupt(); + assertTrue(cleanup.get(1, TimeUnit.SECONDS)); + cleaner.getScheduler().shutdownNow(); + } + } + + @Test(timeout = 5000) + public void testNativeSessionCloneAndScanReleaseAllReferences() throws Exception { + RocksDB.loadLibrary(); + OptionSpace.register("rocksdb", "org.apache.hugegraph.rocksdb.access.RocksDBOptions"); + RocksDBOptions.instance(); + HugeConfig config = new HugeConfig(new MapConfiguration( + Collections.singletonMap("rocksdb.write_buffer_size", "1048576"))); + TTLCleaner cleaner = newCleaner(); + RocksDBSession owner = new RocksDBSession(config, directory.newFolder("ttl-native").getAbsolutePath(), + "ttl-native", 0L); + RocksDBSession lease = owner.clone(); + try { + owner.checkTable(HugeServerTables.VERTEX_TABLE); + assertEquals(2, owner.getRefCount()); + BusinessHandlerImpl handler = mock(BusinessHandlerImpl.class); + when(handler.getSession(1)).thenReturn(lease); + InnerKeyCreator keys = mock(InnerKeyCreator.class); + when(keys.getStartKey(1, "graph")).thenReturn(new byte[]{0}); + when(keys.getEndKey(1, "graph")).thenReturn(new byte[]{1}); + when(handler.getKeyCreator()).thenReturn(keys); + CountDownLatch completed = new CountDownLatch(1); + task(cleaner, handler, completed).run(); + assertEquals(0L, completed.getCount()); + cleaner.awaitCleanup(); + assertEquals("TTL must release both the iterator lease and cloned session", 1, owner.getRefCount()); + RocksDB database = owner.getDB(); + assertTrue(database.isOwningHandle()); + owner.close(); + assertEquals(0, owner.getRefCount()); + assertNull(owner.getDB()); + assertFalse(database.isOwningHandle()); + } finally { + lease.close(); + owner.close(); + cleaner.getScheduler().shutdownNow(); + } + } + + @Test(timeout = 30000) + public void testActiveNativeTtlScanDrainsBeforeInterruptedSpringClose() throws Exception { + RocksDB.loadLibrary(); + OptionSpace.register("rocksdb", "org.apache.hugegraph.rocksdb.access.RocksDBOptions"); + RocksDBOptions.instance(); + HugeConfig config = new HugeConfig(new MapConfiguration( + Collections.singletonMap("rocksdb.write_buffer_size", "1048576"))); + RocksDBSession owner = new RocksDBSession(config, directory.newFolder("ttl-spring").getAbsolutePath(), + "ttl-spring", 0L); + RocksDBSession lease = owner.clone(); + RocksDB database = owner.getDB(); + TTLCleaner cleaner = spy(newCleaner()); + CountDownLatch judging = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + CountDownLatch workerInterrupted = new CountDownLatch(1); + CountDownLatch closeInterrupted = new CountDownLatch(1); + ThreadPoolExecutor workers = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>()) { + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + try { + return super.awaitTermination(timeout, unit); + } catch (InterruptedException e) { + closeInterrupted.countDown(); + throw e; + } + } + }; + Field executor = TTLCleaner.class.getDeclaredField("executor"); + executor.setAccessible(true); + executor.set(cleaner, workers); + Field registry = RocksDBSession.class.getDeclaredField("iteratorMap"); + registry.setAccessible(true); + Map iterators = (Map) registry.get(owner); + BiFunction judge = (key, value) -> { + assertArrayEquals(new byte[]{10}, key); + assertArrayEquals(new byte[]{42}, value); + judging.countDown(); + boolean interrupted = false; + while (release.getCount() != 0) { + try { + release.await(); + } catch (InterruptedException e) { + interrupted = true; + workerInterrupted.countDown(); + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + return false; + }; + doReturn(judge).when(cleaner).getJudge("graph", HugeServerTables.VERTEX_TABLE); + BusinessHandlerImpl handler = mock(BusinessHandlerImpl.class); + when(handler.getSession(1)).thenReturn(lease); + InnerKeyCreator keys = mock(InnerKeyCreator.class); + when(keys.getStartKey(1, "graph")).thenReturn(new byte[]{0}); + when(keys.getEndKey(1, "graph")).thenReturn(new byte[]{1}); + when(handler.getKeyCreator()).thenReturn(keys); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + CountDownLatch completed = new CountDownLatch(1); + Future worker = null; + FutureTask destruction = new FutureTask<>(() -> { + assertEquals(0L, completed.getCount()); + assertTrue(iterators.isEmpty()); + assertEquals(1, owner.getRefCount()); + assertTrue(database.isOwningHandle()); + owner.close(); + assertEquals(0, owner.getRefCount()); + assertFalse(database.isOwningHandle()); + return null; + }); + FutureTask closing = new FutureTask<>(() -> { + context.close(); + return Thread.currentThread().isInterrupted(); + }); + try { + owner.checkTable(HugeServerTables.VERTEX_TABLE); + SessionOperator operator = owner.sessionOp(); + operator.prepare(); + operator.put(HugeServerTables.VERTEX_TABLE, new byte[]{0, 0, 10, 0, 1}, new byte[]{42}); + operator.put(HugeServerTables.VERTEX_TABLE, new byte[]{0, 0, 11, 0, 1}, new byte[]{43}); + operator.commit(); + context.getBeanFactory().registerSingleton("storeStream", mock(HgStoreStreamImpl.class)); + context.getBeanFactory().registerSingleton("queryService", mock(AggregativeQueryService.class)); + context.getBeanFactory().registerSingleton("cleaner", cleaner); + context.getDefaultListableBeanFactory().registerDisposableBean("database", destruction::run); + context.register(ContextClosedListener.class, GrpcShutdownBarrier.class); + context.refresh(); + worker = workers.submit(task(cleaner, handler, completed)); + assertTrue(judging.await(2, TimeUnit.SECONDS)); + assertFalse("the nonempty scan must still own its native iterator", iterators.isEmpty()); + Thread closeThread = new Thread(closing, "test-active-ttl-spring-close"); + closeThread.start(); + assertTrue(workerInterrupted.await(2, TimeUnit.SECONDS)); + closeThread.interrupt(); + assertTrue(closeInterrupted.await(2, TimeUnit.SECONDS)); + assertFalse(closing.isDone()); + assertFalse(destruction.isDone()); + assertEquals(1L, completed.getCount()); + assertFalse(iterators.isEmpty()); + assertTrue(database.isOwningHandle()); + release.countDown(); + worker.get(2, TimeUnit.SECONDS); + assertTrue("Spring close must preserve interruption", closing.get(2, TimeUnit.SECONDS)); + destruction.get(2, TimeUnit.SECONDS); + assertTrue(workers.isTerminated()); + assertNull(owner.getDB()); + } finally { + release.countDown(); + workers.shutdownNow(); + try { + if (worker != null) { + worker.get(2, TimeUnit.SECONDS); + } + } finally { + try { + context.close(); + } finally { + lease.close(); + owner.close(); + cleaner.getScheduler().shutdownNow(); + } + } + } + } + + private static TTLCleaner newCleaner() { + AppConfig config = mock(AppConfig.class); + AppConfig.JobConfig job = mock(AppConfig.JobConfig.class); + when(config.getJobConfig()).thenReturn(job); + when(job.getStartTime()).thenReturn(19); + when(job.getBatchSize()).thenReturn(128); + return new TTLCleaner(config); + } + + private static Runnable task(TTLCleaner cleaner, ScanIterator scan, RocksDBSession session, + CountDownLatch completed) throws Exception { + BusinessHandlerImpl handler = mock(BusinessHandlerImpl.class); + SessionOperator operator = mock(SessionOperator.class); + when(handler.getSession(1)).thenReturn(session); + when(handler.getKeyCreator()).thenReturn(mock(InnerKeyCreator.class)); + when(session.sessionOp()).thenReturn(operator); + when(operator.scan(anyString(), any(), any(), anyInt())).thenReturn(scan); + return task(cleaner, handler, completed); + } + + private static Runnable task(TTLCleaner cleaner, BusinessHandlerImpl handler, + CountDownLatch completed) throws Exception { + TaskInfo info = mock(TaskInfo.class); + when(info.getTableCounter()).thenReturn(new ConcurrentHashMap<>( + Collections.singletonMap(HugeServerTables.VERTEX_TABLE, new AtomicLong()))); + Method method = TTLCleaner.class.getDeclaredMethod("getTask", BusinessHandlerImpl.class, + CountDownLatch.class, Triple.class, Map.class, Map.class); + method.setAccessible(true); + return (Runnable) method.invoke(cleaner, handler, completed, + Triple.of(1, "graph", HugeServerTables.VERTEX_TABLE), + Collections.singletonMap("graph", info), + Collections.singletonMap(1, new AtomicLong())); + } +} diff --git a/hugegraph-store/pom.xml b/hugegraph-store/pom.xml index a8212b70db..edb27c3966 100644 --- a/hugegraph-store/pom.xml +++ b/hugegraph-store/pom.xml @@ -44,7 +44,7 @@ - 2.15.0 + 2.18.0 apache-${release.name}-store-${project.version} @@ -98,7 +98,6 @@ org.jacoco jacoco-maven-plugin - 0.8.8 true @@ -171,7 +170,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 store-client-test @@ -195,7 +193,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 store-core-test @@ -219,7 +216,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 store-common-test @@ -243,7 +239,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 store-rocksdb-test @@ -267,7 +262,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 store-server-test @@ -291,7 +285,6 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20 store-raftcore-test diff --git a/hugegraph-struct/README.md b/hugegraph-struct/README.md index f004bc0c23..12b29d33c2 100644 --- a/hugegraph-struct/README.md +++ b/hugegraph-struct/README.md @@ -1,209 +1,50 @@ # HugeGraph-Struct -### Overview +`hugegraph-struct` owns the shared foundations used by Server and Store: IDs, schema metadata, base graph elements, query conditions, type codes and binary encoding. Core builds the graph engine on these implementations instead of maintaining a second copy. -**hugegraph-struct** is a foundational data structures module that defines the core abstractions and type definitions shared across HugeGraph's distributed components. It serves as the "data contract layer" enabling type-safe communication between hugegraph-pd (Placement Driver), hugegraph-store (distributed storage), and hugegraph-server (graph engine). +## Responsibilities -**Key Characteristics**: -- Pure data structure definitions without business logic -- Lightweight and stateless (no `HugeGraph` instance dependencies) -- Shared type system for distributed RPC communication -- Binary serialization for efficient storage and network transmission +| Owner | Responsibility | +|-------|----------------| +| `hugegraph-struct` | Shared models, queries, codecs, index building and analyzers | +| `hugegraph-common` | General utilities, shared JWT verification/signing and RPC configuration interfaces | +| `hugegraph-core` | Graph/TinkerPop adapters, transactions, traversal, backend execution and jobs | +| `hg-store-core` | Store execution and schema metadata access through PD | +| PD service | Placement/metadata service and its own authentication/transport adaptation | -### Why hugegraph-struct? +Struct is not a DTO-only module or dependency-free library. It retains the libraries required by shared codecs, collections and analyzers, including TinkerPop interfaces and shaded Kryo compatibility. It must not depend on core or own PD connections, listeners and caches. -#### The Problem +`HugeGraphSupplier` lets shared operations obtain schema without a concrete `HugeGraph`. It also supplies naming, configuration and time. Store's `SchemaGraph` and `SchemaDriver` live in Store. The caller owns their PD lifecycle. -Originally, all data structures and graph engine logic resided in `hugegraph-server/hugegraph-core`. As HugeGraph evolved toward a distributed architecture, this created several challenges: +Core's `HugeVertex`/`HugeEdge` wrap shared `BaseVertex`/`BaseEdge` state and add engine behavior. `BaseVertex` owns adjacency; core only caches wrapper identities. Shared `Index` replaces `HugeIndex`, including index IDs and values. Index/OLAP selection uses caller-supplied candidates and sinks; core still executes transactions and backend updates. Engine serializers retain backend adaptation and delegate shared encoding. These adapters have different responsibilities from the shared implementation; a second algorithm or state collection is not an adapter. -1. **Tight Coupling**: PD and Store components needed schema definitions but not the entire graph engine -2. **Circular Dependencies**: Distributed components couldn't share types without pulling in heavy dependencies -3. **Build Inefficiency**: Changes to core required rebuilding all dependent modules -4. **Large Dependencies**: PD/Store had to depend on Jersey, JRaft, K8s client, and other server-specific libraries +## Packages and migration -#### The Solution +| Package under `org.apache.hugegraph` | Content | +|------------------------------------|---------| +| `struct.schema` | Schema metadata | +| `structure` | Base elements, indexes and shared index builder | +| `id`, `query`, `serializer` | IDs, query conditions and encoding | +| `type`, `backend` | Type codes and shared backend values | +| `analyzer`, `util`, `exception` | Shared analysis, codec helpers and exceptions | -We extracted **stateless data structures** from `hugegraph-core` into a separate `hugegraph-struct` module: +The 1.8.0 Java API migration removes the old core copies. For example, schema metadata moves from `org.apache.hugegraph.schema` to `org.apache.hugegraph.struct.schema`; engine schema mutation builders remain in core. Applications using internal Java APIs must update imports and compile against a consistent set of artifacts. -``` -Before (Monolithic): -hugegraph-server/hugegraph-core (everything together) - ├─ Data structures (schema, types, IDs) - ├─ Graph engine (traversal, optimization) - ├─ Transactions (GraphTransaction, SchemaTransaction) - ├─ Storage backends (memory, raft, cache) - └─ Business logic (jobs, tasks, auth) - -After (Modular): -hugegraph-struct (shared foundation) - ├─ Schema definitions (VertexLabel, EdgeLabel, PropertyKey, IndexLabel) - ├─ Type system (HugeType, DataType, IdStrategy) - ├─ Data structures (BaseVertex, BaseEdge, BaseProperty) - ├─ Serialization (BytesBuffer, BinarySerializer) - ├─ Query abstractions (Query, ConditionQuery, Aggregate) - └─ Utilities (ID generation, text analyzers, exceptions) - -hugegraph-core (graph engine only) - ├─ Depends on hugegraph-struct - ├─ Implements graph engine logic - ├─ Manages transactions and storage - └─ Provides TinkerPop API -``` - -### Module Responsibilities +See the [shared-foundation migration guide](../docs/shared-foundation-migration.md) for ownership, upgrade requirements and compatibility checks. Existing client REST DTOs need not be replaced merely because their names resemble these types. -| Module | Purpose | Dependencies | -|--------|---------|--------------| -| **hugegraph-struct** | Shared data structures, type definitions, serialization | Minimal (Guava, TinkerPop, serialization libs) | -| **hugegraph-core** | Graph engine, traversal, transactions, storage abstraction | hugegraph-struct + heavy libs (Jersey, JRaft, K8s) | -| **hugegraph-pd** | Metadata coordination, service discovery | hugegraph-struct only | -| **hugegraph-store** | Distributed storage with Raft | hugegraph-struct only | +## Development and validation -### Dependency Architecture - -``` - hugegraph-struct (foundational) - ↑ - ┌──────────────────┼──────────────────┐ - │ │ │ - hugegraph-pd hugegraph-store hugegraph-core - │ │ │ - └──────────────────┼──────────────────┘ - ↓ - hugegraph-server (REST API) -``` +Run from the repository root: -**Build Order**: ```bash -# 1. Build struct first (required dependency) mvn install -pl hugegraph-struct -am -DskipTests - -# 2. Then build dependent modules -mvn install -pl hugegraph-pd -am -DskipTests -mvn install -pl hugegraph-store -am -DskipTests -mvn install -pl hugegraph-server -am -DskipTests +mvn test -pl hugegraph-struct -am ``` -### Migration Plan - -**Current Status (Transition Period)**: - -Both `hugegraph-struct` and `hugegraph-core` contain similar data structures for backward compatibility. This is a **temporary state** during the migration period. - -**Future Direction**: - -- ✅ **hugegraph-struct**: Will become the **single source of truth** for all data structure definitions -- ⚠️ **hugegraph-core**: Data structure definitions will be **gradually removed** and replaced with references to hugegraph-struct -- 🎯 **End Goal**: hugegraph-core will only contain graph engine logic and depend on hugegraph-struct for all type definitions - -**Migration Strategy**: - -1. **Phase 1 (Current)**: Both modules coexist; new features use struct -2. **Phase 2 (In Progress)**: Gradually migrate core's data structures to import from struct -3. **Phase 3 (Future)**: Remove duplicate definitions from core completely - -**Example Migration**: - -```java -// OLD (hugegraph-core) -import org.apache.hugegraph.schema.SchemaElement; // ❌ Will be deprecated - -// NEW (hugegraph-struct) -import org.apache.hugegraph.struct.schema.SchemaElement; // ✅ Use this -``` - -### Developer Guide - -#### When to Use hugegraph-struct - -Use struct when: -- Building distributed components (PD, Store) -- Defining data transfer objects (DTOs) for RPC -- Implementing serialization/deserialization logic -- Working with type definitions, schema elements, or IDs -- Creating shared utilities needed across modules - -#### When to Use hugegraph-core - -Use core when: -- Implementing graph engine features -- Working with TinkerPop API (Gremlin traversal) -- Managing transactions or backend storage -- Implementing graph algorithms or jobs -- Building server-side business logic - -#### Adding New Data Structures - -**Rule**: All new shared data structures should go into `hugegraph-struct`, not `hugegraph-core`. - -Example: -```java -// ✅ Correct: Add to hugegraph-struct/src/main/java/org/apache/hugegraph/struct/ -public class NewSchemaType extends SchemaElement { - // Pure data structure, no HugeGraph dependency -} - -// ❌ Wrong: Don't add to hugegraph-core unless it's graph engine logic -``` - -#### Modifying Existing Structures - -If you need to modify a data structure: - -1. **Check if it exists in struct**: Modify the struct version -2. **If it only exists in core**: Consider migrating it to struct first -3. **Update serialization**: Ensure binary compatibility or provide migration - -### Package Structure - -``` -org.apache.hugegraph/ -├── struct/schema/ # Schema definitions (VertexLabel, EdgeLabel, etc.) -├── structure/ # Graph elements (BaseVertex, BaseEdge, BaseProperty) -├── type/ # Type system (HugeType, DataType, IdStrategy) -├── id/ # ID generation and management -├── serializer/ # Binary serialization (BytesBuffer, BinarySerializer) -├── query/ # Query abstractions (Query, ConditionQuery, Aggregate) -├── analyzer/ # Text analyzers (8 Chinese NLP implementations) -├── auth/ # Auth utilities (JWT, constants) -├── backend/ # Backend abstractions (BinaryId, BackendColumn, Shard) -├── options/ # Configuration options -├── util/ # Utilities (encoding, compression, collections) -└── exception/ # Exception hierarchy -``` - -### Key Design Principles - -1. **Stateless**: No `HugeGraph` instance dependencies in struct -2. **Minimal Dependencies**: Only essential libraries (no Jersey, JRaft, K8s) -3. **Serialization-Friendly**: All structures support binary serialization -4. **Type Safety**: Strong typing for distributed RPC communication -5. **Backward Compatible**: Careful versioning to avoid breaking changes - -### Building and Testing - -```bash -# Build struct module -mvn clean install -DskipTests - -# Build with tests (when tests are added) -mvn clean install - -# From parent directory -cd /path/to/hugegraph -mvn install -pl hugegraph-struct -am -DskipTests -``` - -### Contributing - -When contributing to hugegraph-struct: +The first command builds without tests. Tests exist under `src/test/java`; a successful build alone does not prove compatibility or distributed startup. See [BUILDING](../docs/BUILDING.md) and module CI for service prerequisites. -1. **No Business Logic**: Keep it pure data structures -2. **No Graph Instances**: Avoid `HugeGraph graph` fields -3. **Document Changes**: Update AGENTS.md if adding new packages -4. **Binary Compatibility**: Consider serialization impact -5. **Minimal Dependencies**: Justify any new dependency additions +Change shared behavior once, in its canonical owner, and migrate consumers. Changes to type codes, ID encoding, binary layout, query JSON or classification must be checked against historical fixtures and Server/Store readers. Do not restore deleted copies to resolve an import or dependency problem. -### License +## License -Apache License 2.0 - See [LICENSE](../LICENSE) file for details. +Apache License 2.0; see [LICENSE](../LICENSE). diff --git a/hugegraph-struct/pom.xml b/hugegraph-struct/pom.xml index b88d0ae204..b283fa9fb0 100644 --- a/hugegraph-struct/pom.xml +++ b/hugegraph-struct/pom.xml @@ -30,30 +30,42 @@ - 11 - 11 UTF-8 25.1-jre - 3.5.1 + 3.8.1 + 8.11.2 - org.apache.hugegraph - hg-pd-client - ${project.version} - - - - jakarta.ws.rs - jakarta.ws.rs-api - 3.0.0 + org.apache.tinkerpop + gremlin-core + ${tinkerpop.version} + + + org.apache.commons + commons-configuration2 + + org.apache.tinkerpop gremlin-test ${tinkerpop.version} + test + + + com.carrotsearch + hppc + 0.7.1 + + + + junit + junit + 4.13.2 + test @@ -78,15 +90,16 @@ guava ${guava.version} - - - - - + + + org.apache.commons + commons-lang3 + 3.18.0 + org.apache.tinkerpop gremlin-shaded - 3.5.1 + ${tinkerpop.version} org.mindrot @@ -125,6 +138,8 @@ org.apdplat word 1.3 + + provided ch.qos.logback @@ -144,34 +159,22 @@ com.hankcs hanlp - portable-1.5.0 + portable-1.8.3 org.apache.lucene lucene-analyzers-smartcn - 7.4.0 + ${lucene.version} org.apache.lucene - lucene-core - 7.4.0 - - - io.jsonwebtoken - jjwt-api - 0.11.2 - - - io.jsonwebtoken - jjwt-impl - 0.11.2 - runtime + lucene-analyzers-common + ${lucene.version} - io.jsonwebtoken - jjwt-jackson - 0.11.2 - runtime + org.apache.lucene + lucene-core + ${lucene.version} com.huaban @@ -181,7 +184,7 @@ org.lionsoul jcseg-core - 2.2.0 + 2.6.2 com.chenlb.mmseg4j @@ -192,8 +195,47 @@ com.janeluo ikanalyzer 2012_u6 + + + org.apache.lucene + lucene-core + + + org.apache.lucene + lucene-analyzers-common + + + + + + org.codehaus.mojo + flatten-maven-plugin + 1.2.7 + + true + resolveCiFriendliesOnly + + + + flatten + process-resources + + flatten + + + + flatten.clean + clean + + clean + + + + + + diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/HugeGraphSupplier.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/HugeGraphSupplier.java index 2ac547db8a..97eb045eed 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/HugeGraphSupplier.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/HugeGraphSupplier.java @@ -19,9 +19,14 @@ package org.apache.hugegraph; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; import java.util.Collection; import java.util.List; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.hugegraph.struct.schema.SchemaElement; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.struct.schema.EdgeLabel; @@ -31,8 +36,7 @@ import org.apache.hugegraph.util.DateUtil; /** - * Actually, it would be better if this interface be called - * "HugeGraphSchemaSupplier". + * Provides shared schema, configuration and time access without requiring a graph engine. */ public interface HugeGraphSupplier { @@ -70,9 +74,33 @@ default EdgeLabel edgeLabelOrNone(Id id) { String name(); + default String spaceGraphName() { + return this.name(); + } + HugeConfig configuration(); default long now() { return DateUtil.now().getTime(); } + + + default List mapElId2Name(Collection ids) { + List names = new ArrayList<>(ids.size()); + for (Id id : ids) { + SchemaElement schema = this.edgeLabel(id); + names.add(schema.name()); + } + return names; + } + + default Set> mapPairId2Name( + Set> pairs) { + Set> results = new HashSet<>(pairs.size()); + for (Pair pair : pairs) { + results.add(Pair.of(this.vertexLabel(pair.getLeft()).name(), + this.vertexLabel(pair.getRight()).name())); + } + return results; + } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/Analyzer.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/Analyzer.java index 4edd2ffa9b..8e7537e072 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/Analyzer.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/Analyzer.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.analyzer; @@ -23,5 +21,5 @@ public interface Analyzer { - public Set segment(String text); + Set segment(String text); } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/AnalyzerFactory.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/AnalyzerFactory.java index bff18ab7b0..61568c1774 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/AnalyzerFactory.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/AnalyzerFactory.java @@ -1,43 +1,46 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.analyzer; - -import org.apache.hugegraph.exception.HugeException; - import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.apache.hugegraph.config.ConfigException; +import org.apache.hugegraph.exception.HugeException; + public class AnalyzerFactory { - private static Map> analyzers; + private static final Map> ANALYZERS; static { - analyzers = new ConcurrentHashMap<>(); + ANALYZERS = new ConcurrentHashMap<>(); } public static Analyzer analyzer(String name, String mode) { name = name.toLowerCase(); switch (name) { case "word": - return new WordAnalyzer(mode); + try { + return new WordAnalyzer(mode); + } catch (NoClassDefFoundError e) { + throw new ConfigException("The word analyzer requires the optional " + + "org.apdplat:word:1.3 dependency on the runtime classpath", e); + } case "ansj": return new AnsjAnalyzer(mode); case "hanlp": @@ -58,7 +61,7 @@ public static Analyzer analyzer(String name, String mode) { } private static Analyzer customizedAnalyzer(String name, String mode) { - Class clazz = analyzers.get(name); + Class clazz = ANALYZERS.get(name); if (clazz == null) { throw new HugeException("Not exists analyzer: %s", name); } @@ -68,12 +71,12 @@ private static Analyzer customizedAnalyzer(String name, String mode) { return clazz.getConstructor(String.class).newInstance(mode); } catch (Exception e) { throw new HugeException( - "Failed to construct analyzer '%s' with mode '%s'", - e, name, mode); + "Failed to construct analyzer '%s' with mode '%s'", + e, name, mode); } } - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({"rawtypes", "unchecked"}) public static void register(String name, String classPath) { ClassLoader classLoader = AnalyzerFactory.class.getClassLoader(); Class clazz; @@ -91,12 +94,12 @@ public static void register(String name, String classPath) { } // Check exists - if (analyzers.containsKey(name)) { + if (ANALYZERS.containsKey(name)) { throw new HugeException("Exists analyzer: %s(%s)", - name, analyzers.get(name).getName()); + name, ANALYZERS.get(name).getName()); } // Register class - analyzers.put(name, (Class) clazz); + ANALYZERS.put(name, (Class) clazz); } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/AnsjAnalyzer.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/AnsjAnalyzer.java index 3f041d31f8..2f857cd171 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/AnsjAnalyzer.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/AnsjAnalyzer.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.analyzer; @@ -34,7 +32,7 @@ import com.google.common.collect.ImmutableList; /** - * Reference from https://my.oschina.net/apdplat/blog/412921 + * Reference from https://github.com/NLPchina/ansj_seg/wiki */ public class AnsjAnalyzer implements Analyzer { @@ -45,20 +43,20 @@ public class AnsjAnalyzer implements Analyzer { "NlpAnalysis" ); - private String analysis; + private final String analysis; public AnsjAnalyzer(String mode) { if (!SUPPORT_MODES.contains(mode)) { throw new ConfigException( - "Unsupported segment mode '%s' for ansj analyzer, " + - "the available values are %s", mode, SUPPORT_MODES); + "Unsupported segment mode '%s' for ansj analyzer, " + + "the available values are %s", mode, SUPPORT_MODES); } this.analysis = mode; } @Override public Set segment(String text) { - Result terms = null; + Result terms; switch (this.analysis) { case "BaseAnalysis": terms = BaseAnalysis.parse(text); @@ -74,7 +72,7 @@ public Set segment(String text) { break; default: throw new AssertionError(String.format( - "Unsupported segment mode '%s'", this.analysis)); + "Unsupported segment mode '%s'", this.analysis)); } assert terms != null; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/HanLPAnalyzer.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/HanLPAnalyzer.java index b8175e400c..8b5fa6aa0a 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/HanLPAnalyzer.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/HanLPAnalyzer.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.analyzer; @@ -22,6 +20,8 @@ import java.util.List; import java.util.Set; +import org.apache.hugegraph.config.ConfigException; +import org.apache.hugegraph.util.InsertionOrderUtil; import com.google.common.collect.ImmutableList; import com.hankcs.hanlp.seg.Dijkstra.DijkstraSegment; @@ -33,23 +33,20 @@ import com.hankcs.hanlp.tokenizer.SpeedTokenizer; import com.hankcs.hanlp.tokenizer.StandardTokenizer; -import org.apache.hugegraph.config.ConfigException; -import org.apache.hugegraph.util.InsertionOrderUtil; - /** - * Reference from https://my.oschina.net/apdplat/blog/412921 + * Reference from https://github.com/hankcs/HanLP/wiki */ public class HanLPAnalyzer implements Analyzer { public static final List SUPPORT_MODES = - ImmutableList.builder() - .add("standard") - .add("nlp") - .add("index") - .add("nShort") - .add("shortest") - .add("speed") - .build(); + ImmutableList.builder() + .add("standard") + .add("nlp") + .add("index") + .add("nShort") + .add("shortest") + .add("speed") + .build(); private static final Segment N_SHORT_SEGMENT = new NShortSegment().enableCustomDictionary(false) @@ -60,20 +57,20 @@ public class HanLPAnalyzer implements Analyzer { .enablePlaceRecognize(true) .enableOrganizationRecognize(true); - private String tokenizer; + private final String tokenizer; public HanLPAnalyzer(String mode) { if (!SUPPORT_MODES.contains(mode)) { throw new ConfigException( - "Unsupported segment mode '%s' for hanlp analyzer, " + - "the available values are %s", mode, SUPPORT_MODES); + "Unsupported segment mode '%s' for hanlp analyzer, " + + "the available values are %s", mode, SUPPORT_MODES); } this.tokenizer = mode; } @Override public Set segment(String text) { - List terms = null; + List terms; switch (this.tokenizer) { case "standard": terms = StandardTokenizer.segment(text); @@ -95,7 +92,7 @@ public Set segment(String text) { break; default: throw new AssertionError(String.format( - "Unsupported segment mode '%s'", this.tokenizer)); + "Unsupported segment mode '%s'", this.tokenizer)); } assert terms != null; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/IKAnalyzer.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/IKAnalyzer.java index a938e8e01f..bf97437fdd 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/IKAnalyzer.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/IKAnalyzer.java @@ -1,38 +1,36 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.analyzer; -import com.google.common.collect.ImmutableList; +import java.io.StringReader; +import java.util.List; +import java.util.Set; -import org.apache.hugegraph.config.ConfigException; import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.config.ConfigException; import org.apache.hugegraph.util.InsertionOrderUtil; import org.wltea.analyzer.core.IKSegmenter; import org.wltea.analyzer.core.Lexeme; -import java.io.StringReader; -import java.util.List; -import java.util.Set; +import com.google.common.collect.ImmutableList; /** - * Reference from https://my.oschina.net/apdplat/blog/412921 + * Reference from https://github.com/yyn0210/IKAnalyzer */ public class IKAnalyzer implements Analyzer { @@ -41,26 +39,24 @@ public class IKAnalyzer implements Analyzer { "max_word" ); - private boolean smartSegMode; - private final IKSegmenter ik; + private final boolean smartSegMode; public IKAnalyzer(String mode) { if (!SUPPORT_MODES.contains(mode)) { throw new ConfigException( - "Unsupported segment mode '%s' for ikanalyzer, " + - "the available values are %s", mode, SUPPORT_MODES); + "Unsupported segment mode '%s' for ikanalyzer, " + + "the available values are %s", mode, SUPPORT_MODES); } this.smartSegMode = SUPPORT_MODES.get(0).equals(mode); - this.ik = new IKSegmenter(new StringReader(""), - this.smartSegMode); } @Override public Set segment(String text) { Set result = InsertionOrderUtil.newSet(); - ik.reset(new StringReader(text)); + IKSegmenter ik = new IKSegmenter(new StringReader(text), + this.smartSegMode); try { - Lexeme word = null; + Lexeme word; while ((word = ik.next()) != null) { result.add(word.getLexemeText()); } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/JcsegAnalyzer.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/JcsegAnalyzer.java index 0a69af8384..7e7b9321bf 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/JcsegAnalyzer.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/JcsegAnalyzer.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.analyzer; @@ -23,19 +21,19 @@ import java.util.List; import java.util.Set; -import org.apache.hugegraph.config.ConfigException; import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.config.ConfigException; import org.apache.hugegraph.util.InsertionOrderUtil; -import org.lionsoul.jcseg.tokenizer.core.ADictionary; -import org.lionsoul.jcseg.tokenizer.core.DictionaryFactory; -import org.lionsoul.jcseg.tokenizer.core.ISegment; -import org.lionsoul.jcseg.tokenizer.core.IWord; -import org.lionsoul.jcseg.tokenizer.core.JcsegTaskConfig; -import org.lionsoul.jcseg.tokenizer.core.SegmentFactory; +import org.lionsoul.jcseg.ISegment; +import org.lionsoul.jcseg.IWord; +import org.lionsoul.jcseg.dic.ADictionary; +import org.lionsoul.jcseg.dic.DictionaryFactory; +import org.lionsoul.jcseg.segmenter.SegmenterConfig; + import com.google.common.collect.ImmutableList; /** - * Reference from https://my.oschina.net/apdplat/blog/412921 + * Reference from https://github.com/lionsoul2014/jcseg */ public class JcsegAnalyzer implements Analyzer { @@ -44,29 +42,34 @@ public class JcsegAnalyzer implements Analyzer { "Complex" ); - private static final JcsegTaskConfig CONFIG = new JcsegTaskConfig(); - private static final ADictionary DIC = - DictionaryFactory.createDefaultDictionary(new JcsegTaskConfig()); + private static final SegmenterConfig CONFIG = new SegmenterConfig(); + private static final ADictionary DIC = DictionaryFactory.createDefaultDictionary(CONFIG); - private int segMode; + private final ISegment.Type type; public JcsegAnalyzer(String mode) { if (!SUPPORT_MODES.contains(mode)) { throw new ConfigException( - "Unsupported segment mode '%s' for jcseg analyzer, " + - "the available values are %s", mode, SUPPORT_MODES); + "Unsupported segment mode '%s' for jcseg analyzer, " + + "the available values are %s", mode, SUPPORT_MODES); + } + + if ("Simple".equals(mode)) { + this.type = ISegment.SIMPLE; + } else { + this.type = ISegment.COMPLEX; } - this.segMode = SUPPORT_MODES.indexOf(mode) + 1; } @Override public Set segment(String text) { Set result = InsertionOrderUtil.newSet(); try { - Object[] args = new Object[]{new StringReader(text), CONFIG, DIC}; - ISegment seg = SegmentFactory.createJcseg(this.segMode, args); - IWord word = null; - while ((word = seg.next()) != null) { + ISegment segmentor = this.type.factory.create(CONFIG, DIC); + segmentor.reset(new StringReader(text)); + + IWord word; + while ((word = segmentor.next()) != null) { result.add(word.getValue()); } } catch (Exception e) { diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/JiebaAnalyzer.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/JiebaAnalyzer.java index 70cae33268..7c280a50a1 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/JiebaAnalyzer.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/JiebaAnalyzer.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.analyzer; @@ -30,7 +28,7 @@ import com.huaban.analysis.jieba.SegToken; /** - * Reference from https://my.oschina.net/apdplat/blog/412921 + * Reference from https://github.com/huaban/jieba-analysis */ public class JiebaAnalyzer implements Analyzer { @@ -41,13 +39,13 @@ public class JiebaAnalyzer implements Analyzer { private static final JiebaSegmenter JIEBA_SEGMENTER = new JiebaSegmenter(); - private JiebaSegmenter.SegMode segMode; + private final JiebaSegmenter.SegMode segMode; public JiebaAnalyzer(String mode) { if (!SUPPORT_MODES.contains(mode)) { throw new ConfigException( - "Unsupported segment mode '%s' for jieba analyzer, " + - "the available values are %s", mode, SUPPORT_MODES); + "Unsupported segment mode '%s' for jieba analyzer, " + + "the available values are %s", mode, SUPPORT_MODES); } this.segMode = JiebaSegmenter.SegMode.valueOf(mode); } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/MMSeg4JAnalyzer.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/MMSeg4JAnalyzer.java index 3316582f73..a4415911fa 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/MMSeg4JAnalyzer.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/MMSeg4JAnalyzer.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.analyzer; @@ -23,10 +21,10 @@ import java.util.List; import java.util.Set; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.config.ConfigException; import org.apache.hugegraph.util.InsertionOrderUtil; -import org.apache.hugegraph.exception.HugeException; import com.chenlb.mmseg4j.ComplexSeg; import com.chenlb.mmseg4j.Dictionary; import com.chenlb.mmseg4j.MMSeg; @@ -37,7 +35,7 @@ import com.google.common.collect.ImmutableList; /** - * Reference from https://my.oschina.net/apdplat/blog/412921 + * Reference from https://github.com/chenlb/mmseg4j-core */ public class MMSeg4JAnalyzer implements Analyzer { @@ -49,13 +47,13 @@ public class MMSeg4JAnalyzer implements Analyzer { private static final Dictionary DIC = Dictionary.getInstance(); - private Seg seg; + private final Seg seg; public MMSeg4JAnalyzer(String mode) { if (!SUPPORT_MODES.contains(mode)) { throw new ConfigException( - "Unsupported segment mode '%s' for mmseg4j analyzer, " + - "the available values are %s", mode, SUPPORT_MODES); + "Unsupported segment mode '%s' for mmseg4j analyzer, " + + "the available values are %s", mode, SUPPORT_MODES); } int index = SUPPORT_MODES.indexOf(mode); switch (index) { @@ -70,7 +68,7 @@ public MMSeg4JAnalyzer(String mode) { break; default: throw new AssertionError(String.format( - "Unsupported segment mode '%s'", this.seg)); + "Unsupported segment mode '%s'", mode)); } } @@ -79,7 +77,7 @@ public Set segment(String text) { Set result = InsertionOrderUtil.newSet(); MMSeg mmSeg = new MMSeg(new StringReader(text), this.seg); try { - Word word = null; + Word word; while ((word = mmSeg.next()) != null) { result.add(word.getString()); } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/SmartCNAnalyzer.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/SmartCNAnalyzer.java index 34c0ea2fba..7b00aa59dd 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/SmartCNAnalyzer.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/analyzer/SmartCNAnalyzer.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.analyzer; @@ -24,23 +22,23 @@ import java.util.List; import java.util.Set; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.util.InsertionOrderUtil; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; -import org.apache.hugegraph.exception.HugeException; import com.google.common.collect.ImmutableList; /** - * Reference from https://my.oschina.net/apdplat/blog/412921 + * Reference from https://lucene.apache.org/core/8_11_2/analyzers-smartcn/index.html */ public class SmartCNAnalyzer implements Analyzer { public static final List SUPPORT_MODES = ImmutableList.of(); private static final SmartChineseAnalyzer ANALYZER = - new SmartChineseAnalyzer(); + new SmartChineseAnalyzer(); public SmartCNAnalyzer(String mode) { // pass @@ -52,7 +50,7 @@ public Set segment(String text) { Reader reader = new StringReader(text); try (TokenStream tokenStream = ANALYZER.tokenStream("text", reader)) { tokenStream.reset(); - CharTermAttribute term = null; + CharTermAttribute term; while (tokenStream.incrementToken()) { term = tokenStream.getAttribute(CharTermAttribute.class); result.add(term.toString()); diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/auth/AuthConstant.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/auth/AuthConstant.java deleted file mode 100644 index 97bd1a0e1c..0000000000 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/auth/AuthConstant.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017 HugeGraph Authors - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -package org.apache.hugegraph.auth; - -public interface AuthConstant { - - /* - * Fields in token - */ - String TOKEN_USER_NAME = "user_name"; - String TOKEN_USER_ID = "user_id"; - String TOKEN_USER_PASSWORD = "user_password"; -} diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/auth/TokenGenerator.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/auth/TokenGenerator.java deleted file mode 100644 index f803894fc2..0000000000 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/auth/TokenGenerator.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2017 HugeGraph Authors - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -package org.apache.hugegraph.auth; - -import org.apache.hugegraph.options.AuthOptions; - -import io.jsonwebtoken.*; -import io.jsonwebtoken.security.Keys; -import jakarta.ws.rs.NotAuthorizedException; - -import org.apache.hugegraph.config.HugeConfig; - -import javax.crypto.SecretKey; - -import java.nio.charset.StandardCharsets; -import java.util.Date; -import java.util.Map; - -public class TokenGenerator { - - private final SecretKey key; - - public TokenGenerator(HugeConfig config) { - String secretKey = config.get(AuthOptions.AUTH_TOKEN_SECRET); - this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); - } - - public TokenGenerator(String secretKey) { - this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); - } - - public String create(Map payload, long expire) { - return Jwts.builder() - .setClaims(payload) - .setExpiration(new Date(System.currentTimeMillis() + expire)) - .signWith(this.key, SignatureAlgorithm.HS256) - .compact(); - } - - public Claims verify(String token) { - try { - Jws claimsJws = Jwts.parserBuilder() - .setSigningKey(key) - .build() - .parseClaimsJws(token); - return claimsJws.getBody(); - } catch (ExpiredJwtException e) { - throw new NotAuthorizedException("The token is expired", e); - } catch (JwtException e) { - throw new NotAuthorizedException("Invalid token", e); - } - } -} diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/backend/BinaryId.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/backend/BinaryId.java index 685a934fd7..3b72d707cc 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/backend/BinaryId.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/backend/BinaryId.java @@ -1,36 +1,33 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.backend; -import org.apache.hugegraph.id.Id; +import java.nio.ByteBuffer; +import java.util.Arrays; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.E; -import java.nio.ByteBuffer; -import java.util.Arrays; - -public final class BinaryId implements Id { +public class BinaryId implements Id { - private final byte[] bytes; - private final Id id; + protected byte[] bytes; + protected Id id; public BinaryId(byte[] bytes, Id id) { this.bytes = bytes; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/ErrorCodeProvider.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/ErrorCodeProvider.java index d5034b703a..a5777f9fd1 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/ErrorCodeProvider.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/ErrorCodeProvider.java @@ -21,7 +21,7 @@ public interface ErrorCodeProvider { - public String format(Object... args); + String format(Object... args); - public String with(String message); + String with(String message); } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/LimitExceedException.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/LimitExceedException.java index 10652dca2c..6defbf7a44 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/LimitExceedException.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/LimitExceedException.java @@ -1,24 +1,24 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.exception; +import org.apache.hugegraph.exception.HugeException; + public class LimitExceedException extends HugeException { private static final long serialVersionUID = 7384276720045597709L; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/NotAllowException.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/NotAllowException.java index 3781b6d482..75e4f9b597 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/NotAllowException.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/NotAllowException.java @@ -1,24 +1,24 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.exception; +import org.apache.hugegraph.exception.HugeException; + public class NotAllowException extends HugeException { private static final long serialVersionUID = -1407924451828873200L; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/NotFoundException.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/NotFoundException.java index 8567ceb018..7eaadd3fb6 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/NotFoundException.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/NotFoundException.java @@ -1,24 +1,24 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.exception; +import org.apache.hugegraph.exception.HugeException; + public class NotFoundException extends HugeException { private static final long serialVersionUID = -5912665926327173032L; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/NotSupportException.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/NotSupportException.java index 49d3dad49c..713295af70 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/NotSupportException.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/exception/NotSupportException.java @@ -1,24 +1,24 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.exception; +import org.apache.hugegraph.exception.HugeException; + public class NotSupportException extends HugeException { private static final long serialVersionUID = -2914329541122906234L; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/id/EdgeId.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/id/EdgeId.java index 2b03e97d33..dfb530702f 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/id/EdgeId.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/id/EdgeId.java @@ -1,50 +1,45 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.id; -import org.apache.hugegraph.perf.PerfUtil.Watched; -import org.apache.hugegraph.testutil.Assert; -import org.apache.hugegraph.util.E; - import org.apache.hugegraph.exception.NotFoundException; +import org.apache.hugegraph.perf.PerfUtil.Watched; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.StringEncoding; /** * Class used to format and parse id of edge, the edge id consists of: * EdgeId = { source-vertex-id > direction > parentEdgeLabelId > subEdgeLabelId - * >sortKeys > target-vertex-id } + * > sortKeys > target-vertex-id } * NOTE: - * 1. for edges with edgeLabel-type=NORMAL,edgelabelId=parentEdgeLabelId=subEdgeLabelId, - * for edges with edgeLabel type=PARENT,edgelabelId = subEdgeLabelId , - * parentEdgeLabelId = edgelabelId.fatherId - * - * 2.if we use `entry.type()` which is IN or OUT as a part of id, + *

1. for edges with edgeLabelType = NORMAL: edgeLabelId = parentEdgeLabelId = subEdgeLabelId; + * for edges with edgeLabelType = PARENT: edgeLabelId = subEdgeLabelId, parentEdgeLabelId = + * edgeLabelId.fatherId + *

2.if we use `entry.type()` which is IN or OUT as a part of id, * an edge's id will be different due to different directions (belongs * to 2 owner vertex) */ public class EdgeId implements Id { - public static final HugeKeys[] KEYS = new HugeKeys[] { + public static final HugeKeys[] KEYS = new HugeKeys[]{ HugeKeys.OWNER_VERTEX, HugeKeys.DIRECTION, HugeKeys.LABEL, @@ -53,32 +48,28 @@ public class EdgeId implements Id { HugeKeys.OTHER_VERTEX }; - private final Id ownerVertexId; - private final Directions direction; - private final Id edgeLabelId; - private final Id subLabelId; - private final String sortValues; - private final Id otherVertexId; - - private final boolean directed; - private String cache; - + protected final Id ownerVertexId; + protected final Id edgeLabelId; + protected final Id subLabelId; + protected final Id otherVertexId; + protected final Directions direction; + protected final boolean directed; + protected String sortValues; + protected String cache; - public EdgeId(Id ownerVertexId, Directions direction, Id edgeLabelId, - Id subLabelId, String sortValues, - Id otherVertexId) { - this(ownerVertexId, direction, edgeLabelId, - subLabelId, sortValues, otherVertexId, false); + public EdgeId(Id ownerVertexId, Directions direction, Id edgeLabelId, Id subLabelId, + String sortValues, Id otherVertexId) { + this(ownerVertexId, direction, edgeLabelId, subLabelId, + sortValues, otherVertexId, false); } - public EdgeId(Id ownerVertexId, Directions direction, Id edgeLabelId, - Id subLabelId, String sortValues, - Id otherVertexId, boolean directed) { + public EdgeId(Id ownerVertexId, Directions direction, Id edgeLabelId, Id subLabelId, + String sortValues, Id otherVertexId, boolean directed) { this.ownerVertexId = ownerVertexId; this.direction = direction; this.edgeLabelId = edgeLabelId; - this.sortValues = sortValues; this.subLabelId = subLabelId; + this.sortValues = sortValues; this.otherVertexId = otherVertexId; this.directed = directed; this.cache = null; @@ -87,14 +78,13 @@ public EdgeId(Id ownerVertexId, Directions direction, Id edgeLabelId, @Watched public EdgeId switchDirection() { Directions direction = this.direction.opposite(); - return new EdgeId(this.otherVertexId, direction, this.edgeLabelId, - this.subLabelId, this.sortValues, this.ownerVertexId, - this.directed); + return new EdgeId(this.otherVertexId, direction, this.edgeLabelId, this.subLabelId, + this.sortValues, this.ownerVertexId, this.directed); } public EdgeId directed(boolean directed) { - return new EdgeId(this.ownerVertexId, this.direction, this.edgeLabelId, - this.subLabelId, this.sortValues, this.otherVertexId, directed); + return new EdgeId(this.ownerVertexId, this.direction, this.edgeLabelId, this.subLabelId, + this.sortValues, this.otherVertexId, directed); } private Id sourceVertexId() { @@ -109,10 +99,6 @@ private Id targetVertexId() { this.ownerVertexId; } - public Id subLabelId(){ - return this.subLabelId; - } - public Id ownerVertexId() { return this.ownerVertexId; } @@ -121,6 +107,10 @@ public Id edgeLabelId() { return this.edgeLabelId; } + public Id subLabelId() { + return this.subLabelId; + } + public Directions direction() { return this.direction; } @@ -147,23 +137,34 @@ public String asString() { if (this.cache != null) { return this.cache; } + this.cache = this.formatString(); + return this.cache; + } + + /** + * Format the shared edge identity without retaining a heap cache. Read + * components through accessors so storage adapters can keep them off heap. + */ + protected final String formatString() { + Id owner = this.ownerVertexId(); + Id other = this.otherVertexId(); if (this.directed) { - this.cache = SplicingIdGenerator.concat( - IdUtil.writeString(this.ownerVertexId), - this.direction.type().string(), - IdUtil.writeLong(this.edgeLabelId), - IdUtil.writeLong(this.subLabelId), - this.sortValues, - IdUtil.writeString(this.otherVertexId)); - } else { - this.cache = SplicingIdGenerator.concat( - IdUtil.writeString(this.sourceVertexId()), - IdUtil.writeLong(this.edgeLabelId), - IdUtil.writeLong(this.subLabelId), - this.sortValues, - IdUtil.writeString(this.targetVertexId())); + return SplicingIdGenerator.concat( + IdUtil.writeString(owner), + this.direction().type().string(), + IdUtil.writeLong(this.edgeLabelId()), + IdUtil.writeLong(this.subLabelId()), + this.sortValues(), + IdUtil.writeString(other)); } - return this.cache; + Id source = this.direction() == Directions.OUT ? owner : other; + Id target = this.direction() == Directions.OUT ? other : owner; + return SplicingIdGenerator.concat( + IdUtil.writeString(source), + IdUtil.writeLong(this.edgeLabelId()), + IdUtil.writeLong(this.subLabelId()), + this.sortValues(), + IdUtil.writeString(target)); } @Override @@ -219,14 +220,14 @@ public boolean equals(Object object) { return this.ownerVertexId.equals(other.ownerVertexId) && this.direction == other.direction && this.edgeLabelId.equals(other.edgeLabelId) && - this.sortValues.equals(other.sortValues) && this.subLabelId.equals(other.subLabelId) && + this.sortValues.equals(other.sortValues) && this.otherVertexId.equals(other.otherVertexId); } else { return this.sourceVertexId().equals(other.sourceVertexId()) && this.edgeLabelId.equals(other.edgeLabelId) && - this.sortValues.equals(other.sortValues) && this.subLabelId.equals(other.subLabelId) && + this.sortValues.equals(other.sortValues) && this.targetVertexId().equals(other.targetVertexId()); } } @@ -241,7 +242,7 @@ public static byte directionToCode(Directions direction) { } public static Directions directionFromCode(byte code) { - return (code == HugeType.EDGE_OUT.code()) ? Directions.OUT : Directions.IN; + return Directions.convert(HugeType.fromCode(code)); } public static boolean isOutDirectionFromCode(byte code) { @@ -253,7 +254,7 @@ public static EdgeId parse(String id) throws NotFoundException { } public static EdgeId parse(String id, boolean returnNullIfError) - throws NotFoundException { + throws NotFoundException { String[] idParts = SplicingIdGenerator.split(id); if (!(idParts.length == 5 || idParts.length == 6)) { if (returnNullIfError) { @@ -270,8 +271,8 @@ public static EdgeId parse(String id, boolean returnNullIfError) Id subLabelId = IdUtil.readLong(idParts[2]); String sortValues = idParts[3]; Id otherVertexId = IdUtil.readString(idParts[4]); - return new EdgeId(ownerVertexId, Directions.OUT, edgeLabelId, - subLabelId, sortValues, otherVertexId); + return new EdgeId(ownerVertexId, Directions.OUT, edgeLabelId, subLabelId, + sortValues, otherVertexId); } else { assert idParts.length == 6; Id ownerVertexId = IdUtil.readString(idParts[0]); @@ -281,8 +282,7 @@ public static EdgeId parse(String id, boolean returnNullIfError) String sortValues = idParts[4]; Id otherVertexId = IdUtil.readString(idParts[5]); return new EdgeId(ownerVertexId, Directions.convert(direction), - edgeLabelId, subLabelId, - sortValues, otherVertexId); + edgeLabelId, subLabelId, sortValues, otherVertexId); } } catch (Throwable e) { if (returnNullIfError) { @@ -301,8 +301,8 @@ public static Id parseStoredString(String id) { Id subLabelId = IdGenerator.ofStoredString(idParts[2], IdType.LONG); String sortValues = idParts[3]; Id otherVertexId = IdUtil.readStoredString(idParts[4]); - return new EdgeId(ownerVertexId, Directions.OUT, edgeLabelId, - subLabelId, sortValues, otherVertexId); + return new EdgeId(ownerVertexId, Directions.OUT, edgeLabelId, subLabelId, + sortValues, otherVertexId); } public static String asStoredString(Id id) { @@ -326,25 +326,4 @@ public static String[] split(Id id) { public static String[] split(String id) { return SplicingIdGenerator.split(id); } - - - public static void main(String[] args) { - EdgeId edgeId1 = new EdgeId(IdGenerator.of("1:marko"), Directions.OUT, - IdGenerator.of(1), - IdGenerator.of(1), "", - IdGenerator.of("1:josh")); - EdgeId edgeId2 = new EdgeId(IdGenerator.of("1:marko"), Directions.OUT, - IdGenerator.of(1), - IdGenerator.of(1), "", - IdGenerator.of("1:josh")); - EdgeId edgeId3 = new EdgeId(IdGenerator.of("1:josh"), Directions.IN, - IdGenerator.of(1), - IdGenerator.of(1), "", - IdGenerator.of("1:marko")); - Assert.assertTrue(edgeId1.equals(edgeId2)); - Assert.assertTrue(edgeId2.equals(edgeId1)); - Assert.assertTrue(edgeId1.equals(edgeId3)); - Assert.assertTrue(edgeId3.equals(edgeId1)); - } - } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/id/Id.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/id/Id.java index aeb7810a9d..89f61d9583 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/id/Id.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/id/Id.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.id; @@ -25,37 +23,37 @@ public interface Id extends Comparable, Serializable { - public static final int UUID_LENGTH = 16; + int UUID_LENGTH = 16; - public Object asObject(); + Object asObject(); - public String asString(); + String asString(); - public long asLong(); + long asLong(); - public byte[] asBytes(); + byte[] asBytes(); - public int length(); + int length(); - public IdType type(); + IdType type(); - public default boolean number() { + default boolean number() { return this.type() == IdType.LONG; } - public default boolean uuid() { + default boolean uuid() { return this.type() == IdType.UUID; } - public default boolean string() { + default boolean string() { return this.type() == IdType.STRING; } - public default boolean edge() { + default boolean edge() { return this.type() == IdType.EDGE; } - public enum IdType { + enum IdType { UNKNOWN, LONG, @@ -71,8 +69,7 @@ public char prefix() { } public static IdType valueOfPrefix(String id) { - E.checkArgument(id != null && id.length() > 0, - "Invalid id '%s'", id); + E.checkArgument(id != null && !id.isEmpty(), "Invalid id '%s'", id); switch (id.charAt(0)) { case 'L': return IdType.LONG; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/id/IdGenerator.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/id/IdGenerator.java index b6687262db..8eb746e400 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/id/IdGenerator.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/id/IdGenerator.java @@ -1,36 +1,33 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.id; +import java.util.Objects; +import java.util.UUID; + +import org.apache.hugegraph.id.Id.IdType; import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.structure.BaseVertex; -import org.apache.hugegraph.util.StringEncoding; -import com.google.common.primitives.Longs; - +import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.LongEncoding; import org.apache.hugegraph.util.NumericUtil; - -import java.nio.charset.Charset; -import java.util.Objects; -import java.util.UUID; +import org.apache.hugegraph.util.StringEncoding; public abstract class IdGenerator { @@ -38,19 +35,19 @@ public abstract class IdGenerator { public abstract Id generate(BaseVertex vertex); - public final static Id of(String id) { + public static Id of(String id) { return new StringId(id); } - public final static Id of(UUID id) { + public static Id of(UUID id) { return new UuidId(id); } - public final static Id of(String id, boolean uuid) { + public static Id of(String id, boolean uuid) { return uuid ? new UuidId(id) : new StringId(id); } - public final static Id of(long id) { + public static Id of(long id) { return new LongId(id); } @@ -67,7 +64,7 @@ public static Id of(Object id) { return new ObjectId(id); } - public final static Id of(byte[] bytes, Id.IdType type) { + public static Id of(byte[] bytes, IdType type) { switch (type) { case LONG: return new LongId(bytes); @@ -80,13 +77,13 @@ public final static Id of(byte[] bytes, Id.IdType type) { } } - public final static Id ofStoredString(String id, Id.IdType type) { + public static Id ofStoredString(String id, IdType type) { switch (type) { case LONG: return of(LongEncoding.decodeSignedB64(id)); case UUID: byte[] bytes = StringEncoding.decodeBase64(id); - return of(bytes, Id.IdType.UUID); + return of(bytes, IdType.UUID); case STRING: return of(id); default: @@ -94,7 +91,7 @@ public final static Id ofStoredString(String id, Id.IdType type) { } } - public final static String asStoredString(Id id) { + public static String asStoredString(Id id) { switch (id.type()) { case LONG: return LongEncoding.encodeSignedB64(id.asLong()); @@ -107,40 +104,45 @@ public final static String asStoredString(Id id) { } } - public final static Id.IdType idType(Id id) { + public static IdType idType(Id id) { if (id instanceof LongId) { - return Id.IdType.LONG; + return IdType.LONG; } if (id instanceof UuidId) { - return Id.IdType.UUID; + return IdType.UUID; } if (id instanceof StringId) { - return Id.IdType.STRING; + return IdType.STRING; } if (id instanceof EdgeId) { - return Id.IdType.EDGE; + return IdType.EDGE; } - return Id.IdType.UNKNOWN; + return IdType.UNKNOWN; } - private final static int compareType(Id id1, Id id2) { + public static int compareType(Id id1, Id id2) { return idType(id1).ordinal() - idType(id2).ordinal(); } /****************************** id defines ******************************/ - public static final class StringId implements Id { + public static class StringId implements Id { - private final String id; - private static final Charset CHARSET = Charset.forName("UTF-8"); + protected String id; + protected byte[] bytes; public StringId(String id) { - E.checkArgument(!id.isEmpty(), "The id can't be empty"); + E.checkArgument(id != null && !id.isEmpty(), + "The id can't be null or empty"); this.id = id; + this.bytes = null; } public StringId(byte[] bytes) { - this.id = StringEncoding.decode(bytes); + E.checkArgument(bytes != null && bytes.length > 0, + "The id bytes can't be null or empty"); + this.bytes = bytes; + this.id = null; } @Override @@ -150,27 +152,35 @@ public IdType type() { @Override public Object asObject() { - return this.id; + return this.asString(); } @Override public String asString() { + if (this.id == null) { + assert this.bytes != null; + this.id = StringEncoding.decode(this.bytes); + } return this.id; } @Override public long asLong() { - return Long.parseLong(this.id); + return Long.parseLong(this.asString()); } @Override public byte[] asBytes() { - return this.id.getBytes(CHARSET); + if (this.bytes == null) { + assert this.id != null; + this.bytes = StringEncoding.encode(this.id); + } + return this.bytes; } @Override public int length() { - return this.id.length(); + return this.asString().length(); } @Override @@ -179,33 +189,41 @@ public int compareTo(Id other) { if (cmp != 0) { return cmp; } - return this.id.compareTo(other.asString()); + return this.asString().compareTo(other.asString()); } @Override public int hashCode() { - return this.id.hashCode(); + return this.asString().hashCode(); } @Override - public boolean equals(Object other) { - if (!(other instanceof StringId)) { + public boolean equals(Object obj) { + if (!(obj instanceof StringId)) { return false; } - return this.id.equals(((StringId) other).id); + StringId other = (StringId) obj; + if (this.id != null) { + return this.id.equals(other.asString()); + } else if (other.bytes == null) { + return this.asString().equals(other.asString()); + } else { + assert this.bytes != null; + return Bytes.equals(this.bytes, other.asBytes()); + } } @Override public String toString() { - return this.id; + return this.asString(); } } - public static final class LongId extends Number implements Id { + public static class LongId extends Number implements Id { private static final long serialVersionUID = -7732461469037400190L; - private final long id; + protected Long id; public LongId(long id) { this.id = id; @@ -238,8 +256,7 @@ public long asLong() { @Override public byte[] asBytes() { - return Longs.toByteArray(this.id); - // return NumericUtil.longToBytes(this.id); + return NumericUtil.longToBytes(this.id); } @Override @@ -264,26 +281,11 @@ public int hashCode() { @Override public boolean equals(Object other) { if (!(other instanceof Number)) { - if (idDigitalObject(other)) { - return this.id == (long) Double.parseDouble(other.toString()); - } return false; } return this.id == ((Number) other).longValue(); } - private static boolean idDigitalObject(Object object) { - String string = object.toString(); - for (int i = string.length(); --i >= 0; ) { - char c = string.charAt(i); - if (!Character.isDigit(c) && - '.' != c) { - return false; - } - } - return true; - } - @Override public String toString() { return String.valueOf(this.id); @@ -291,7 +293,7 @@ public String toString() { @Override public int intValue() { - return (int) this.id; + return this.id.intValue(); } @Override @@ -310,9 +312,9 @@ public double doubleValue() { } } - public static final class UuidId implements Id { + public static class UuidId implements Id { - private final UUID uuid; + protected UUID uuid; public UuidId(String string) { this(StringEncoding.uuid(string)); @@ -400,9 +402,9 @@ public String toString() { /** * This class is just used by backend store for wrapper object as Id */ - public static final class ObjectId implements Id { + public static class ObjectId implements Id { - private final Object object; + protected Object object; public ObjectId(Object object) { E.checkNotNull(object, "object"); diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/id/IdUtil.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/id/IdUtil.java index b394c79a12..ce7c970edc 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/id/IdUtil.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/id/IdUtil.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.id; @@ -22,7 +20,7 @@ import java.nio.ByteBuffer; import org.apache.commons.lang3.StringUtils; - +import org.apache.hugegraph.id.Id.IdType; import org.apache.hugegraph.serializer.BytesBuffer; public final class IdUtil { @@ -45,7 +43,7 @@ public static String writeStoredString(Id id) { } public static Id readStoredString(String id) { - Id.IdType type = Id.IdType.valueOfPrefix(id); + IdType type = IdType.valueOfPrefix(id); String idContent = id.substring(1); switch (type) { case LONG: @@ -82,23 +80,20 @@ public static Id fromBytes(byte[] bytes) { return buffer.readId(); } - public static String writeString(Id id) { String idString = id.asString(); - StringBuilder sb = new StringBuilder(1 + idString.length()); - sb.append(id.type().prefix()).append(idString); - return sb.toString(); + return id.type().prefix() + idString; } public static Id readString(String id) { - Id.IdType type = Id.IdType.valueOfPrefix(id); + IdType type = IdType.valueOfPrefix(id); String idContent = id.substring(1); switch (type) { case LONG: return IdGenerator.of(Long.parseLong(idContent)); case STRING: case UUID: - return IdGenerator.of(idContent, type == Id.IdType.UUID); + return IdGenerator.of(idContent, type == IdType.UUID); case EDGE: return EdgeId.parse(idContent); default: @@ -145,12 +140,13 @@ public static String escape(char splitor, char escape, String... values) { public static String[] unescape(String id, String splitor, String escape) { /* - * Note that the `splitor`/`escape` maybe special characters in regular + * Note that the `splitter`/`escape` maybe special characters in regular * expressions, but this is a frequently called method, for faster * execution, we forbid the use of special characters as delimiter * or escape sign. + * * The `limit` param -1 in split method can ensure empty string be - * splited to a part. + * split to a part. */ String[] parts = id.split("(? values) { // Convert the object list to string array @@ -108,8 +109,9 @@ public static String concatValues(List values) { /** * Concat property values with NAME_SPLITOR + * * @param values the property values to be concatted - * @return concatted string value + * @return concatted string value */ public static String concatValues(Object... values) { return concatValues(Arrays.asList(values)); @@ -117,8 +119,9 @@ public static String concatValues(Object... values) { /** * Concat multiple parts into a single id with ID_SPLITOR + * * @param parts the string id values to be spliced - * @return spliced id object + * @return spliced id object */ public static Id splicing(String... parts) { String escaped = IdUtil.escape(ID_SPLITOR, ESCAPE, parts); @@ -141,8 +144,9 @@ public static Id generateBinaryId(Id id) { /** * Parse a single id into multiple parts with ID_SPLITOR + * * @param id the id object to be parsed - * @return parsed string id parts + * @return parsed string id parts */ public static String[] parse(Id id) { return IdUtil.unescape(id.asString(), ID_SPLITOR_STR, ESCAPE_STR); diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/options/AuthOptions.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/options/AuthOptions.java deleted file mode 100644 index c61946ae8b..0000000000 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/options/AuthOptions.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2017 HugeGraph Authors - * - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -package org.apache.hugegraph.options; - -import org.apache.hugegraph.config.ConfigOption; -import org.apache.hugegraph.config.OptionHolder; - -import java.security.SecureRandom; -import java.util.Base64; - -import static org.apache.hugegraph.config.OptionChecker.*; - -public class AuthOptions extends OptionHolder { - - private AuthOptions() { - super(); - } - - private static volatile AuthOptions instance; - - public static synchronized AuthOptions instance() { - if (instance == null) { - instance = new AuthOptions(); - instance.registerOptions(); - } - return instance; - } - - public static final ConfigOption AUTH_TOKEN_SECRET = - new ConfigOption<>( - "auth.token_secret", - "Secret key of HS256 algorithm.", - disallowEmpty(), - "FXQXbJtbCLxODc6tGci732pkH1cyf8Qg" - ); - - public static final ConfigOption AUTH_AUDIT_LOG_RATE = - new ConfigOption<>( - "auth.audit_log_rate", - "The max rate of audit log output per user, " + - "default value is 1000 records per second.", - rangeDouble(0.0, Double.MAX_VALUE), - 1000.0 - ); - - public static final ConfigOption AUTH_PROXY_CACHE_EXPIRE = - new ConfigOption<>( - "auth.proxy_cache_expire", - "The expiration time in seconds of auth cache in " + - "auth client.", - rangeInt(0L, Long.MAX_VALUE), - (1 * 60L) - ); - - public static final ConfigOption AUTH_CACHE_CAPACITY = - new ConfigOption<>( - "auth.cache_capacity", - "The max cache capacity of each auth cache item.", - rangeInt(0L, Long.MAX_VALUE), - (1024 * 10L) - ); - - public static final ConfigOption AUTHENTICATOR = - new ConfigOption<>( - "auth.authenticator", - "The class path of authenticator implementation. " + - "e.g., org.apache.hugegraph.auth.StandardAuthenticator.", - null, - "" - ); - - public static final ConfigOption AUTH_GRAPH_STORE = - new ConfigOption<>( - "auth.graph_store", - "The name of graph used to store authentication information, " + - "like users, only for org.apache.hugegraph.auth.StandardAuthenticator.", - disallowEmpty(), - "hugegraph" - ); - - public static final ConfigOption AUTH_REMOTE_URL = - new ConfigOption<>( - "auth.remote_url", - "If the address is empty, it provide auth service, " + - "otherwise it is auth client and also provide auth service " + - "through rpc forwarding. The remote url can be set to " + - "multiple addresses, which are concat by ','.", - null, - "" - ); - - public static final ConfigOption AUTH_CACHE_EXPIRE = - new ConfigOption<>( - "auth.cache_expire", - "The expiration time in seconds of auth cache in " + - "auth client and auth server.", - rangeInt(0L, Long.MAX_VALUE), - (60 * 10L) - ); - - public static final ConfigOption AUTH_TOKEN_EXPIRE = - new ConfigOption<>( - "auth.token_expire", - "The expiration time in seconds after token created", - rangeInt(0L, Long.MAX_VALUE), - (3600 * 24L) - ); - - private static String generateRandomBase64Key() { - SecureRandom random = new SecureRandom(); - // 32 bytes for HMAC-SHA256 - byte[] bytes = new byte[32]; - random.nextBytes(bytes); - return Base64.getEncoder().encodeToString(bytes); - } -} diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/options/CoreOptions.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/options/CoreOptions.java deleted file mode 100644 index e11f76d349..0000000000 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/options/CoreOptions.java +++ /dev/null @@ -1,660 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph.options; - -import org.apache.hugegraph.config.ConfigConvOption; -import org.apache.hugegraph.config.ConfigOption; -import org.apache.hugegraph.config.OptionHolder; -import org.apache.hugegraph.query.Query; -import org.apache.hugegraph.type.define.CollectionType; -import org.apache.hugegraph.util.Bytes; - -import static org.apache.hugegraph.config.OptionChecker.*; -import static org.apache.hugegraph.query.Query.COMMIT_BATCH; - -public class CoreOptions extends OptionHolder { - - public static final int CPUS = Runtime.getRuntime().availableProcessors(); - public static final ConfigOption GREMLIN_GRAPH = - new ConfigOption<>( - "gremlin.graph", - "Gremlin entrance to create graph.", - disallowEmpty(), - "org.apache.hugegraph.HugeFactory" - ); - public static final ConfigOption BACKEND = - new ConfigOption<>( - "backend", - "The data store type.", - disallowEmpty(), - "memory" - ); - public static final ConfigOption STORE = - new ConfigOption<>( - "store", - "The backend database name.", - disallowEmpty(), - "hugegraph" - ); - public static final ConfigOption STORE_GRAPH = - new ConfigOption<>( - "store.graph", - "The graph table name, which store vertex, edge and property.", - disallowEmpty(), - "g" - ); - public static final ConfigOption ALIAS_NAME = - new ConfigOption<>( - "alias.graph.id", - "The graph alias id.", - "" - ); - public static final ConfigOption SERIALIZER = - new ConfigOption<>( - "serializer", - "The serializer for backend store, like: text/binary.", - disallowEmpty(), - "text" - ); - public static final ConfigOption RAFT_MODE = - new ConfigOption<>( - "raft.mode", - "Whether the backend storage works in raft mode.", - disallowEmpty(), - false - ); - public static final ConfigOption RAFT_SAFE_READ = - new ConfigOption<>( - "raft.safe_read", - "Whether to use linearly consistent read.", - disallowEmpty(), - false - ); - public static final ConfigOption RAFT_PATH = - new ConfigOption<>( - "raft.path", - "The log path of current raft node.", - disallowEmpty(), - "./raftlog" - ); - public static final ConfigOption RAFT_REPLICATOR_PIPELINE = - new ConfigOption<>( - "raft.use_replicator_pipeline", - "Whether to use replicator line, when turned on it " + - "multiple logs can be sent in parallel, and the next log " + - "doesn't have to wait for the ack message of the current " + - "log to be sent.", - disallowEmpty(), - true - ); - public static final ConfigOption RAFT_ELECTION_TIMEOUT = - new ConfigOption<>( - "raft.election_timeout", - "Timeout in milliseconds to launch a round of election.", - rangeInt(0, Integer.MAX_VALUE), - 10000 - ); - public static final ConfigOption RAFT_SNAPSHOT_INTERVAL = - new ConfigOption<>( - "raft.snapshot_interval", - "The interval in seconds to trigger snapshot save.", - rangeInt(0, Integer.MAX_VALUE), - 3600 - ); - public static final ConfigOption RAFT_SNAPSHOT_THREADS = - new ConfigOption<>( - "raft.snapshot_threads", - "The thread number used to do snapshot.", - rangeInt(0, Integer.MAX_VALUE), - 4 - ); - public static final ConfigOption RAFT_SNAPSHOT_PARALLEL_COMPRESS = - new ConfigOption<>( - "raft.snapshot_parallel_compress", - "Whether to enable parallel compress.", - disallowEmpty(), - false - ); - public static final ConfigOption RAFT_SNAPSHOT_COMPRESS_THREADS = - new ConfigOption<>( - "raft.snapshot_compress_threads", - "The thread number used to do snapshot compress.", - rangeInt(0, Integer.MAX_VALUE), - 4 - ); - public static final ConfigOption RAFT_SNAPSHOT_DECOMPRESS_THREADS = - new ConfigOption<>( - "raft.snapshot_decompress_threads", - "The thread number used to do snapshot decompress.", - rangeInt(0, Integer.MAX_VALUE), - 4 - ); - public static final ConfigOption RAFT_BACKEND_THREADS = - new ConfigOption<>( - "raft.backend_threads", - "The thread number used to apply task to backend.", - rangeInt(0, Integer.MAX_VALUE), - CPUS - ); - public static final ConfigOption RAFT_READ_INDEX_THREADS = - new ConfigOption<>( - "raft.read_index_threads", - "The thread number used to execute reading index.", - rangeInt(0, Integer.MAX_VALUE), - 8 - ); - public static final ConfigOption RAFT_READ_STRATEGY = - new ConfigOption<>( - "raft.read_strategy", - "The linearizability of read strategy.", - allowValues("ReadOnlyLeaseBased", "ReadOnlySafe"), - "ReadOnlyLeaseBased" - ); - public static final ConfigOption RAFT_APPLY_BATCH = - new ConfigOption<>( - "raft.apply_batch", - "The apply batch size to trigger disruptor event handler.", - positiveInt(), - // jraft default value is 32 - 1 - ); - public static final ConfigOption RAFT_QUEUE_SIZE = - new ConfigOption<>( - "raft.queue_size", - "The disruptor buffers size for jraft RaftNode, " + - "StateMachine and LogManager.", - positiveInt(), - // jraft default value is 16384 - 16384 - ); - public static final ConfigOption RAFT_QUEUE_PUBLISH_TIMEOUT = - new ConfigOption<>( - "raft.queue_publish_timeout", - "The timeout in second when publish event into disruptor.", - positiveInt(), - // jraft default value is 10(sec) - 60 - ); - public static final ConfigOption RAFT_RPC_THREADS = - new ConfigOption<>( - "raft.rpc_threads", - "The rpc threads for jraft RPC layer", - positiveInt(), - // jraft default value is 80 - Math.max(CPUS * 2, 80) - ); - public static final ConfigOption RAFT_RPC_CONNECT_TIMEOUT = - new ConfigOption<>( - "raft.rpc_connect_timeout", - "The rpc connect timeout for jraft rpc.", - positiveInt(), - // jraft default value is 1000(ms) - 5000 - ); - public static final ConfigOption RAFT_RPC_TIMEOUT = - new ConfigOption<>( - "raft.rpc_timeout", - "The general rpc timeout in seconds for jraft rpc.", - positiveInt(), - // jraft default value is 5s - 60 - ); - public static final ConfigOption RAFT_INSTALL_SNAPSHOT_TIMEOUT = - new ConfigOption<>( - "raft.install_snapshot_rpc_timeout", - "The install snapshot rpc timeout in seconds for jraft rpc.", - positiveInt(), - // jraft default value is 5 minutes - 10 * 60 * 60 - ); - public static final ConfigOption RAFT_RPC_BUF_LOW_WATER_MARK = - new ConfigOption<>( - "raft.rpc_buf_low_water_mark", - "The ChannelOutboundBuffer's low water mark of netty, " + - "when buffer size less than this size, the method " + - "ChannelOutboundBuffer.isWritable() will return true, " + - "it means that low downstream pressure or good network.", - positiveInt(), - 10 * 1024 * 1024 - ); - public static final ConfigOption RAFT_RPC_BUF_HIGH_WATER_MARK = - new ConfigOption<>( - "raft.rpc_buf_high_water_mark", - "The ChannelOutboundBuffer's high water mark of netty, " + - "only when buffer size exceed this size, the method " + - "ChannelOutboundBuffer.isWritable() will return false, " + - "it means that the downstream pressure is too great to " + - "process the request or network is very congestion, " + - "upstream needs to limit rate at this time.", - positiveInt(), - 20 * 1024 * 1024 - ); - public static final ConfigOption RATE_LIMIT_WRITE = - new ConfigOption<>( - "rate_limit.write", - "The max rate(items/s) to add/update/delete vertices/edges.", - rangeInt(0, Integer.MAX_VALUE), - 0 - ); - public static final ConfigOption RATE_LIMIT_READ = - new ConfigOption<>( - "rate_limit.read", - "The max rate(times/s) to execute query of vertices/edges.", - rangeInt(0, Integer.MAX_VALUE), - 0 - ); - public static final ConfigOption TASK_SCHEDULE_PERIOD = - new ConfigOption<>( - "task.schedule_period", - "Period time when scheduler to schedule task", - rangeInt(0L, Long.MAX_VALUE), - 10L - ); - public static final ConfigOption TASK_WAIT_TIMEOUT = - new ConfigOption<>( - "task.wait_timeout", - "Timeout in seconds for waiting for the task to " + - "complete, such as when truncating or clearing the " + - "backend.", - rangeInt(0L, Long.MAX_VALUE), - 10L - ); - public static final ConfigOption TASK_INPUT_SIZE_LIMIT = - new ConfigOption<>( - "task.input_size_limit", - "The job input size limit in bytes.", - rangeInt(0L, Bytes.GB), - 16 * Bytes.MB - ); - public static final ConfigOption TASK_RESULT_SIZE_LIMIT = - new ConfigOption<>( - "task.result_size_limit", - "The job result size limit in bytes.", - rangeInt(0L, Bytes.GB), - 16 * Bytes.MB - ); - public static final ConfigOption TASK_TTL_DELETE_BATCH = - new ConfigOption<>( - "task.ttl_delete_batch", - "The batch size used to delete expired data.", - rangeInt(1, 500), - 1 - ); - - public static final ConfigOption TASK_SYNC_DELETION = - new ConfigOption<>( - "task.sync_deletion", - "Whether to delete schema or expired data synchronously.", - disallowEmpty(), - false - ); - public static final ConfigOption TASK_RETRY = - new ConfigOption<>( - "task.retry", - "Task retry times.", - rangeInt(0, 3), - 0 - ); - public static final ConfigOption STORE_CONN_DETECT_INTERVAL = - new ConfigOption<>( - "store.connection_detect_interval", - "The interval in seconds for detecting connections, " + - "if the idle time of a connection exceeds this value, " + - "detect it and reconnect if needed before using, " + - "value 0 means detecting every time.", - rangeInt(0L, Long.MAX_VALUE), - 600L - ); - public static final ConfigOption VERTEX_DEFAULT_LABEL = - new ConfigOption<>( - "vertex.default_label", - "The default vertex label.", - disallowEmpty(), - "vertex" - ); - public static final ConfigOption VERTEX_CHECK_CUSTOMIZED_ID_EXIST = - new ConfigOption<>( - "vertex.check_customized_id_exist", - "Whether to check the vertices exist for those using " + - "customized id strategy.", - disallowEmpty(), - false - ); - public static final ConfigOption VERTEX_REMOVE_LEFT_INDEX = - new ConfigOption<>( - "vertex.remove_left_index_at_overwrite", - "Whether remove left index at overwrite.", - disallowEmpty(), - false - ); - public static final ConfigOption VERTEX_ADJACENT_VERTEX_EXIST = - new ConfigOption<>( - "vertex.check_adjacent_vertex_exist", - "Whether to check the adjacent vertices of edges exist.", - disallowEmpty(), - false - ); - public static final ConfigOption VERTEX_ADJACENT_VERTEX_LAZY = - new ConfigOption<>( - "vertex.lazy_load_adjacent_vertex", - "Whether to lazy load adjacent vertices of edges.", - disallowEmpty(), - true - ); - public static final ConfigOption VERTEX_PART_EDGE_COMMIT_SIZE = - new ConfigOption<>( - "vertex.part_edge_commit_size", - "Whether to enable the mode to commit part of edges of " + - "vertex, enabled if commit size > 0, 0 meas disabled.", - rangeInt(0, (int) Query.DEFAULT_CAPACITY), - 5000 - ); - public static final ConfigOption VERTEX_ENCODE_PK_NUMBER = - new ConfigOption<>( - "vertex.encode_primary_key_number", - "Whether to encode number value of primary key " + - "in vertex id.", - disallowEmpty(), - true - ); - public static final ConfigOption VERTEX_TX_CAPACITY = - new ConfigOption<>( - "vertex.tx_capacity", - "The max size(items) of vertices(uncommitted) in " + - "transaction.", - rangeInt((int) COMMIT_BATCH, 1000000), - 10000 - ); - public static final ConfigOption QUERY_IGNORE_INVALID_DATA = - new ConfigOption<>( - "query.ignore_invalid_data", - "Whether to ignore invalid data of vertex or edge.", - disallowEmpty(), - true - ); - public static final ConfigOption QUERY_OPTIMIZE_AGGR_BY_INDEX = - new ConfigOption<>( - "query.optimize_aggregate_by_index", - "Whether to optimize aggregate query(like count) by index.", - disallowEmpty(), - false - ); - public static final ConfigOption QUERY_BATCH_SIZE = - new ConfigOption<>( - "query.batch_size", - "The size of each batch when querying by batch.", - rangeInt(1, (int) Query.DEFAULT_CAPACITY), - 1000 - ); - public static final ConfigOption QUERY_PAGE_SIZE = - new ConfigOption<>( - "query.page_size", - "The size of each page when querying by paging.", - rangeInt(1, (int) Query.DEFAULT_CAPACITY), - 500 - ); - public static final ConfigOption QUERY_INDEX_INTERSECT_THRESHOLD = - new ConfigOption<>( - "query.index_intersect_threshold", - "The maximum number of intermediate results to " + - "intersect indexes when querying by multiple single " + - "index properties.", - rangeInt(1, (int) Query.DEFAULT_CAPACITY), - 1000 - ); - public static final ConfigOption SCHEMA_INIT_TEMPLATE = - new ConfigOption<>( - "schema.init_template", - "The template schema used to init graph", - null, - "" - ); - public static final ConfigOption SCHEMA_INDEX_REBUILD_USING_PUSHDOWN = - new ConfigOption<>( - "schema.index_rebuild_using_pushdown", - "Whether to use pushdown when to create/rebuid index.", - true - ); - public static final ConfigOption QUERY_RAMTABLE_ENABLE = - new ConfigOption<>( - "query.ramtable_enable", - "Whether to enable ramtable for query of adjacent edges.", - disallowEmpty(), - false - ); - public static final ConfigOption QUERY_RAMTABLE_VERTICES_CAPACITY = - new ConfigOption<>( - "query.ramtable_vertices_capacity", - "The maximum number of vertices in ramtable, " + - "generally the largest vertex id is used as capacity.", - rangeInt(1L, Integer.MAX_VALUE * 2L), - 10000000L - ); - public static final ConfigOption QUERY_RAMTABLE_EDGES_CAPACITY = - new ConfigOption<>( - "query.ramtable_edges_capacity", - "The maximum number of edges in ramtable, " + - "include OUT and IN edges.", - rangeInt(1, Integer.MAX_VALUE), - 20000000 - ); - /** - * The schema name rule: - * 1. Not allowed end with spaces - * 2. Not allowed start with '~' - */ - public static final ConfigOption SCHEMA_ILLEGAL_NAME_REGEX = - new ConfigOption<>( - "schema.illegal_name_regex", - "The regex specified the illegal format for schema name.", - disallowEmpty(), - ".*\\s+$|~.*" - ); - public static final ConfigOption SCHEMA_CACHE_CAPACITY = - new ConfigOption<>( - "schema.cache_capacity", - "The max cache size(items) of schema cache.", - rangeInt(0L, Long.MAX_VALUE), - 10000L - ); - public static final ConfigOption VERTEX_CACHE_TYPE = - new ConfigOption<>( - "vertex.cache_type", - "The type of vertex cache, allowed values are [l1, l2].", - allowValues("l1", "l2"), - "l2" - ); - public static final ConfigOption VERTEX_CACHE_CAPACITY = - new ConfigOption<>( - "vertex.cache_capacity", - "The max cache size(items) of vertex cache.", - rangeInt(0L, Long.MAX_VALUE), - (1000 * 1000 * 10L) - ); - public static final ConfigOption VERTEX_CACHE_EXPIRE = - new ConfigOption<>( - "vertex.cache_expire", - "The expiration time in seconds of vertex cache.", - rangeInt(0, Integer.MAX_VALUE), - (60 * 10) - ); - public static final ConfigOption EDGE_CACHE_TYPE = - new ConfigOption<>( - "edge.cache_type", - "The type of edge cache, allowed values are [l1, l2].", - allowValues("l1", "l2"), - "l2" - ); - public static final ConfigOption EDGE_CACHE_CAPACITY = - new ConfigOption<>( - "edge.cache_capacity", - "The max cache size(items) of edge cache.", - rangeInt(0L, Long.MAX_VALUE), - ((long) 1000 * 1000) - ); - public static final ConfigOption EDGE_CACHE_EXPIRE = - new ConfigOption<>( - "edge.cache_expire", - "The expiration time in seconds of edge cache.", - rangeInt(0, Integer.MAX_VALUE), - (60 * 10) - ); - public static final ConfigOption SNOWFLAKE_WORKER_ID = - new ConfigOption<>( - "snowflake.worker_id", - "The worker id of snowflake id generator.", - disallowEmpty(), - 0L - ); - public static final ConfigOption SNOWFLAKE_DATACENTER_ID = - new ConfigOption<>( - "snowflake.datacenter_id", - "The datacenter id of snowflake id generator.", - disallowEmpty(), - 0L - ); - public static final ConfigOption SNOWFLAKE_FORCE_STRING = - new ConfigOption<>( - "snowflake.force_string", - "Whether to force the snowflake long id to be a string.", - disallowEmpty(), - false - ); - public static final ConfigOption TEXT_ANALYZER = - new ConfigOption<>( - "search.text_analyzer", - "Choose a text analyzer for searching the " + - "vertex/edge properties, available type are " + - "[ansj, hanlp, smartcn, jieba, jcseg, " + - "mmseg4j, ikanalyzer].", - disallowEmpty(), - "ikanalyzer" - ); - public static final ConfigOption TEXT_ANALYZER_MODE = - new ConfigOption<>( - "search.text_analyzer_mode", - "Specify the mode for the text analyzer, " + - "the available mode of analyzer are " + - "ansj: [BaseAnalysis, IndexAnalysis, ToAnalysis, " + - "NlpAnalysis], " + - "hanlp: [standard, nlp, index, nShort, shortest, speed], " + - "smartcn: [], " + - "jieba: [SEARCH, INDEX], " + - "jcseg: [Simple, Complex], " + - "mmseg4j: [Simple, Complex, MaxWord], " + - "ikanalyzer: [smart, max_word]" + - "}.", - disallowEmpty(), - "smart" - ); - public static final ConfigOption COMPUTER_CONFIG = - new ConfigOption<>( - "computer.config", - "The config file path of computer job.", - disallowEmpty(), - "./conf/computer.yaml" - ); - public static final ConfigOption K8S_OPERATOR_TEMPLATE = - new ConfigOption<>( - "k8s.operator_template", - "the path of operator container template.", - disallowEmpty(), - "./conf/operator-template.yaml" - ); - public static final ConfigOption K8S_QUOTA_TEMPLATE = - new ConfigOption<>( - "k8s.quota_template", - "the path of resource quota template.", - disallowEmpty(), - "./conf/resource-quota-template.yaml" - ); - public static final ConfigOption OLTP_CONCURRENT_THREADS = - new ConfigOption<>( - "oltp.concurrent_threads", - "Thread number to concurrently execute oltp algorithm.", - rangeInt(0, 65535), - 10 - ); - public static final ConfigOption OLTP_CONCURRENT_DEPTH = - new ConfigOption<>( - "oltp.concurrent_depth", - "The min depth to enable concurrent oltp algorithm.", - rangeInt(0, 65535), - 10 - ); - public static final ConfigConvOption OLTP_COLLECTION_TYPE = - new ConfigConvOption<>( - "oltp.collection_type", - "The implementation type of collections " + - "used in oltp algorithm.", - allowValues("JCF", "EC", "FU"), - CollectionType::valueOf, - "EC" - ); - public static final ConfigOption PD_PEERS = new ConfigOption<>( - "pd.peers", - "The addresses of pd nodes, separated with commas.", - disallowEmpty(), - "127.0.0.1:8686" - ); - public static final ConfigOption MEMORY_MODE = new ConfigOption<>( - "memory.mode", - "The memory mode used for query in HugeGraph.", - disallowEmpty(), - "off-heap" - ); - public static final ConfigOption MAX_MEMORY_CAPACITY = new ConfigOption<>( - "memory.max_capacity", - "The maximum memory capacity that can be managed for all queries in HugeGraph.", - nonNegativeInt(), - Bytes.GB - ); - public static final ConfigOption ONE_QUERY_MAX_MEMORY_CAPACITY = new ConfigOption<>( - "memory.one_query_max_capacity", - "The maximum memory capacity that can be managed for a query in HugeGraph.", - nonNegativeInt(), - Bytes.MB * 100 - ); - public static final ConfigOption MEMORY_ALIGNMENT = new ConfigOption<>( - "memory.alignment", - "The alignment used for round memory size.", - nonNegativeInt(), - 8L - ); - public static final ConfigOption GRAPH_SPACE = - new ConfigOption<>( - "graphspace", - "The graph space name.", - null, - "DEFAULT" - ); - private static volatile CoreOptions instance; - - private CoreOptions() { - super(); - } - - public static synchronized CoreOptions instance() { - if (instance == null) { - instance = new CoreOptions(); - // Should initialize all static members first, then register. - instance.registerOptions(); - } - return instance; - } -} diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Aggregate.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Aggregate.java index 38f1365f67..51d2ee9db4 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Aggregate.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Aggregate.java @@ -20,19 +20,21 @@ package org.apache.hugegraph.query; import java.util.Iterator; +import java.util.function.BiFunction; + +import org.apache.tinkerpop.gremlin.util.NumberHelper; -@Deprecated public class Aggregate

{ private final AggregateFuncDefine

func; private final String column; - public Aggregate(AggregateFuncDefine func, String column) { + public Aggregate(AggregateFuncDefine

func, String column) { this.func = func; this.column = column; } - public AggregateFuncDefine func() { + public AggregateFuncDefine

func() { return this.func; } @@ -58,4 +60,58 @@ public String toString() { this.column == null ? "*" : this.column); } + public enum AggregateFunc implements AggregateFuncDefine { + + COUNT("count", 0L, NumberHelper::add), + MAX("max", -Double.MAX_VALUE, NumberHelper::max), + MIN("min", Double.MAX_VALUE, NumberHelper::min), + AVG("avg", 0D, NumberHelper::add), + SUM("sum", 0L, NumberHelper::add); + + private final String name; + private final Number defaultValue; + private final BiFunction merger; + + AggregateFunc(String name, Number defaultValue, + BiFunction merger) { + this.name = name; + this.defaultValue = defaultValue; + this.merger = merger; + } + + @Override + public String string() { + return this.name; + } + + @Override + public boolean countAll() { + return this == COUNT; + } + + @Override + public Number defaultValue() { + return this.defaultValue; + } + + @Override + public Number reduce(Iterator results) { + Number number; + long count = 0L; + if (results.hasNext()) { + number = results.next(); + count++; + } else { + return this.defaultValue; + } + while (results.hasNext()) { + number = this.merger.apply(number, results.next()); + count++; + } + if (this == AVG) { + number = NumberHelper.div(number, count); + } + return number; + } + } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/AggregateFuncDefine.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/AggregateFuncDefine.java index d883aab49c..7cd74118c6 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/AggregateFuncDefine.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/AggregateFuncDefine.java @@ -26,7 +26,8 @@ * * @param

*/ -public interface AggregateFuncDefine

{ +public interface AggregateFuncDefine

{ + String string(); P defaultValue(); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/BatchConditionQuery.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/BatchConditionQuery.java similarity index 96% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/BatchConditionQuery.java rename to hugegraph-struct/src/main/java/org/apache/hugegraph/query/BatchConditionQuery.java index 181ee48648..32f09e03e3 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/BatchConditionQuery.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/BatchConditionQuery.java @@ -15,13 +15,13 @@ * limitations under the License. */ -package org.apache.hugegraph.backend.query; +package org.apache.hugegraph.query; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import org.apache.hugegraph.backend.query.Condition.RelationType; +import org.apache.hugegraph.query.Condition.RelationType; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.util.E; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Condition.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Condition.java index 415405a1bd..c2d63e4f35 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Condition.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Condition.java @@ -17,10 +17,16 @@ package org.apache.hugegraph.query; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import org.apache.commons.lang.ArrayUtils; -import org.apache.commons.text.similarity.LevenshteinDistance; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.regex.Pattern; + import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.structure.BaseElement; @@ -30,11 +36,12 @@ import org.apache.hugegraph.util.DateUtil; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.NumericUtil; +import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate; -import java.util.*; -import java.util.function.BiFunction; -import java.util.function.BiPredicate; -import java.util.regex.Pattern; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.text.similarity.LevenshteinDistance; public abstract class Condition { @@ -199,7 +206,7 @@ public enum ConditionType { NOT } - public enum RelationType implements BiPredicate { + public enum RelationType implements PBiPredicate { EQ("==", RelationType::equals), @@ -522,6 +529,11 @@ public String string() { return this.operator; } + @Override + public String getPredicateName() { + return this.operator; + } + private void checkBaseType(Object value, Class clazz) { if (!clazz.isInstance(value)) { String valueClass = value == null ? "null" : diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/ConditionQuery.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/ConditionQuery.java index 553fec9b8c..8fca84212c 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/ConditionQuery.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/ConditionQuery.java @@ -17,17 +17,26 @@ package org.apache.hugegraph.query; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.id.SplicingIdGenerator; import org.apache.hugegraph.perf.PerfUtil.Watched; import org.apache.hugegraph.query.Condition.Relation; import org.apache.hugegraph.query.Condition.RelationType; +import org.apache.hugegraph.query.serializer.AggregateAdapter; import org.apache.hugegraph.query.serializer.QueryAdapter; import org.apache.hugegraph.query.serializer.QueryIdAdapter; import org.apache.hugegraph.structure.BaseElement; @@ -35,12 +44,18 @@ import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.CollectionType; import org.apache.hugegraph.type.define.HugeKeys; -import org.apache.hugegraph.util.*; +import org.apache.hugegraph.util.CollectionUtil; +import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.InsertionOrderUtil; +import org.apache.hugegraph.util.LongEncoding; +import org.apache.hugegraph.util.NumericUtil; import org.apache.hugegraph.util.collection.CollectionFactory; -import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; -import java.util.*; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; public class ConditionQuery extends IdQuery { @@ -57,9 +72,9 @@ public class ConditionQuery extends IdQuery { private static final Gson gson = new GsonBuilder() .registerTypeAdapter(Condition.class, new QueryAdapter()) .registerTypeAdapter(Id.class, new QueryIdAdapter()) + .registerTypeAdapter(Aggregate.class, new AggregateAdapter()) .setDateFormat("yyyy-MM-dd HH:mm:ss.SSS") .create(); - private static final int indexStringValueLength = 20; static { List list = new ArrayList<>(INDEX_SYM_MAX - INDEX_SYM_MIN); @@ -91,91 +106,6 @@ public ConditionQuery(HugeType resultType, Query originQuery) { super(resultType, originQuery); } - /** - * Index and composite index interception - * - * @param values - * @return - */ - public static String concatValuesLimitLength(List values) { - List newValues = new ArrayList<>(values.size()); - for (Object v : values) { - v = convertLargeValue(v); - newValues.add(convertNumberIfNeeded(v)); - } - return SplicingIdGenerator.concatValues(newValues); - } - - /** - * Index and composite index interception - * - * @param value - * @return - */ - public static String concatValuesLimitLength(Object value) { - if (value instanceof List) { - return concatValuesLimitLength((List) value); - } - - if (needConvertNumber(value)) { - return LongEncoding.encodeNumber(value); - } - value = convertLargeValue(value); - return value.toString(); - } - - public static int getIndexStringValueLength() { - return indexStringValueLength; - } - - /** - * Extract the String value - * - * @param v - * @return - */ - private static Object convertLargeValue(Object v) { - - if (Objects.nonNull(v) && v instanceof String && - ((String) v).length() > getIndexStringValueLength()) { - - v = ((String) v).substring(0, getIndexStringValueLength()); - - } - - return v; - } - - private static Object convertNumberIfNeeded(Object value) { - if (needConvertNumber(value)) { - return LongEncoding.encodeNumber(value); - } - return value; - } - - private static boolean removeValue(Set values, Object value) { - for (Object compareValue : values) { - if (numberEquals(compareValue, value)) { - values.remove(compareValue); - return true; - } - } - return false; - } - - private static boolean numberEquals(Object number1, Object number2) { - // Same class compare directly - if (number1.getClass().equals(number2.getClass())) { - return number1.equals(number2); - } - // Otherwise convert to BigDecimal to make two numbers comparable - Number n1 = NumericUtil.convertToNumber(number1); - Number n2 = NumericUtil.convertToNumber(number2); - BigDecimal b1 = BigDecimal.valueOf(n1.doubleValue()); - BigDecimal b2 = BigDecimal.valueOf(n2.doubleValue()); - return b1.compareTo(b2) == 0; - } - public static String concatValues(List values) { assert !values.isEmpty(); List newValues = new ArrayList<>(values.size()); @@ -199,15 +129,8 @@ public static String concatValues(Object value) { } public static ConditionQuery fromBytes(byte[] bytes) { - Gson gson = new GsonBuilder() - .registerTypeAdapter(Condition.class, new QueryAdapter()) - .registerTypeAdapter(Id.class, new QueryIdAdapter()) - .setDateFormat("yyyy-MM-dd HH:mm:ss.SSS") - .create(); - String cqs = new String(bytes, StandardCharsets.UTF_8); - ConditionQuery conditionQuery = gson.fromJson(cqs, ConditionQuery.class); - - return conditionQuery; + return gson.fromJson(new String(bytes, StandardCharsets.UTF_8), + ConditionQuery.class); } private static boolean needConvertNumber(Object value) { @@ -256,11 +179,7 @@ public boolean shard() { return this.shard; } - private void ensureElement2IndexValueMap() { - if (this.element2IndexValueMap == null) { - this.element2IndexValueMap = new Element2IndexValueMap(); - } - } + public ConditionQuery query(Condition condition) { // Query by id (HugeGraph-259) @@ -345,12 +264,10 @@ public void resetConditions() { } public void recordIndexValue(Id propertyId, Id id, Object indexValue) { - this.ensureElement2IndexValueMap(); this.element2IndexValueMap().addIndexValue(propertyId, id, indexValue); } public void selectedIndexField(Id indexField) { - this.ensureElement2IndexValueMap(); this.element2IndexValueMap().selectedIndexField(indexField); } @@ -459,24 +376,32 @@ public boolean containsLabelOrUserpropRelation() { return false; } + /** + * Returns the legacy condition value of the specified key. + * + * This method keeps the historical behavior for existing callers: + *
    + *
  • returns {@code null} if no top-level EQ/IN relation exists
  • + *
  • returns the single EQ value if there is exactly one top-level EQ + * relation and no top-level IN relation
  • + *
  • returns the raw IN list, including an empty list, if there is exactly + * one top-level IN relation and no top-level EQ relation
  • + *
  • returns {@code null} if several top-level EQ/IN relations resolve to + * an empty set
  • + *
  • returns the single value if several relations resolve to one value
  • + *
  • throws if multiple values remain after resolving several relations
  • + *
+ * + * Prefer {@link #conditionValues(Object)}, + * {@link #singleConditionValueOrNull(Object)} or + * {@link #conditionValue(Object)} for new code that needs explicit + * semantics. + */ @Watched public T condition(Object key) { List valuesEQ = InsertionOrderUtil.newList(); List valuesIN = InsertionOrderUtil.newList(); - for (Condition c : this.conditions) { - if (c.isRelation()) { - Condition.Relation r = (Condition.Relation) c; - if (r.key().equals(key)) { - if (r.relation() == RelationType.EQ) { - valuesEQ.add(r.value()); - } else if (r.relation() == RelationType.IN) { - Object value = r.value(); - assert value instanceof List; - valuesIN.add(value); - } - } - } - } + this.collectConditionValues(key, valuesEQ, valuesIN); if (valuesEQ.isEmpty() && valuesIN.isEmpty()) { return null; } @@ -491,35 +416,106 @@ public T condition(Object key) { return value; } - Set intersectValues = InsertionOrderUtil.newSet(); - for (Object value : valuesEQ) { - List valueAsList = ImmutableList.of(value); - if (intersectValues.isEmpty()) { - intersectValues.addAll(valueAsList); - } else { - CollectionUtil.intersectWithModify(intersectValues, - valueAsList); + Set intersectValues = this.resolveConditionValues(valuesEQ, + valuesIN); + + if (intersectValues.isEmpty()) { + return null; + } + E.checkState(intersectValues.size() == 1, + "Illegal key '%s' with more than one value: %s", + key, intersectValues); + @SuppressWarnings("unchecked") + T value = (T) intersectValues.iterator().next(); + return value; + } + + /** + * Returns whether there is any top-level relation for the specified key. + */ + private boolean containsCondition(Object key) { + for (Condition c : this.conditions) { + if (c.isRelation()) { + Condition.Relation r = (Condition.Relation) c; + if (r.key().equals(key)) { + return true; + } } } - for (Object value : valuesIN) { - @SuppressWarnings("unchecked") - List valueAsList = (List) value; - if (intersectValues.isEmpty()) { - intersectValues.addAll(valueAsList); - } else { - CollectionUtil.intersectWithModify(intersectValues, - valueAsList); + return false; + } + + /** + * Returns the resolved candidate values of the specified key from + * top-level EQ/IN relations. + * + * Use {@link #containsConditionValues(Object)} to distinguish "no EQ/IN + * condition" from "EQ/IN conditions exist but resolve to an empty + * intersection". + */ + public Set conditionValues(Object key) { + List valuesEQ = InsertionOrderUtil.newList(); + List valuesIN = InsertionOrderUtil.newList(); + this.collectConditionValues(key, valuesEQ, valuesIN); + if (valuesEQ.isEmpty() && valuesIN.isEmpty()) { + return InsertionOrderUtil.newSet(); + } + return this.resolveConditionValues(valuesEQ, valuesIN); + } + + /** + * Returns whether there is any top-level EQ/IN relation for the specified + * key. + */ + public boolean containsConditionValues(Object key) { + for (Condition c : this.conditions) { + if (c.isRelation()) { + Condition.Relation r = (Condition.Relation) c; + if (r.key().equals(key) && + (r.relation() == RelationType.EQ || + r.relation() == RelationType.IN)) { + return true; + } } } + return false; + } - if (intersectValues.isEmpty()) { + /** + * Returns the unique resolved value of the specified key from top-level + * EQ/IN relations. + * + * Returns {@code null} when the resolved candidate set is empty. Throws + * if multiple values remain after resolution. + */ + public T conditionValue(Object key) { + Set values = this.conditionValues(key); + if (values.isEmpty()) { return null; } - E.checkState(intersectValues.size() == 1, - "Illegal key '%s' with more than one value: %s", - key, intersectValues); + E.checkState(values.size() == 1, + "Illegal key '%s' with more than one value: %s", + key, values); @SuppressWarnings("unchecked") - T value = (T) intersectValues.iterator().next(); + T value = (T) values.iterator().next(); + return value; + } + + /** + * Returns the unique resolved value of the specified key from top-level + * EQ/IN relations, or {@code null} if the resolved candidate set doesn't + * contain exactly one value. + * + * Use this method when callers want "single-or-null" semantics instead of + * treating multiple remaining values as an error. + */ + public T singleConditionValueOrNull(Object key) { + Set values = this.conditionValues(key); + if (values.size() != 1) { + return null; + } + @SuppressWarnings("unchecked") + T value = (T) values.iterator().next(); return value; } @@ -528,15 +524,57 @@ public void unsetCondition(Object key) { } public boolean containsCondition(HugeKeys key) { + return this.containsCondition((Object) key); + } + + public boolean containsConditionValues(HugeKeys key) { + return this.containsConditionValues((Object) key); + } + + private void collectConditionValues(Object key, List valuesEQ, + List valuesIN) { for (Condition c : this.conditions) { if (c.isRelation()) { Condition.Relation r = (Condition.Relation) c; if (r.key().equals(key)) { - return true; + if (r.relation() == RelationType.EQ) { + valuesEQ.add(r.value()); + } else if (r.relation() == RelationType.IN) { + Object value = r.value(); + assert value instanceof List; + valuesIN.add(value); + } } } } - return false; + } + + private Set resolveConditionValues(List valuesEQ, + List valuesIN) { + boolean initialized = false; + Set intersectValues = InsertionOrderUtil.newSet(); + for (Object value : valuesEQ) { + List valueAsList = ImmutableList.of(value); + if (!initialized) { + intersectValues.addAll(valueAsList); + initialized = true; + } else { + CollectionUtil.intersectWithModify(intersectValues, + valueAsList); + } + } + for (Object value : valuesIN) { + @SuppressWarnings("unchecked") + List valueAsList = (List) value; + if (!initialized) { + intersectValues.addAll(valueAsList); + initialized = true; + } else { + CollectionUtil.intersectWithModify(intersectValues, + valueAsList); + } + } + return intersectValues; } public boolean containsCondition(Condition.RelationType type) { @@ -705,30 +743,7 @@ public String userpropValuesString(List fields) { return concatValues(values); } - public String userpropValuesStringForIndex(List fields) { - List values = new ArrayList<>(fields.size()); - for (Id field : fields) { - boolean got = false; - for (Relation r : this.userpropRelations()) { - if (r.key().equals(field) && !r.isSysprop()) { - E.checkState(r.relation() == RelationType.EQ || - r.relation() == RelationType.CONTAINS, - "Method userpropValues(List) only " + - "used for secondary index, " + - "relation must be EQ or CONTAINS, but got %s", - r.relation()); - values.add(r.serialValue()); - got = true; - } - } - if (!got) { - throw new BackendException( - "No such userprop named '%s' in the query '%s'", - field, this); - } - } - return concatValuesLimitLength(values); - } + public Set userpropValues(Id field) { Set values = new HashSet<>(); @@ -856,6 +871,10 @@ public Condition.Relation copyRelationAndUpdateQuery(Object key) { @Override public boolean test(BaseElement element) { + return this.test(element, this.resultsFilter); + } + + public boolean test(BaseElement element, ResultsFilter filter) { if (!this.ids().isEmpty() && !super.test(element)) { return false; } @@ -867,19 +886,25 @@ public boolean test(BaseElement element) { * We can't use sub-query results-filter here for fresh element which is * not committed to backend store, because it's not from a sub-query. */ - if (this.resultsFilter != null && !element.fresh()) { - return this.resultsFilter.test(element); + if (filter != null && !element.fresh()) { + return filter.test(element); } /* * NOTE: seems need to keep call checkRangeIndex() for each condition, * so don't break early even if test() return false. - */ + */ boolean valid = true; + Map rangeIndexMatches = null; + if (this.element2IndexValueMap != null) { + rangeIndexMatches = new HashMap<>(); + } for (Condition cond : this.conditions) { valid &= cond.test(element); - valid &= this.element2IndexValueMap == null || - this.element2IndexValueMap.checkRangeIndex(element, cond); + if (this.element2IndexValueMap != null) { + valid &= this.element2IndexValueMap.checkRangeIndex( + element, cond, rangeIndexMatches); + } } return valid; } @@ -944,6 +969,10 @@ public void registerResultsFilter(ResultsFilter filter) { this.resultsFilter = filter; } + public ResultsFilter resultsFilter() { + return this.resultsFilter; + } + public void updateResultsFilter() { Query originQuery = this.originQuery(); if (originQuery instanceof ConditionQuery) { @@ -1017,15 +1046,7 @@ private static boolean removeFieldValue(Set values, return false; } - private static boolean removeValue(Set values, Object value) { - for (Object compareValue : values) { - if (numberEquals(compareValue, value)) { - values.remove(compareValue); - return true; - } - } - return false; - } + private static boolean numberEquals(Object number1, Object number2) { // Same class compare directly @@ -1068,10 +1089,7 @@ public Set toRemoveIndexValues(Id indexField, Id elementId) { } public Set removeIndexValues(Id indexField, Id elementId) { - if (!this.filed2IndexValues.containsKey(indexField)) { - return null; - } - return this.filed2IndexValues.get(indexField).get(elementId); + return this.toRemoveIndexValues(indexField, elementId); } public void addLeftIndex(Id elementId, Id indexField, @@ -1090,12 +1108,7 @@ public Set getLeftIndex(Id elementId) { public void addLeftIndex(Id indexField, Set indexValues, Id elementId) { - LeftIndex leftIndex = new LeftIndex(indexValues, indexField); - if (this.leftIndexMap.containsKey(elementId)) { - this.leftIndexMap.get(elementId).add(leftIndex); - } else { - this.leftIndexMap.put(elementId, Sets.newHashSet(leftIndex)); - } + this.addLeftIndex(elementId, indexField, indexValues); } public void removeElementLeftIndex(Id elementId) { @@ -1103,25 +1116,36 @@ public void removeElementLeftIndex(Id elementId) { } public boolean checkRangeIndex(BaseElement element, Condition cond) { + return this.checkRangeIndex(element, cond, new HashMap<>()); + } + + public boolean checkRangeIndex(BaseElement element, Condition cond, + Map rangeIndexMatches) { // Not UserpropRelation if (!(cond instanceof Condition.UserpropRelation)) { return true; } - Condition.UserpropRelation propRelation = (Condition.UserpropRelation) cond; + Condition.UserpropRelation propRelation = + (Condition.UserpropRelation) cond; Id propId = propRelation.key(); Set fieldValues = this.toRemoveIndexValues(propId, - element.id()); + element.id()); if (fieldValues == null) { // Not range index return true; } + if (rangeIndexMatches.containsKey(propId)) { + return rangeIndexMatches.get(propId); + } + BaseProperty property = element.getProperty(propId); if (property == null) { // Property value has been deleted, so it's not matched this.addLeftIndex(element.id(), propId, fieldValues); - return false; + return this.cacheRangeIndexMatch(rangeIndexMatches, propId, + false); } /* @@ -1130,7 +1154,7 @@ public boolean checkRangeIndex(BaseElement element, Condition cond) { * wait the left-index to be removed. */ boolean hasRightValue = removeFieldValue(fieldValues, - property.value()); + property.value()); if (!fieldValues.isEmpty()) { this.addLeftIndex(element.id(), propId, fieldValues); } @@ -1142,55 +1166,23 @@ public boolean checkRangeIndex(BaseElement element, Condition cond) { * the element is valid or not. */ if (this.selectedIndexField != null) { - return !propId.equals(this.selectedIndexField) || hasRightValue; + hasRightValue = !propId.equals(this.selectedIndexField) || + hasRightValue; } - return hasRightValue; + return this.cacheRangeIndexMatch(rangeIndexMatches, propId, + hasRightValue); } - public boolean validRangeIndex(BaseElement element, Condition cond) { - // Not UserpropRelation - if (!(cond instanceof Condition.UserpropRelation)) { - return true; - } - - Condition.UserpropRelation propRelation = (Condition.UserpropRelation) cond; - Id propId = propRelation.key(); - Set fieldValues = this.removeIndexValues(propId, - element.id()); - if (fieldValues == null) { - // Not range index - return true; - } - - BaseProperty hugeProperty = element.getProperty(propId); - if (hugeProperty == null) { - // Property value has been deleted - this.addLeftIndex(propId, fieldValues, element.id()); - return false; - } - - /* - * NOTE: If success remove means has correct index, - * we should add left index values to left index map - * waiting to be removed - */ - boolean hasRightValue = removeValue(fieldValues, hugeProperty.value()); - if (fieldValues.size() > 0) { - this.addLeftIndex(propId, fieldValues, element.id()); - } - - /* - * NOTE: When query by more than one range index field, - * if current field is not the selected one, it can only be used to - * determine whether the index values matched, can't determine - * the element is valid or not - */ - if (this.selectedIndexField != null) { - return !propId.equals(this.selectedIndexField) || hasRightValue; - } + private boolean cacheRangeIndexMatch(Map rangeIndexMatches, + Id propertyId, + boolean matched) { + rangeIndexMatches.put(propertyId, matched); + return matched; + } - return hasRightValue; + public boolean validRangeIndex(BaseElement element, Condition cond) { + return this.checkRangeIndex(element, cond, new HashMap<>()); } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/IdPrefixQuery.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/IdPrefixQuery.java similarity index 94% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/IdPrefixQuery.java rename to hugegraph-struct/src/main/java/org/apache/hugegraph/query/IdPrefixQuery.java index c14a07070d..933d1ea9f2 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/IdPrefixQuery.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/IdPrefixQuery.java @@ -15,10 +15,10 @@ * limitations under the License. */ -package org.apache.hugegraph.backend.query; +package org.apache.hugegraph.query; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.structure.HugeElement; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.structure.BaseElement; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.E; @@ -76,7 +76,7 @@ public boolean empty() { } @Override - public boolean test(HugeElement element) { + public boolean test(BaseElement element) { byte[] elem = element.id().asBytes(); int cmp = Bytes.compare(elem, this.start.asBytes()); boolean matchedStart = this.inclusiveStart ? cmp >= 0 : cmp > 0; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/IdQuery.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/IdQuery.java index 1235dfebc0..b07359af4e 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/IdQuery.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/IdQuery.java @@ -29,6 +29,7 @@ import org.apache.hugegraph.util.InsertionOrderUtil; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; public class IdQuery extends Query { @@ -124,4 +125,67 @@ public IdQuery copy() { InsertionOrderUtil.newList(this.ids); return query; } + + public static final class OneIdQuery extends IdQuery { + + private Id id; + + public OneIdQuery(HugeType resultType, Id id) { + super(resultType); + super.mustSortByInput = false; + this.id = id; + } + + public OneIdQuery(Query originQuery, Id id) { + super(originQuery.resultType(), originQuery); + super.mustSortByInput = false; + this.id = id; + } + + public Id id() { + return this.id; + } + + public void resetId(Id id) { + this.id = id; + } + + @Override + public int idsSize() { + return this.id == null ? 0 : 1; + } + + @Override + public Set ids() { + return this.id == null ? ImmutableSet.of() : + ImmutableSet.of(this.id); + } + + @Override + public void resetIds() { + this.id = null; + } + + @Override + public IdQuery query(Id id) { + E.checkArgumentNotNull(id, "Query id can't be null"); + this.id = id; + return this; + } + + @Override + public boolean test(BaseElement element) { + if (this.id == null) { + return true; + } + return this.id.equals(element.id()); + } + + @Override + public IdQuery copy() { + OneIdQuery query = (OneIdQuery) super.copy(); + assert this.id.equals(query.id); + return query; + } + } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/IdRangeQuery.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/IdRangeQuery.java similarity index 94% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/IdRangeQuery.java rename to hugegraph-struct/src/main/java/org/apache/hugegraph/query/IdRangeQuery.java index 781dc72d08..d276d8bc54 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/IdRangeQuery.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/IdRangeQuery.java @@ -15,10 +15,10 @@ * limitations under the License. */ -package org.apache.hugegraph.backend.query; +package org.apache.hugegraph.query; -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.structure.HugeElement; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.structure.BaseElement; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.E; @@ -82,7 +82,7 @@ public boolean empty() { } @Override - public boolean test(HugeElement element) { + public boolean test(BaseElement element) { int cmp1 = Bytes.compare(element.id().asBytes(), this.start.asBytes()); int cmp2 = Bytes.compare(element.id().asBytes(), this.end.asBytes()); return (this.inclusiveStart ? cmp1 >= 0 : cmp1 > 0) && diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Query.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Query.java index 2151cd6d0b..3cf97d4a14 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Query.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Query.java @@ -81,8 +81,7 @@ public class Query implements Cloneable { private List selects = InsertionOrderUtil.newList(); - @Deprecated - private transient Aggregate aggregate; + private Aggregate aggregate; private Query originQuery; @@ -97,10 +96,6 @@ public Query() { } - private static final ThreadLocal capacityContext = new ThreadLocal<>(); - - private static int indexStringValueLength = 20; - public Query(HugeType resultType) { this(resultType, null); } @@ -271,12 +266,12 @@ public Set skipOffsetIfNeeded(Set elems) { if (fromIndex < 0L) { // Skipping offset is overhead, no need to skip fromIndex = 0L; - } else if (fromIndex > 0L) { + } + // An index holder yields ids in batches, only count this batch's ids + fromIndex = Math.min(fromIndex, elems.size()); + if (fromIndex > 0L) { this.goOffset(fromIndex); } - E.checkArgument(fromIndex <= Integer.MAX_VALUE, - "Offset must be <= 0x7fffffff, but got '%s'", - fromIndex); if (fromIndex >= elems.size()) { return ImmutableSet.of(); @@ -447,21 +442,21 @@ public void checkCapacity(long count) throws LimitExceedException { } } - public Aggregate aggregate() { + public Aggregate aggregate() { return this.aggregate; } - public Aggregate aggregateNotNull() { + public Aggregate aggregateNotNull() { E.checkArgument(this.aggregate != null, "The aggregate must be set for number query"); return this.aggregate; } - public void aggregate(AggregateFuncDefine func, String property) { - this.aggregate = new Aggregate(func, property); + public void aggregate(AggregateFuncDefine func, String property) { + this.aggregate = new Aggregate<>(func, property); } - public void aggregate(Aggregate aggregate) { + public void aggregate(Aggregate aggregate) { this.aggregate = aggregate; } @@ -569,7 +564,6 @@ public int hashCode() { Objects.hashCode(this.page) ^ this.ids().hashCode() ^ this.conditions().hashCode() ^ - this.selects().hashCode() ^ Boolean.hashCode(this.withProperties); if (this.resultType == null) { return hash; @@ -665,16 +659,9 @@ public boolean isTaskQuery() { return false; } - public static int getIndexStringValueLength() { - return indexStringValueLength; - } - public static void setIndexStringValueLength(int indexStringValueLengthTmp) { - if (indexStringValueLengthTmp <= 1) { - indexStringValueLengthTmp = 20; - } - indexStringValueLength = indexStringValueLengthTmp; - } + + public void select(Id id) { if (!this.selects.contains(id)) { diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/serializer/AggregateAdapter.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/serializer/AggregateAdapter.java new file mode 100644 index 0000000000..c2fd2196a0 --- /dev/null +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/serializer/AggregateAdapter.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.hugegraph.query.serializer; + +import java.lang.reflect.Type; +import java.util.Locale; + +import org.apache.hugegraph.query.Aggregate; +import org.apache.hugegraph.query.Aggregate.AggregateFunc; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** Preserves the original query aggregate descriptor without Java class names. */ +public final class AggregateAdapter implements JsonSerializer>, + JsonDeserializer> { + + @Override + public JsonElement serialize(Aggregate aggregate, Type type, + JsonSerializationContext context) { + JsonObject result = new JsonObject(); + // Both the shared enum and external strategies expose their function name. + // Only names supported by the original query protocol may cross the wire. + AggregateFunc func = function(aggregate.func().string().toUpperCase(Locale.ROOT)); + result.addProperty("func", func.name()); + if (aggregate.column() != null) { + result.addProperty("column", aggregate.column()); + } + return result; + } + + @Override + public Aggregate deserialize(JsonElement json, Type type, + JsonDeserializationContext context) { + JsonObject aggregate = json.getAsJsonObject(); + AggregateFunc func = function(aggregate.get("func").getAsString()); + JsonElement column = aggregate.get("column"); + return new Aggregate<>(func, column == null || column.isJsonNull() ? null : + column.getAsString()); + } + + private static AggregateFunc function(String name) { + try { + return AggregateFunc.valueOf(name); + } catch (IllegalArgumentException e) { + throw new JsonParseException("Unsupported aggregate function: " + name, e); + } + } +} diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/serializer/QueryAdapter.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/serializer/QueryAdapter.java index e9975f57cd..888549effd 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/serializer/QueryAdapter.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/serializer/QueryAdapter.java @@ -25,6 +25,7 @@ import org.apache.hugegraph.query.Condition; import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.util.LegacyClassNames; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonDeserializationContext; @@ -35,6 +36,7 @@ import com.google.gson.JsonSerializationContext; import com.google.gson.reflect.TypeToken; +// Historical query values use Java class names; readers retain their legacy mappings. public class QueryAdapter extends AbstractSerializerAdapter { static ImmutableMap cls = @@ -55,6 +57,13 @@ static boolean isPrimitive(Class clz) { } } + // Old server queries include Java class names in relation values. + // Resolve only the migrated names here; no legacy implementation is needed. + private static Class valueClass(String name) throws ClassNotFoundException { + Class mapped = LegacyClassNames.lookup(name); + return mapped != null ? mapped : Class.forName(name); + } + @Override public Map validType() { return cls; @@ -72,7 +81,7 @@ public Condition deserialize(JsonElement json, Type typeOfT, JsonDeserialization if (valueElement.isJsonObject()) { String cls = valueElement.getAsJsonObject().get("cls").getAsString(); try { - Class actualClass = Class.forName(cls); + Class actualClass = valueClass(cls); Object obj = context.deserialize(valueElement, actualClass); ((Condition.Relation) condition).value(obj); } catch (ClassNotFoundException e) { @@ -82,7 +91,7 @@ public Condition deserialize(JsonElement json, Type typeOfT, JsonDeserialization if (valueElement.isJsonArray()) { String cls = elElement.getAsJsonObject().get("valuecls").getAsString(); try { - Class actualClass = Class.forName(cls); + Class actualClass = valueClass(cls); Type type = TypeToken.getParameterized(ArrayList.class, actualClass) .getType(); Object value = context.deserialize(valueElement, type); @@ -93,7 +102,7 @@ public Condition deserialize(JsonElement json, Type typeOfT, JsonDeserialization } else { String cls = elElement.getAsJsonObject().get("valuecls").getAsString(); try { - Class actualClass = Class.forName(cls); + Class actualClass = valueClass(cls); Object obj = context.deserialize(valueElement, actualClass); ((Condition.Relation) condition).value(obj); } catch (ClassNotFoundException e) { diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/BinaryElementSerializer.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/BinaryElementSerializer.java index 03229b42f7..82f2f8952e 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/BinaryElementSerializer.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/BinaryElementSerializer.java @@ -24,18 +24,19 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collection; -import java.util.Map; +import java.util.function.BiConsumer; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.NotImplementedException; import org.apache.hugegraph.HugeGraphSupplier; import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.backend.BinaryId; -import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.id.EdgeId; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.struct.schema.SchemaElement; import org.apache.hugegraph.struct.schema.VertexLabel; @@ -43,10 +44,14 @@ import org.apache.hugegraph.structure.BaseElement; import org.apache.hugegraph.structure.BaseProperty; import org.apache.hugegraph.structure.BaseVertex; +import org.apache.hugegraph.structure.BaseVertex.TypeContext; import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.serializer.BytesBuffer.IndexColumnName; +import org.apache.hugegraph.serializer.BytesBuffer.IndexExpiryLayout; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.EdgeLabelType; +import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; @@ -107,32 +112,46 @@ public static Id ownerId(Index index) { protected void parseProperty(HugeGraphSupplier graph, Id pkeyId, - BytesBuffer buffer, - BaseElement owner) { - PropertyKey pkey = graph != null ? - graph.propertyKey(pkeyId) : - new PropertyKey(graph, pkeyId, ""); - // Parse value - Object value = buffer.readProperty(pkey); - // Set properties of vertex/edge - if (pkey.cardinality() == Cardinality.SINGLE) { - owner.addProperty(pkey, value); - } else { - if (!(value instanceof Collection)) { - throw new HugeException( - "Invalid value of non-single property: %s", value); - } - owner.addProperty(pkey, value); + BytesBuffer buffer, BaseElement owner) { + this.parseProperty(graph, pkeyId, buffer, true, owner::addProperty); + } + + public void parseSchemaProperty(HugeGraphSupplier graph, Id pkeyId, + BytesBuffer buffer, + BiConsumer propertySink) { + E.checkArgumentNotNull(graph, "Schema property decoding requires a schema supplier"); + this.parseProperty(graph, pkeyId, buffer, false, propertySink); + } + + private void parseProperty(HugeGraphSupplier graph, Id pkeyId, + BytesBuffer buffer, boolean tagged, + BiConsumer propertySink) { + PropertyKey pkey = graph != null ? graph.propertyKey(pkeyId) : + new PropertyKey(null, pkeyId, ""); + Object value = tagged ? buffer.readProperty(pkey) : buffer.readSchemaProperty(pkey); + if (pkey.cardinality() != Cardinality.SINGLE && !(value instanceof Collection)) { + throw new BackendException("Invalid value of non-single property: %s", value); } + propertySink.accept(pkey, value); } public void parseProperties(HugeGraphSupplier graph, BytesBuffer buffer, BaseElement owner) { + this.parseProperties(graph, buffer, true, owner::addProperty); + } + + public void parseSchemaProperties(HugeGraphSupplier graph, BytesBuffer buffer, + BiConsumer propertySink) { + E.checkArgumentNotNull(graph, "Schema property decoding requires a schema supplier"); + this.parseProperties(graph, buffer, false, propertySink); + } + + private void parseProperties(HugeGraphSupplier graph, BytesBuffer buffer, + boolean tagged, BiConsumer propertySink) { int size = buffer.readVInt(); assert size >= 0; for (int i = 0; i < size; i++) { - Id pkeyId = IdGenerator.of(buffer.readVInt()); - this.parseProperty(graph, pkeyId, buffer, owner); + this.parsePropertyRecord(graph, buffer, tagged, propertySink); } } @@ -145,74 +164,131 @@ public void parseProperties(HugeGraphSupplier graph, BytesBuffer buffer, */ public BaseVertex parseVertex(HugeGraphSupplier graph, BackendColumn vertexCol, BaseVertex vertex) { + return this.parseVertex(graph, vertexCol, vertex, true, TypeContext.STORAGE); + } + + /** Decode a persisted Server row using schema-supplied property metadata. */ + public BaseVertex parseSchemaVertex(HugeGraphSupplier graph, BackendColumn vertexCol, + BaseVertex vertex) { + return this.parseSchemaVertex(graph, vertexCol, vertex, TypeContext.STORAGE); + } + + public BaseVertex parseSchemaVertex(HugeGraphSupplier graph, BackendColumn vertexCol, + BaseVertex vertex, TypeContext typeContext) { + E.checkArgumentNotNull(graph, "Schema vertex decoding requires a schema supplier"); + return this.parseVertex(graph, vertexCol, vertex, false, typeContext); + } + + private BaseVertex parseVertex(HugeGraphSupplier graph, BackendColumn vertexCol, + BaseVertex vertex, boolean tagged, TypeContext typeContext) { if (vertex == null) { BinaryId binaryId = BytesBuffer.wrap(vertexCol.name).parseId(HugeType.VERTEX); - vertex = new BaseVertex(binaryId.origin(), VertexLabel.NONE); + vertex = new BaseVertex(binaryId.origin(), VertexLabel.NONE, typeContext); } if (ArrayUtils.isEmpty(vertexCol.value)) { // No need to parse vertex properties return vertex; } - BytesBuffer buffer = BytesBuffer.wrap(vertexCol.value); - Id labelId = buffer.readId(); - // Parse vertex label - if (graph != null) { - VertexLabel label = graph.vertexLabelOrNone(labelId); - vertex.correctVertexLabel(label); - } else { - VertexLabel label = new VertexLabel(null, labelId, UNDEF); - vertex.correctVertexLabel(label); - } - // Parse properties - this.parseProperties(graph, buffer, vertex); - - // Parse vertex expired time if needed - if (buffer.remaining() > 0 /*edge.hasTtl()*/) { - this.parseExpiredTime(buffer, vertex); - } + this.parseVertexValue(graph, vertexCol.value, vertex, tagged, vertex::addProperty); return vertex; } + public void parseSchemaVertexValue(HugeGraphSupplier graph, byte[] value, + BaseVertex vertex, + BiConsumer propertySink) { + E.checkArgumentNotNull(graph, "Schema vertex decoding requires a schema supplier"); + this.parseVertexValue(graph, value, vertex, false, propertySink); + } + + private void parseVertexValue(HugeGraphSupplier graph, byte[] value, BaseVertex vertex, + boolean tagged, BiConsumer propertySink) { + BytesBuffer buffer = BytesBuffer.wrap(value); + Id labelId = buffer.readId(); + VertexLabel label = graph != null ? graph.vertexLabelOrNone(labelId) : + new VertexLabel(null, labelId, UNDEF); + vertex.correctVertexLabel(label); + this.parseElementValue(graph, buffer, vertex, tagged, propertySink); + } + /** * Reverse sequence the vertex kv data into vertices of type BaseVertex * * @param olapVertexCol It must be a column of vertex data - * @param vertex When vertex==null, it is used for operator sinking to reverse sequence the col data into olapBaseVertex. - * vertex! When =null, add the col information to olapBaseVertex + * @param vertex The destination vertex; null creates one from the OLAP key. */ public BaseVertex parseVertexOlap(HugeGraphSupplier graph, BackendColumn olapVertexCol, BaseVertex vertex) { + return this.parseVertexOlap(graph, olapVertexCol, vertex, true, TypeContext.STORAGE); + } + + public BaseVertex parseSchemaVertexOlap(HugeGraphSupplier graph, + BackendColumn olapVertexCol, BaseVertex vertex) { + return this.parseSchemaVertexOlap(graph, olapVertexCol, vertex, TypeContext.STORAGE); + } + + public BaseVertex parseSchemaVertexOlap(HugeGraphSupplier graph, + BackendColumn olapVertexCol, BaseVertex vertex, + TypeContext typeContext) { + E.checkArgumentNotNull(graph, "Schema OLAP decoding requires a schema supplier"); + return this.parseVertexOlap(graph, olapVertexCol, vertex, false, typeContext); + } + + private BaseVertex parseVertexOlap(HugeGraphSupplier graph, BackendColumn olapVertexCol, + BaseVertex vertex, boolean tagged, TypeContext typeContext) { if (vertex == null) { BytesBuffer idBuffer = BytesBuffer.wrap(olapVertexCol.name); - // read olap property id - idBuffer.readId(); - // read vertex id which olap property belongs to + // Transient Store OLAP keys include the property ID; Server's + // persisted OLAP column name contains only the vertex ID. + if (tagged) { + idBuffer.readId(); + } Id vertexId = idBuffer.readId(); - vertex = new BaseVertex(vertexId, VertexLabel.NONE); + vertex = new BaseVertex(vertexId, VertexLabel.NONE, typeContext); } - BytesBuffer buffer = BytesBuffer.wrap(olapVertexCol.value); - Id pkeyId = IdGenerator.of(buffer.readVInt()); - this.parseProperty(graph, pkeyId, buffer, vertex); + this.parsePropertyRecord(graph, BytesBuffer.wrap(olapVertexCol.value), + tagged, vertex::addProperty); return vertex; } + public void parseSchemaPropertyRecord(HugeGraphSupplier graph, BytesBuffer buffer, + BiConsumer propertySink) { + E.checkArgumentNotNull(graph, "Schema property decoding requires a schema supplier"); + this.parsePropertyRecord(graph, buffer, false, propertySink); + } + + private void parsePropertyRecord(HugeGraphSupplier graph, BytesBuffer buffer, + boolean tagged, BiConsumer propertySink) { + Id pkeyId = IdGenerator.of(buffer.readVInt()); + this.parseProperty(graph, pkeyId, buffer, tagged, propertySink); + } + /** - * @param cols Deserializing a complete vertex may require multiple cols - * The first col represents the common vertex information in the g+v table, and each subsequent col represents the olap vertices stored in the olap table + * @param cols The first column contains vertex data; subsequent columns contain OLAP properties. */ public BaseVertex parseVertexFromCols(HugeGraphSupplier graph, BackendColumn... cols) { + return this.parseVertexFromCols(graph, true, cols); + } + + public BaseVertex parseSchemaVertexFromCols(HugeGraphSupplier graph, + BackendColumn... cols) { + E.checkArgumentNotNull(graph, "Schema vertex decoding requires a schema supplier"); + return this.parseVertexFromCols(graph, false, cols); + } + + private BaseVertex parseVertexFromCols(HugeGraphSupplier graph, boolean tagged, + BackendColumn... cols) { assert cols.length > 0; BaseVertex vertex = null; for (int index = 0; index < cols.length; index++) { BackendColumn col = cols[index]; if (index == 0) { - vertex = this.parseVertex(graph, col, vertex); + vertex = this.parseVertex(graph, col, vertex, tagged, TypeContext.STORAGE); } else { - this.parseVertexOlap(graph, col, vertex); + this.parseVertexOlap(graph, col, vertex, tagged, TypeContext.STORAGE); } } return vertex; @@ -235,48 +311,38 @@ public Id parseLabelFromCol(BackendColumn col, boolean isVertex) { } public BaseEdge parseEdge(HugeGraphSupplier graph, BackendColumn edgeCol, - BaseVertex ownerVertex, - boolean withEdgeProperties) { - // owner-vertex + dir + edge-label.id() + subLabel.id() + - // + sort-values + other-vertex - - BytesBuffer buffer = BytesBuffer.wrap(edgeCol.name); - // Consume owner-vertex id - Id id = buffer.readId(); - if (ownerVertex == null) { - ownerVertex = new BaseVertex(id, VertexLabel.NONE); - } - - E.checkState(buffer.remaining() > 0, "Missing column type"); + BaseVertex ownerVertex, boolean withEdgeProperties) { + return this.parseEdge(graph, edgeCol, ownerVertex, withEdgeProperties, + true, TypeContext.STORAGE); + } - byte type = buffer.read(); - if (type == HugeType.EDGE_IN.code() || - type == HugeType.EDGE_OUT.code()) { - E.checkState(true, - "Invalid column(%s) with unknown type(%s): 0x%s", - id, type & 0xff, Bytes.toHex(edgeCol.name)); - } + public BaseEdge parseSchemaEdge(HugeGraphSupplier graph, BackendColumn edgeCol, + BaseVertex ownerVertex, boolean withEdgeProperties) { + return this.parseSchemaEdge(graph, edgeCol, ownerVertex, withEdgeProperties, + TypeContext.STORAGE); + } - Id labelId = buffer.readId(); - Id subLabelId = buffer.readId(); - String sortValues = buffer.readStringWithEnding(); - Id otherVertexId = buffer.readId(); - boolean direction = EdgeId.isOutDirectionFromCode(type); - BaseEdge edge; - EdgeLabel edgeLabel; - if (graph == null) { /* when calculation sinking */ - edgeLabel = new EdgeLabel(null, subLabelId, UNDEF); - // If not equal here, need to add fatherId for correct operator sinking - if (subLabelId != labelId) { - edgeLabel.edgeLabelType(EdgeLabelType.SUB); - edgeLabel.fatherId(labelId); - } + public BaseEdge parseSchemaEdge(HugeGraphSupplier graph, BackendColumn edgeCol, + BaseVertex ownerVertex, boolean withEdgeProperties, + TypeContext typeContext) { + E.checkArgumentNotNull(graph, "Schema edge decoding requires a schema supplier"); + return this.parseEdge(graph, edgeCol, ownerVertex, withEdgeProperties, + false, typeContext); + } - } else { - edgeLabel = graph.edgeLabelOrNone(subLabelId); + private BaseEdge parseEdge(HugeGraphSupplier graph, BackendColumn edgeCol, + BaseVertex ownerVertex, boolean withEdgeProperties, + boolean tagged, TypeContext typeContext) { + EdgeId id = BytesBuffer.wrap(edgeCol.name).readEdgeId(true, null); + if (ownerVertex == null) { + ownerVertex = new BaseVertex(id.ownerVertexId(), VertexLabel.NONE, typeContext); } + boolean direction = id.direction() == Directions.OUT; + EdgeLabel edgeLabel = this.edgeLabel(graph, id); + BaseEdge edge; edge = BaseEdge.constructEdge(graph, ownerVertex, direction, - edgeLabel, sortValues, otherVertexId); + edgeLabel, id.sortValues(), id.otherVertexId()); + edge.otherVertex().typeContext(typeContext); if (!withEdgeProperties /*&& !edge.hasTtl()*/) { // only skip properties for edge without ttl @@ -289,19 +355,42 @@ public BaseEdge parseEdge(HugeGraphSupplier graph, BackendColumn edgeCol, return edge; } - // Parse edge-id + edge-properties - buffer = BytesBuffer.wrap(edgeCol.value); + this.parseElementValue(graph, BytesBuffer.wrap(edgeCol.value), edge, + tagged, edge::addProperty); + return edge; + } - // Parse edge properties - this.parseProperties(graph, buffer, edge); + public EdgeLabel edgeLabel(HugeGraphSupplier graph, EdgeId id) { + return this.edgeLabel(graph, id, graph); + } - /* Skip TTL parsing process first - * Can't determine if edge has TTL through edge, need to judge by bytebuffer length */ -// // Parse edge expired time if needed - if (buffer.remaining() > 0 /*edge.hasTtl()*/) { - this.parseExpiredTime(buffer, edge); + public EdgeLabel edgeLabel(HugeGraphSupplier lookupGraph, EdgeId id, + HugeGraphSupplier schemaGraph) { + if (lookupGraph != null) { + return lookupGraph.edgeLabelOrNone(id.subLabelId()); + } + EdgeLabel label = new EdgeLabel(schemaGraph, id.subLabelId(), UNDEF); + if (id.subLabelId() != id.edgeLabelId()) { + label.edgeLabelType(EdgeLabelType.SUB); + label.fatherId(id.edgeLabelId()); + } + return label; + } + + public void parseSchemaEdgeValue(HugeGraphSupplier graph, byte[] value, + BaseEdge edge, + BiConsumer propertySink) { + E.checkArgumentNotNull(graph, "Schema edge decoding requires a schema supplier"); + this.parseElementValue(graph, BytesBuffer.wrap(value), edge, false, propertySink); + } + + private void parseElementValue(HugeGraphSupplier graph, BytesBuffer buffer, + BaseElement element, boolean tagged, + BiConsumer propertySink) { + this.parseProperties(graph, buffer, tagged, propertySink); + if (tagged ? buffer.remaining() > 0 : element.hasTtl()) { + this.parseExpiredTime(buffer, element); } - return edge; } /** @@ -310,19 +399,45 @@ public BaseEdge parseEdge(HugeGraphSupplier graph, BackendColumn edgeCol, */ public Index parseIndex(HugeGraphSupplier graph, BackendColumn indexCol, Index index) { - HugeType indexType = parseIndexType(indexCol); + return this.parseIndex(graph, indexCol, index, false); + } + + /** Decode Server index rows, whose optional expiry is in the column name. */ + public Index parseSchemaIndex(HugeGraphSupplier graph, BackendColumn indexCol) { + E.checkArgumentNotNull(graph, "Schema index decoding requires a schema supplier"); + return this.parseIndex(graph, indexCol, null, true); + } - BytesBuffer buffer = BytesBuffer.wrap(indexCol.name); - BinaryId indexId = buffer.readIndexId(indexType); - Id elemId = buffer.readId(); + private Index parseIndex(HugeGraphSupplier graph, BackendColumn indexCol, + Index index, boolean schema) { + HugeType indexType = parseIndexType(indexCol); + IndexColumnName name = BytesBuffer.wrap(indexCol.name).readIndexColumnName( + indexType, true, schema ? IndexExpiryLayout.OPTIONAL : IndexExpiryLayout.NONE); if (index == null) { - index = Index.parseIndexId(graph, indexType, indexId.asBytes()); + index = Index.parseIndexId(graph, indexType, name.indexId().asBytes()); } - long expiredTime = 0L; - - if (indexCol.value.length > 0) { + long expiredTime = name.expiredTime(); + + if (schema) { + boolean hasTtl = !index.indexLabel().system() && + index.indexLabel().baseLabel().ttl() > 0; + Long legacyExpiredTime = null; + if (!name.hasExpiredTime()) { + // Legacy Store writers kept TTL in an exact value envelope. + // Only use it after the index/element name fields end, never + // search arbitrary field-value bytes for a delimiter. + legacyExpiredTime = this.legacyStoreIndexExpiredTime(indexCol.value); + E.checkState(!hasTtl || legacyExpiredTime != null, + "Missing TTL in schema index column"); + expiredTime = legacyExpiredTime == null ? 0L : legacyExpiredTime; + } + if (legacyExpiredTime == null && indexType.isStringIndex() && + !ArrayUtils.isEmpty(indexCol.value)) { + index.fieldValues(StringEncoding.decode(indexCol.value)); + } + } else if (indexCol.value.length > 0) { // Get delimiter address int delimiterIndex = @@ -355,10 +470,33 @@ public Index parseIndex(HugeGraphSupplier graph, BackendColumn indexCol, } } - index.elementIds(elemId, expiredTime); + index.elementIds(name.elementId(), expiredTime); return index; } + /** Decode the existing Store label envelope only at a complete label-index key boundary. */ + public Long storedLabelIndexExpiredTime(HugeType type, boolean atNameEnd, byte[] value) { + return type.isLabelIndex() && atNameEnd ? this.legacyStoreIndexExpiredTime(value) : null; + } + + private Long legacyStoreIndexExpiredTime(byte[] value) { + // A Store TTL writer emits exactly 0x00 + Base64(long), with no fields. + if (value == null || value.length != 13 || value[0] != BytesBuffer.STRING_ENDING_BYTE) { + return null; + } + try { + byte[] expiry = Base64.getDecoder().decode(Arrays.copyOfRange(value, 1, value.length)); + if (expiry.length != Longs.BYTES || + !Arrays.equals(Base64.getEncoder().encode(expiry), + Arrays.copyOfRange(value, 1, value.length))) { + return null; + } + return Longs.fromByteArray(expiry); + } catch (IllegalArgumentException ignored) { + return null; + } + } + public BackendColumn parseIndex(BackendColumn indexCol) { // Self-parsing index throw new NotImplementedException( @@ -373,21 +511,19 @@ public BackendColumn writeVertex(BaseVertex vertex) { BytesBuffer bufferName = BytesBuffer.allocate(vertex.id().length()); bufferName.writeId(vertex.id()); - int propsCount = vertex.getProperties().size(); - BytesBuffer buffer = BytesBuffer.allocate(8 + 16 * propsCount); - - // Write vertex label - buffer.writeId(vertex.schemaLabel().id()); - - // Write all properties of the vertex - this.formatProperties(vertex.getProperties().values(), buffer); + return BackendColumn.of(bufferName.bytes(), this.formatVertexValue(vertex, true)); + } - // Write vertex expired time if needed - if (vertex.hasTtl()) { - this.formatExpiredTime(vertex.expiredTime(), buffer); - } + public byte[] formatSchemaVertexValue(BaseVertex vertex) { + return this.formatVertexValue(vertex, false); + } - return BackendColumn.of(bufferName.bytes(), buffer.bytes()); + private byte[] formatVertexValue(BaseVertex vertex, boolean tagged) { + BytesBuffer buffer = BytesBuffer.allocate(8 + 16 * vertex.sizeOfProperties()); + buffer.writeId(vertex.schemaLabel().id()); + this.formatElementProperties(vertex, buffer, tagged); + this.formatElementExpiredTime(vertex, buffer); + return buffer.bytes(); } public BackendColumn writeOlapVertex(BaseVertex vertex) { @@ -396,18 +532,9 @@ public BackendColumn writeOlapVertex(BaseVertex vertex) { BaseProperty baseProperty = vertex.getProperties().values() .iterator().next(); PropertyKey propertyKey = baseProperty.propertyKey(); - buffer.writeVInt(SchemaElement.schemaId(propertyKey.id())); - buffer.writeProperty(propertyKey.cardinality(), propertyKey.dataType(), - baseProperty.value()); - - // OLAP table merge, key is {property_key_id}{vertex_id} - BytesBuffer bufferName = - BytesBuffer.allocate(1 + propertyKey.id().length() + 1 + - vertex.id().length()); - bufferName.writeId(propertyKey.id()); - bufferName.writeId(vertex.id()).bytes(); + this.formatProperty(baseProperty, buffer, true); - return BackendColumn.of(bufferName.bytes(), buffer.bytes()); + return BackendColumn.of(OlapKey.format(propertyKey.id(), vertex.id()), buffer.bytes()); } public BackendColumn writeEdge(BaseEdge edge) { @@ -424,6 +551,49 @@ public BackendColumn writeIndex(Index index) { formatIndexValue(index)); } + /** Write Store-owned rows with stable keys and the existing storage label-expiry envelope. */ + public BackendColumn writeSchemaIndex(Index index) { + BackendColumn column = this.formatSchemaIndex( + index.type(), index.indexLabelId(), index.fieldValues(), index.elementId(), + true, index.hasTtl(), index.expiredTime()); + if (this.storageLabelHasTtl(index)) { + // Preserve label-index keys so Core append/remove addresses the same row. + column.value = this.formatIndexValue(index); + } + return column; + } + + public BackendColumn formatSchemaIndex(HugeType type, Id indexLabelId, Object fieldValues, + Id elementId, boolean withIdPrefix, + boolean hasTtl, long expiredTime) { + Id rawId = Index.formatIndexId(type, indexLabelId, fieldValues); + boolean hashed = !type.isNumericIndex() && indexIdLengthExceedLimit(rawId); + Id indexId = schemaIndexId(type, indexLabelId, fieldValues); + int capacity = 1 + elementId.length() + (withIdPrefix ? 1 + indexId.length() : 0); + BytesBuffer name = BytesBuffer.allocate(capacity); + if (withIdPrefix) { + name.writeIndexId(indexId, type); + } + name.writeId(elementId); + if (hasTtl) { + name.writeVLong(expiredTime); + } + byte[] value = hashed ? StringEncoding.encode(fieldValues.toString()) : BytesBuffer.BYTES_EMPTY; + return BackendColumn.of(name.bytes(), value); + } + + public static Id schemaIndexId(HugeType type, Id indexLabelId, Object fieldValues) { + Id id = Index.formatIndexId(type, indexLabelId, fieldValues); + if (!type.isNumericIndex() && indexIdLengthExceedLimit(id)) { + id = Index.formatIndexHashId(type, indexLabelId, fieldValues); + } + return id; + } + + public static boolean indexIdLengthExceedLimit(Id id) { + return id.asBytes().length > BytesBuffer.INDEX_HASH_ID_THRESHOLD; + } + private byte[] formatIndexName(Index index) { BytesBuffer buffer; Id elemId = index.elementId(); @@ -444,8 +614,13 @@ private byte[] formatIndexName(Index index) { * @return format * | empty(field-value) | 0x00 | base64(expiredtime) | */ + private boolean storageLabelHasTtl(Index index) { + return (index.indexLabel() == IndexLabel.label(HugeType.VERTEX) || + index.indexLabel() == IndexLabel.label(HugeType.EDGE)) && index.expiredTime() > 0; + } + private byte[] formatIndexValue(Index index) { - if (index.hasTtl()) { + if (this.storageLabelHasTtl(index) || index.hasTtl()) { BytesBuffer valueBuffer = BytesBuffer.allocate(14); valueBuffer.write(BytesBuffer.STRING_ENDING_BYTE); @@ -494,32 +669,64 @@ public byte[] formatEdgeName(BaseEdge edge) { } protected byte[] formatEdgeValue(BaseEdge edge) { - Map> properties = edge.getProperties(); - int propsCount = properties.size(); - BytesBuffer buffer = BytesBuffer.allocate(4 + 16 * propsCount); + return this.formatEdgeValue(edge, true); + } - // Write edge properties - this.formatProperties(properties.values(), buffer); + public byte[] formatSchemaEdgeValue(BaseEdge edge) { + return this.formatEdgeValue(edge, false); + } - // Write edge expired time if needed - if (edge.hasTtl()) { - this.formatExpiredTime(edge.expiredTime(), buffer); + private byte[] formatEdgeValue(BaseEdge edge, boolean tagged) { + BytesBuffer buffer = BytesBuffer.allocate(4 + 16 * edge.sizeOfProperties()); + this.formatElementProperties(edge, buffer, tagged); + this.formatElementExpiredTime(edge, buffer); + return buffer.bytes(); + } + + private void formatElementProperties(BaseElement element, BytesBuffer buffer, + boolean tagged) { + if (tagged) { + this.formatProperties(element.getProperties().values(), buffer); + } else { + this.formatSchemaProperties(element, buffer); } + } - return buffer.bytes(); + private void formatElementExpiredTime(BaseElement element, BytesBuffer buffer) { + if (element.hasTtl()) { + this.formatExpiredTime(element.expiredTime(), buffer); + } } public void formatProperties(Collection> props, BytesBuffer buffer) { - // Write properties size - buffer.writeVInt(props.size()); + this.formatProperties(props, props.size(), buffer, true); + } - // Write properties data + public void formatSchemaProperties(BaseElement owner, BytesBuffer buffer) { + this.formatProperties(owner.properties().values(), owner.sizeOfProperties(), buffer, false); + } + + private void formatProperties(Iterable> props, int size, + BytesBuffer buffer, boolean tagged) { + buffer.writeVInt(size); for (BaseProperty property : props) { - PropertyKey pkey = property.propertyKey(); - buffer.writeVInt(SchemaElement.schemaId(pkey.id())); - buffer.writeProperty(pkey.cardinality(), pkey.dataType(), - property.value()); + this.formatProperty(property, buffer, tagged); + } + } + + public void formatSchemaProperty(BaseProperty property, BytesBuffer buffer) { + this.formatProperty(property, buffer, false); + } + + private void formatProperty(BaseProperty property, BytesBuffer buffer, + boolean tagged) { + PropertyKey pkey = property.propertyKey(); + buffer.writeVInt(SchemaElement.schemaId(pkey.id())); + if (tagged) { + buffer.writeProperty(pkey, property.value()); + } else { + buffer.writeSchemaProperty(pkey, property.value()); } } @@ -527,7 +734,7 @@ public void formatExpiredTime(long expiredTime, BytesBuffer buffer) { buffer.writeVLong(expiredTime); } - protected void parseExpiredTime(BytesBuffer buffer, BaseElement element) { + public void parseExpiredTime(BytesBuffer buffer, BaseElement element) { element.expiredTime(buffer.readVLong()); } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/BytesBuffer.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/BytesBuffer.java index 4ec6aad194..050ad75031 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/BytesBuffer.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/BytesBuffer.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.serializer; @@ -28,11 +26,11 @@ import java.util.LinkedHashSet; import java.util.UUID; -import org.apache.hugegraph.backend.BinaryId; import org.apache.hugegraph.id.EdgeId; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.id.Id.IdType; import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.backend.BinaryId; import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; @@ -41,6 +39,7 @@ import org.apache.hugegraph.util.Blob; import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.KryoUtil; import org.apache.hugegraph.util.StringEncoding; /** @@ -55,31 +54,44 @@ public class BytesBuffer extends OutputStream { public static final int CHAR_LEN = Character.BYTES; public static final int FLOAT_LEN = Float.BYTES; public static final int DOUBLE_LEN = Double.BYTES; - public static final int BLOB_LEN = 4; + public static final int BYTES_LEN = 4; + public static final int BLOB_LEN = 5; public static final int UINT8_MAX = ((byte) -1) & 0xff; public static final int UINT16_MAX = ((short) -1) & 0xffff; public static final long UINT32_MAX = (-1) & 0xffffffffL; - public static final long WRITE_BYTES_MAX_LENGTH = 10 * Bytes.MB; + // TODO: support user-defined configuration // NOTE: +1 to let code 0 represent length 1 - public static final int ID_LEN_MAX = 0x7fff + 1; + public static final int ID_LEN_MAX = 0x3fff + 1; // 16KB + public static final int EID_LEN_MAX = 64 * 1024; public static final int BIG_ID_LEN_MAX = 0xfffff + 1; public static final byte STRING_ENDING_BYTE = (byte) 0x00; public static final byte STRING_ENDING_BYTE_FF = (byte) 0xff; + + // TODO: support user-defined configuration + public static final long BYTES_LEN_MAX = 10 * Bytes.MB; + public static final long WRITE_BYTES_MAX_LENGTH = BYTES_LEN_MAX; public static final int STRING_LEN_MAX = UINT16_MAX; - public static final long BLOB_LEN_MAX = Bytes.GB; + public static final long BLOB_LEN_MAX = 1 * Bytes.GB; + + public static final int MAX_PROPERTIES = BytesBuffer.UINT16_MAX; // The value must be in range [8, ID_LEN_MAX] public static final int INDEX_HASH_ID_THRESHOLD = 32; public static final int DEFAULT_CAPACITY = 64; public static final int MAX_BUFFER_CAPACITY = 128 * 1024 * 1024; // 128M + public static final int MAX_BUFFER_CAPACITY_UPPER_BOUND = (int) Bytes.GB; public static final int BUF_EDGE_ID = 128; public static final int BUF_PROPERTY = 64; + public static final byte[] BYTES_EMPTY = new byte[0]; + + private static volatile Integer maxBufferCapacity; + private ByteBuffer buffer; private final boolean resize; @@ -88,9 +100,12 @@ public BytesBuffer() { } public BytesBuffer(int capacity) { - E.checkArgument(capacity <= MAX_BUFFER_CAPACITY, - "Capacity exceeds max buffer capacity: %s", - MAX_BUFFER_CAPACITY); + int maxCapacity = maxBufferCapacity(); + if (capacity > maxCapacity) { + E.checkArgument(false, + "Capacity %s exceeds max buffer capacity: %s", + capacity, maxCapacity); + } this.buffer = ByteBuffer.allocate(capacity); this.resize = true; } @@ -117,6 +132,43 @@ public static BytesBuffer wrap(byte[] array, int offset, int length) { return new BytesBuffer(ByteBuffer.wrap(array, offset, length)); } + public static int maxBufferCapacity() { + Integer capacity = maxBufferCapacity; + return capacity != null ? capacity : MAX_BUFFER_CAPACITY; + } + + public static synchronized void initMaxBufferCapacity(int capacity) { + initMaxBufferCapacity(capacity, true); + } + + public static synchronized void initMaxBufferCapacity(int capacity, + boolean explicit) { + E.checkArgument(capacity >= DEFAULT_CAPACITY && + capacity <= MAX_BUFFER_CAPACITY_UPPER_BOUND, + "Max buffer capacity must be in range [%s, %s], " + + "but got %s", + DEFAULT_CAPACITY, MAX_BUFFER_CAPACITY_UPPER_BOUND, + capacity); + + if (!explicit) { + return; + } + + if (maxBufferCapacity == null) { + maxBufferCapacity = capacity; + return; + } + + if (maxBufferCapacity == capacity) { + return; + } + + throw new IllegalArgumentException(String.format( + "The process-wide serializer buffer max capacity has been " + + "initialized to %s, but got conflicting value %s", + maxBufferCapacity, capacity)); + } + public ByteBuffer asByteBuffer() { return this.buffer; } @@ -160,18 +212,26 @@ public int remaining() { private void require(int size) { // Does need to resize? - if (this.buffer.limit() - this.buffer.position() >= size) { + if (this.buffer.remaining() >= size) { return; } // Can't resize for wrapped buffer since will change the origin ref - E.checkState(this.resize, "Can't resize for wrapped buffer"); + if (!this.resize) { + E.checkState(false, "Can't resize for wrapped buffer"); + } + + int maxCapacity = maxBufferCapacity(); + long requiredCapacity = (long) this.buffer.position() + size; + if (requiredCapacity > maxCapacity) { + E.checkArgument(false, + "Capacity %s exceeds max buffer capacity: %s", + requiredCapacity, maxCapacity); + } // Extra capacity as buffer - int newcapacity = size + this.buffer.limit() + DEFAULT_CAPACITY; - E.checkArgument(newcapacity <= MAX_BUFFER_CAPACITY, - "Capacity exceeds max buffer capacity: %s", - MAX_BUFFER_CAPACITY); - ByteBuffer newBuffer = ByteBuffer.allocate(newcapacity); + long newCapacity = Math.min(requiredCapacity + DEFAULT_CAPACITY, + maxCapacity); + ByteBuffer newBuffer = ByteBuffer.allocate((int) newCapacity); this.buffer.flip(); newBuffer.put(this.buffer); this.buffer = newBuffer; @@ -248,7 +308,7 @@ public byte peek() { } public byte peekLast() { - return this.buffer.get(this.buffer.capacity() - 1); + return this.buffer.get(this.buffer.limit() - 1); } public byte read() { @@ -261,12 +321,6 @@ public byte[] read(int length) { return bytes; } - public byte[] readToEnd() { - byte[] bytes = new byte[this.remaining()]; - this.buffer.get(bytes); - return bytes; - } - public boolean readBoolean() { return this.buffer.get() != 0; } @@ -296,12 +350,12 @@ public double readDouble() { } public BytesBuffer writeBytes(byte[] bytes) { - // Original limit as above, consider this limit may be due to performance considerations when multiple storage backends are used. - // The above limit will cause errors when writing value to property exceeds the limit. So adjust size to 5M - E.checkArgument(bytes.length <= WRITE_BYTES_MAX_LENGTH, - "The max length of bytes is %s, but got %s", - WRITE_BYTES_MAX_LENGTH, bytes.length); - require(SHORT_LEN + bytes.length); + if (bytes.length > BYTES_LEN_MAX) { + E.checkArgument(false, + "The max length of bytes is %s, but got %s", + BYTES_LEN_MAX, bytes.length); + } + require(BYTES_LEN + bytes.length); this.writeVInt(bytes.length); this.write(bytes); return this; @@ -310,14 +364,15 @@ public BytesBuffer writeBytes(byte[] bytes) { public byte[] readBytes() { int length = this.readVInt(); assert length >= 0; - byte[] bytes = this.read(length); - return bytes; + return this.read(length); } public BytesBuffer writeBigBytes(byte[] bytes) { - E.checkArgument(bytes.length <= BLOB_LEN_MAX, - "The max length of bytes is %s, but got %s", - BLOB_LEN_MAX, bytes.length); + if (bytes.length > BLOB_LEN_MAX) { + E.checkArgument(false, + "The max length of bytes is %s, but got %s", + BLOB_LEN_MAX, bytes.length); + } require(BLOB_LEN + bytes.length); this.writeVInt(bytes.length); this.write(bytes); @@ -327,8 +382,7 @@ public BytesBuffer writeBigBytes(byte[] bytes) { public byte[] readBigBytes() { int length = this.readVInt(); assert length >= 0; - byte[] bytes = this.read(length); - return bytes; + return this.read(length); } public BytesBuffer writeStringRaw(String val) { @@ -356,11 +410,9 @@ public BytesBuffer writeStringWithEnding(String value) { * 0xFF is not a valid byte in UTF8 bytes */ assert !Bytes.contains(bytes, STRING_ENDING_BYTE_FF) : - "Invalid UTF8 bytes: " + value; + "Invalid UTF8 bytes: " + value; if (Bytes.contains(bytes, STRING_ENDING_BYTE)) { - E.checkArgument(false, - "Can't contains byte '0x00' in string: '%s'", - value); + E.checkArgument(false, "Can't contains byte '0x00' in string: '%s'", value); } this.write(bytes); } @@ -378,17 +430,6 @@ public BytesBuffer writeStringWithEnding(String value) { public String readStringWithEnding() { return StringEncoding.decode(this.readBytesWithEnding()); } - public String skipBytesWithEnding(){ - boolean foundEnding = false; - while (this.remaining() > 0) { - byte current = this.read(); - if (current == STRING_ENDING_BYTE) { - foundEnding = true; - break; - } - } - return ""; - } public BytesBuffer writeStringToRemaining(String value) { byte[] bytes = StringEncoding.encode(value); @@ -444,7 +485,7 @@ public BytesBuffer writeVInt(int value) { this.write(0x80 | ((value >>> 14) & 0x7f)); } if (value > 0x7f || value < 0) { - this.write(0x80 | ((value >>> 7) & 0x7f)); + this.write(0x80 | ((value >>> 7) & 0x7f)); } this.write(value & 0x7f); @@ -453,6 +494,11 @@ public BytesBuffer writeVInt(int value) { public int readVInt() { byte leading = this.read(); + if (leading == 0x80) { + E.checkArgument(false, + "Unexpected varint with leading byte '0x%s'", + Bytes.toHex(leading)); + } int value = leading & 0x7f; if (leading >= 0) { assert (leading & 0x80) == 0; @@ -470,6 +516,16 @@ public int readVInt() { } } + if (i >= 5) { + E.checkArgument(false, + "Unexpected varint %s with too many bytes(%s)", + value, i + 1); + } + if (i >= 4 && (leading & 0x70) != 0) { + E.checkArgument(false, + "Unexpected varint %s with leading byte '0x%s'", + value, Bytes.toHex(leading)); + } return value; } @@ -499,7 +555,7 @@ public BytesBuffer writeVLong(long value) { this.write(0x80 | ((int) (value >>> 14) & 0x7f)); } if (value > 0x7fL || value < 0L) { - this.write(0x80 | ((int) (value >>> 7) & 0x7f)); + this.write(0x80 | ((int) (value >>> 7) & 0x7f)); } this.write((int) value & 0x7f); @@ -508,9 +564,11 @@ public BytesBuffer writeVLong(long value) { public long readVLong() { byte leading = this.read(); - E.checkArgument(leading != 0x80, - "Unexpected varlong with leading byte '0x%s'", - Bytes.toHex(leading)); + if (leading == 0x80) { + E.checkArgument(false, + "Unexpected varlong with leading byte '0x%s'", + Bytes.toHex(leading)); + } long value = leading & 0x7fL; if (leading >= 0) { assert (leading & 0x80) == 0; @@ -528,84 +586,99 @@ public long readVLong() { } } - E.checkArgument(i < 10, - "Unexpected varlong %s with too many bytes(%s)", - value, i + 1); - E.checkArgument(i < 9 || (leading & 0x7e) == 0, - "Unexpected varlong %s with leading byte '0x%s'", - value, Bytes.toHex(leading)); + if (i >= 10) { + E.checkArgument(false, + "Unexpected varlong %s with too many bytes(%s)", + value, i + 1); + } + if (i >= 9 && (leading & 0x7e) != 0) { + E.checkArgument(false, + "Unexpected varlong %s with leading byte '0x%s'", + value, Bytes.toHex(leading)); + } return value; } - public T newValue(Cardinality cardinality) { - switch (cardinality) { - case SET: - return (T) new LinkedHashSet<>(); - case LIST: - return (T) new ArrayList<>(); - default: - // pass - break; - } - return null; + /** + * Store properties carry their cardinality and type in a leading byte. + * Schema-driven backend properties use {@link #writeSchemaProperty}. + */ + public BytesBuffer writeProperty(PropertyKey pkey, Object value) { + return this.writeProperty(pkey.cardinality(), pkey.dataType(), value); } - private byte getCardinalityAndType(int cardinality, int type){ - return (byte) ((cardinality << 6) | type); + public BytesBuffer writeProperty(Cardinality cardinality, DataType dataType, + Object value) { + this.write((cardinality.code() << 6) | dataType.code()); + return this.writePropertyValue(cardinality, dataType, value); } - public static byte getCardinality(int value){ - return (byte) ((value & 0xc0) >> 6); + public Object readProperty(PropertyKey pkey) { + int metadata = this.readUInt8(); + Cardinality cardinality = SerialEnum.fromCode(Cardinality.class, + getCardinality(metadata)); + DataType dataType = SerialEnum.fromCode(DataType.class, getType(metadata)); + pkey.cardinality(cardinality); + pkey.dataType(dataType); + return this.readPropertyValue(cardinality, dataType); } - public static byte getType(int value){ - return (byte) (value & 0x3f); + /** + * Encode a backend property whose metadata is supplied by the schema. + * Unlike Store's tagged format, no metadata byte is persisted here. + */ + public BytesBuffer writeSchemaProperty(PropertyKey pkey, Object value) { + return this.writePropertyValue(pkey.cardinality(), pkey.dataType(), value); } - public BytesBuffer writeProperty(PropertyKey pkey, Object value) { - return writeProperty(pkey.cardinality(), pkey.dataType(), value); + public Object readSchemaProperty(PropertyKey pkey) { + return this.readPropertyValue(pkey.cardinality(), pkey.dataType()); } - public BytesBuffer writeProperty(Cardinality cardinality, DataType dataType, Object value) { - this.write(getCardinalityAndType(cardinality.code(),dataType.code())); + private BytesBuffer writePropertyValue(Cardinality cardinality, + DataType dataType, Object value) { if (cardinality == Cardinality.SINGLE) { this.writeProperty(dataType, value); return this; } - assert cardinality == Cardinality.LIST || - cardinality == Cardinality.SET; + assert cardinality == Cardinality.LIST || cardinality == Cardinality.SET; Collection values = (Collection) value; this.writeVInt(values.size()); - for (Object o : values) { - this.writeProperty(dataType, o); + for (Object item : values) { + this.writeProperty(dataType, item); } return this; } - public Object readProperty(PropertyKey propertyKey) { - byte cardinalityAndType = this.read(); - Cardinality cardinality; - DataType type; - cardinality = SerialEnum.fromCode(Cardinality.class, - getCardinality(cardinalityAndType)); - - type = SerialEnum.fromCode(DataType.class, getType(cardinalityAndType)); - propertyKey.cardinality(cardinality); - propertyKey.dataType(type); + private Object readPropertyValue(Cardinality cardinality, DataType dataType) { if (cardinality == Cardinality.SINGLE) { - Object value = this.readProperty(type); - return value; + return this.readProperty(dataType); } - Collection values = this.newValue(cardinality); - assert cardinality == Cardinality.LIST || - cardinality == Cardinality.SET; + assert cardinality == Cardinality.LIST || cardinality == Cardinality.SET; int size = this.readVInt(); + Collection values = this.newValue(cardinality); for (int i = 0; i < size; i++) { - values.add(this.readProperty(type)); + values.add(this.readProperty(dataType)); } return values; } + @SuppressWarnings("unchecked") + public T newValue(Cardinality cardinality) { + switch (cardinality) { + case SET: + return (T) new LinkedHashSet<>(); + case LIST: + return (T) new ArrayList<>(); + default: + return null; + } + } + + public static byte getCardinality(int value) { + return (byte) ((value & 0xc0) >> 6); + } + public void writeProperty(DataType dataType, Object value) { switch (dataType) { case BOOLEAN: @@ -633,8 +706,7 @@ public void writeProperty(DataType dataType, Object value) { this.writeString((String) value); break; case BLOB: - byte[] bytes = value instanceof byte[] ? - (byte[]) value : ((Blob) value).bytes(); + byte[] bytes = value instanceof byte[] ? (byte[]) value : ((Blob) value).bytes(); this.writeBigBytes(bytes); break; case UUID: @@ -644,10 +716,15 @@ public void writeProperty(DataType dataType, Object value) { this.writeLong(uuid.getLeastSignificantBits()); break; default: - throw new IllegalArgumentException("Unsupported data type " + dataType); + this.writeBytes(KryoUtil.toKryoWithType(value)); + break; } } + public static byte getType(int value) { + return (byte) (value & 0x3f); + } + public Object readProperty(DataType dataType) { switch (dataType) { case BOOLEAN: @@ -671,15 +748,69 @@ public Object readProperty(DataType dataType) { case UUID: return new UUID(this.readLong(), this.readLong()); default: - throw new IllegalArgumentException("Unsupported data type " + dataType); + return KryoUtil.fromKryoWithType(this.readBytes()); } } - public BytesBuffer writeId(Id id) { - return this.writeId(id, false); + /** Skip a tagged property without allocating its value or collection. */ + public void skipProperty() { + int metadata = this.readUInt8(); + Cardinality cardinality = SerialEnum.fromCode(Cardinality.class, + getCardinality(metadata)); + DataType dataType = SerialEnum.fromCode(DataType.class, getType(metadata)); + this.skipPropertyValue(cardinality, dataType); } - public BytesBuffer writeId(Id id, boolean big) { + public void skipSchemaProperty(PropertyKey pkey) { + this.skipPropertyValue(pkey.cardinality(), pkey.dataType()); + } + + private void skipPropertyValue(Cardinality cardinality, DataType dataType) { + if (cardinality == Cardinality.SINGLE) { + this.skipProperty(dataType); + return; + } + assert cardinality == Cardinality.LIST || cardinality == Cardinality.SET; + int size = this.readVInt(); + E.checkArgument(size >= 0, "Invalid property collection size: %s", size); + for (int i = 0; i < size; i++) { + this.skipProperty(dataType); + } + } + + private void skipProperty(DataType dataType) { + switch (dataType) { + case BOOLEAN: + case BYTE: + case INT: + this.readVInt(); + return; + case LONG: + case DATE: + this.readVLong(); + return; + case FLOAT: + this.skip(FLOAT_LEN); + return; + case DOUBLE: + this.skip(DOUBLE_LEN); + return; + case UUID: + this.skip(Id.UUID_LENGTH); + return; + default: + // Text, blob and Kryo fallback payloads all carry a byte length. + this.skip(this.readVInt()); + } + } + + private void skip(int length) { + E.checkArgument(length >= 0 && length <= this.remaining(), + "Invalid skipped byte length %s, remaining %s", length, this.remaining()); + this.buffer.position(this.buffer.position() + length); + } + + public BytesBuffer writeId(Id id) { switch (id.type()) { case LONG: // Number Id @@ -687,35 +818,39 @@ public BytesBuffer writeId(Id id, boolean big) { this.writeNumber(value); break; case UUID: - // UUID Id + // UUID ID byte[] bytes = id.asBytes(); assert bytes.length == Id.UUID_LENGTH; this.writeUInt8(0x7f); // 0b01111111 means UUID this.write(bytes); break; case EDGE: - // Edge Id + // Edge ID this.writeUInt8(0x7e); // 0b01111110 means EdgeId this.writeEdgeId(id); break; default: - // String Id + // String Id (VertexID) bytes = id.asBytes(); int len = bytes.length; - E.checkArgument(len > 0, "Can't write empty id"); - E.checkArgument(len <= 16384, - "Big id max length is %s, but got %s {%s}", - 16384, len, id); - len -= 1; - if (len <= 63) { + if (len <= 0) { + E.checkArgument(false, "Can't write empty id"); + } + if (len > ID_LEN_MAX) { + E.checkArgument(false, "Big id max length is %s, but got %s {%s}", + ID_LEN_MAX, len, id); + } + len -= 1; // mapping [1, 16384] to [0, 16383] + if (len <= 0x3f) { + // If length is <= 63, use a single byte with the highest bit set to 1 this.writeUInt8(len | 0x80); } else { int high = len >> 8; int low = len & 0xff; + // Write high 8 bits with highest two bits set to 11 this.writeUInt8(high | 0xc0); this.writeUInt8(low); } - this.write(bytes); break; } @@ -723,10 +858,6 @@ public BytesBuffer writeId(Id id, boolean big) { } public Id readId() { - return this.readId(false); - } - - public Id readId(boolean big) { byte b = this.read(); boolean number = (b & 0x80) == 0; if (number) { @@ -742,19 +873,32 @@ public Id readId(boolean big) { } } else { // String Id - int len = b & 0x3f; - if ((b & 0x40) != 0) { + int len = b & 0x3f; // Take the lowest 6 bits as part of the length + if ((b & 0x40) != 0) { // If the 7th bit is set, length information spans 2 bytes int high = len << 8; int low = this.readUInt8(); len = high + low; } - len += 1; + len += 1; // restore [0, 16383] to [1, 16384] byte[] id = this.read(len); return IdGenerator.of(id, IdType.STRING); } } + /** + * Preserve the existing Store API. Both modes use the same persisted ID + * header; the flag has never selected a different on-disk format. + */ + public BytesBuffer writeId(Id id, boolean big) { + return this.writeId(id); + } + + public Id readId(boolean big) { + return this.readId(); + } + public BytesBuffer writeEdgeId(Id id) { + // owner-vertex + dir + edge-label + sub-edge-label + sort-values + other-vertex EdgeId edge = (EdgeId) id; this.writeId(edge.ownerVertexId()); this.write(edge.directionCode()); @@ -766,18 +910,79 @@ public BytesBuffer writeEdgeId(Id id) { } public Id readEdgeId() { - return new EdgeId(this.readId(), EdgeId.directionFromCode(this.read()), - this.readId(), this.readId(), - this.readStringWithEnding(), this.readId()); + return this.readEdgeId(true, null); + } + + public EdgeId readEdgeId(boolean withOwnerPrefix, Id suppliedOwner) { + return this.readEdgeId(withOwnerPrefix, suppliedOwner, false); + } + + private EdgeId readEdgeId(boolean withOwnerPrefix, Id suppliedOwner, boolean skipSortValues) { + Id owner = withOwnerPrefix ? this.readId() : suppliedOwner; + E.checkArgumentNotNull(owner, "Edge decoding requires an owner ID"); + byte direction = this.read(); + E.checkState(direction == HugeType.EDGE_IN.code() || direction == HugeType.EDGE_OUT.code(), + "Invalid edge column type: %s", direction & 0xff); + Id label = this.readId(); + Id subLabel = this.readId(); + String sortValues = skipSortValues ? this.skipBytesWithEnding() : this.readStringWithEnding(); + return new EdgeId(owner, EdgeId.directionFromCode(direction), label, subLabel, + sortValues, this.readId()); + } + + public enum IndexExpiryLayout { + NONE, + REQUIRED, + OPTIONAL + } + + /** Decode the shared index/element/expiry field order; callers choose the layout. */ + public IndexColumnName readIndexColumnName(HugeType type, boolean withIndexPrefix, + IndexExpiryLayout expiryLayout) { + E.checkArgumentNotNull(expiryLayout, "Index expiry layout can't be null"); + BinaryId indexId = withIndexPrefix ? this.readIndexId(type) : null; + Id elementId = this.readId(); + boolean hasExpiredTime = expiryLayout == IndexExpiryLayout.REQUIRED || + expiryLayout == IndexExpiryLayout.OPTIONAL && this.remaining() > 0; + long expiredTime = hasExpiredTime ? this.readVLong() : 0L; + if (expiryLayout == IndexExpiryLayout.OPTIONAL) { + E.checkState(this.remaining() == 0, + "Unexpected trailing bytes in schema index column: %s", this.remaining()); + } + return new IndexColumnName(indexId, elementId, expiredTime, hasExpiredTime); } - public Id readEdgeIdSkipSortValues() { - return new EdgeId(this.readId(), EdgeId.directionFromCode(this.read()), - this.readId(), this.readId(), - this.skipBytesWithEnding(), - this.readId()); - } + public static final class IndexColumnName { + + private final BinaryId indexId; + private final Id elementId; + private final long expiredTime; + private final boolean hasExpiredTime; + + private IndexColumnName(BinaryId indexId, Id elementId, long expiredTime, + boolean hasExpiredTime) { + this.indexId = indexId; + this.elementId = elementId; + this.expiredTime = expiredTime; + this.hasExpiredTime = hasExpiredTime; + } + public BinaryId indexId() { + return this.indexId; + } + + public Id elementId() { + return this.elementId; + } + + public long expiredTime() { + return this.expiredTime; + } + + public boolean hasExpiredTime() { + return this.hasExpiredTime; + } + } public BytesBuffer writeIndexId(Id id, HugeType type) { return this.writeIndexId(id, type, true); @@ -786,17 +991,18 @@ public BytesBuffer writeIndexId(Id id, HugeType type) { public BytesBuffer writeIndexId(Id id, HugeType type, boolean withEnding) { byte[] bytes = id.asBytes(); int len = bytes.length; - E.checkArgument(len > 0, "Can't write empty id"); + if (len == 0) { + E.checkArgument(false, "Can't write empty id"); + } this.write(bytes); if (type.isStringIndex()) { if (Bytes.contains(bytes, STRING_ENDING_BYTE)) { - // Not allow STRING_ENDING_BYTE exist in string index id + // Not allow STRING_ENDING_BYTE to exist in string index id E.checkArgument(false, "The %s type index id can't contains " + - "byte '0x%s', but got: 0x%s", type, - Bytes.toHex(STRING_ENDING_BYTE), - Bytes.toHex(bytes)); + "byte '0x%s', but got: 0x%s", + type, Bytes.toHex(STRING_ENDING_BYTE), Bytes.toHex(bytes)); } if (withEnding) { this.writeStringWithEnding(""); @@ -808,10 +1014,10 @@ public BytesBuffer writeIndexId(Id id, HugeType type, boolean withEnding) { public BinaryId readIndexId(HugeType type) { byte[] id; if (type.isRange4Index()) { - // HugeCodeType 1 bytes + IndexLabel 4 bytes + fieldValue 4 bytes + // HugeTypeCode 1 byte + IndexLabel 4 bytes + fieldValue 4 bytes id = this.read(9); } else if (type.isRange8Index()) { - // HugeCodeType 1 bytes + IndexLabel 4 bytes + fieldValue 8 bytes + // HugeTypeCode 1 byte + IndexLabel 4 bytes + fieldValue 8 bytes id = this.read(13); } else { assert type.isStringIndex(); @@ -825,10 +1031,17 @@ public BinaryId asId() { } public BinaryId parseId(HugeType type) { + return this.parseId(type, false); + } + + public BinaryId parseId(HugeType type, boolean enablePartition) { if (type.isIndex()) { return this.readIndexId(type); } // Parse id from bytes + if ((type.isVertex() || type.isEdge()) && enablePartition) { + this.readShort(); + } int start = this.buffer.position(); /* * Since edge id in edges table doesn't prefix with leading 0x7e, @@ -844,7 +1057,7 @@ public BinaryId parseId(HugeType type) { } /** - * Parse OLAP id + * Analyze olap id * @param type * @param isOlap * @return @@ -855,12 +1068,9 @@ public BinaryId parseOlapId(HugeType type, boolean isOlap) { } // Parse id from bytes int start = this.buffer.position(); - /** - * OLAP - * {PropertyKey}{VertexId} - */ + // OLAP {PropertyKey}{VertexId} if (isOlap) { - // First read OLAP property id + // Read olap property id first Id pkId = this.readId(); } Id id = this.readId(); @@ -939,6 +1149,11 @@ private void writeNumber(long val) { } private long readNumber(byte b) { + if ((b & 0x80) != 0) { + E.checkArgument(false, + "Not a number type with prefix byte '0x%s'", + Bytes.toHex(b)); + } // Parse the kind from byte 0kkksxxx int kind = b >>> 4; boolean positive = (b & 0x08) > 0; @@ -986,27 +1201,48 @@ private long readNumber(byte b) { private byte[] readBytesWithEnding() { int start = this.buffer.position(); boolean foundEnding = false; - while (this.remaining() > 0) { + int remaining = this.remaining(); + for (int i = 0; i < remaining; i++) { byte current = this.read(); if (current == STRING_ENDING_BYTE) { foundEnding = true; break; } } - E.checkArgument(foundEnding, "Not found ending '0x%s'", - Bytes.toHex(STRING_ENDING_BYTE)); + if (!foundEnding) { + E.checkArgument(false, + "Not found ending '0x%s'", + Bytes.toHex(STRING_ENDING_BYTE)); + } int end = this.buffer.position() - 1; int len = end - start; + if (len <= 0) { + return BYTES_EMPTY; + } byte[] bytes = new byte[len]; System.arraycopy(this.array(), start, bytes, 0, len); return bytes; } - public byte[] remainingBytes(){ - int length = this.remaining(); + public byte[] readToEnd() { + return this.read(this.remaining()); + } + + public String skipBytesWithEnding() { + while (this.remaining() > 0) { + if (this.read() == STRING_ENDING_BYTE) { + break; + } + } + return ""; + } + + public Id readEdgeIdSkipSortValues() { + return this.readEdgeId(true, null, true); + } + + public byte[] remainingBytes() { int start = this.position(); - byte[] bytes = new byte[length]; - System.arraycopy(this.array(), start, bytes, 0, length); - return bytes; + return Arrays.copyOfRange(this.array(), start, start + this.remaining()); } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/DirectBinarySerializer.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/DirectBinarySerializer.java index bad18ebf8a..8aa325eabe 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/DirectBinarySerializer.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/DirectBinarySerializer.java @@ -22,9 +22,13 @@ import java.util.Arrays; import java.util.Base64; +import org.apache.hugegraph.HugeGraphSupplier; +import org.apache.hugegraph.backend.BackendColumn; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.id.IdGenerator; -import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.Log; import org.slf4j.Logger; @@ -111,18 +115,56 @@ public DirectHugeElement parseEdge(byte[] key, byte[] value) { return new DirectHugeElement(IdGenerator.of(key), expiredTime); } + public DirectHugeElement parseSchemaIndex(HugeGraphSupplier graph, byte[] key, byte[] value) { + Index index = BinaryElementSerializer.getInstance().parseSchemaIndex( + graph, BackendColumn.of(key, value)); + return new DirectHugeElement(IdGenerator.of(key), index.expiredTime()); + } + + /** Read TTL from an untagged persisted Server vertex row. */ + public DirectHugeElement parseSchemaVertex(HugeGraphSupplier graph, byte[] key, byte[] value) { + E.checkArgumentNotNull(graph, "Schema vertex decoding requires a schema supplier"); + BytesBuffer buffer = BytesBuffer.wrap(value); + Id labelId = buffer.readId(); + this.skipProperties(buffer, graph); + long expiredTime = graph.vertexLabelOrNone(labelId).ttl() > 0 ? buffer.readVLong() : 0L; + return new DirectHugeElement(IdGenerator.of(key), expiredTime); + } + + /** Read TTL from an untagged persisted Server edge row. */ + public DirectHugeElement parseSchemaEdge(HugeGraphSupplier graph, byte[] key, byte[] value) { + E.checkArgumentNotNull(graph, "Schema edge decoding requires a schema supplier"); + BytesBuffer name = BytesBuffer.wrap(key); + name.readId(); + byte type = name.read(); + E.checkState(type == HugeType.EDGE_IN.code() || type == HugeType.EDGE_OUT.code(), + "Invalid edge column type: %s", type & 0xff); + name.readId(); + Id subLabelId = name.readId(); + BytesBuffer buffer = BytesBuffer.wrap(value); + this.skipProperties(buffer, graph); + long expiredTime = graph.edgeLabelOrNone(subLabelId).ttl() > 0 ? buffer.readVLong() : 0L; + return new DirectHugeElement(IdGenerator.of(key), expiredTime); + } + private void skipProperties(BytesBuffer buffer) { + this.skipProperties(buffer, null); + } + + private void skipProperties(BytesBuffer buffer, HugeGraphSupplier graph) { int size = buffer.readVInt(); assert size >= 0; for (int i = 0; i < size; i++) { Id pkeyId = IdGenerator.of(buffer.readVInt()); - this.skipProperty(pkeyId, buffer); + if (graph == null) { + this.skipProperty(pkeyId, buffer); + } else { + buffer.skipSchemaProperty(graph.propertyKey(pkeyId)); + } } } protected void skipProperty(Id pkeyId, BytesBuffer buffer) { - // Parse value - PropertyKey pkey = new PropertyKey(null, pkeyId, ""); - buffer.readProperty(pkey); + buffer.skipProperty(); } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/OlapKey.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/OlapKey.java new file mode 100644 index 0000000000..814aea66e9 --- /dev/null +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/serializer/OlapKey.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.hugegraph.serializer; + +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.struct.schema.SchemaElement; + +/** Physical keys for the merged HStore OLAP table, independent of the value codec. */ +public final class OlapKey { + + private OlapKey() { + } + + public static byte[] format(Id propertyId, Id vertexId) { + return BytesBuffer.allocate(propertyId.length() + vertexId.length() + 18) + .writeId(propertyId).writeId(vertexId).bytes(); + } + + public static boolean matchesProperty(byte[] value, Id propertyId) { + if (value == null || value.length == 0) { + return false; + } + try { + return BytesBuffer.wrap(value).readVInt() == SchemaElement.schemaId(propertyId); + } catch (IllegalArgumentException | java.nio.BufferUnderflowException e) { + return false; + } + } +} diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/EdgeLabel.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/EdgeLabel.java index 7148495a78..128f8f4f63 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/EdgeLabel.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/EdgeLabel.java @@ -332,6 +332,7 @@ public static EdgeLabel fromMap(Map map, HugeGraphSupplier graph Id id = IdGenerator.of((int) map.get(EdgeLabel.P.ID)); String name = (String) map.get(EdgeLabel.P.NAME); EdgeLabel edgeLabel = new EdgeLabel(graph, id, name); + restoreLinks(map, edgeLabel); for (Map.Entry entry : map.entrySet()) { switch (entry.getKey()) { case P.ID: @@ -373,25 +374,9 @@ public static EdgeLabel fromMap(Map map, HugeGraphSupplier graph edgeLabel.ttlStartTime(IdGenerator.of(ttlStartTime)); break; case P.LINKS: - // TODO: serialize and deserialize - List list = (List) entry.getValue(); - for (Map m : list) { - for (Object key : m.keySet()) { - Id sid = IdGenerator.of(Long.parseLong((String) key)); - Id tid = IdGenerator.of(Long.parseLong(String.valueOf(m.get(key)))); - edgeLabel.links(Pair.of(sid, tid)); - } - } - break; case P.SOURCE_LABEL: - long sourceLabel = - Long.parseLong((String) entry.getValue()); - edgeLabel.sourceLabel(IdGenerator.of(sourceLabel)); - break; case P.TARGET_LABEL: - long targetLabel = - Long.parseLong((String) entry.getValue()); - edgeLabel.targetLabel(IdGenerator.of(targetLabel)); + // These fields describe the same links and were restored together. break; case P.FATHER_ID: long fatherId = @@ -410,9 +395,9 @@ public static EdgeLabel fromMap(Map map, HugeGraphSupplier graph edgeLabel.frequency(frequency); break; case P.SORT_KEYS: - ids = ((List) entry.getValue()).stream().map( - IdGenerator::of).collect(Collectors.toSet()); - edgeLabel.sortKeys(ids.toArray(new Id[0])); + List orderedIds = ((List) entry.getValue()).stream().map( + IdGenerator::of).collect(Collectors.toList()); + edgeLabel.sortKeys(orderedIds.toArray(new Id[0])); break; default: throw new AssertionError(String.format( @@ -423,6 +408,48 @@ public static EdgeLabel fromMap(Map map, HugeGraphSupplier graph return edgeLabel; } + private static void restoreLinks(Map map, EdgeLabel edgeLabel) { + Set> links = new HashSet<>(); + if (map.containsKey(P.LINKS)) { + Object value = map.get(P.LINKS); + E.checkArgument(value instanceof List, "Invalid edge label links: %s", value); + for (Object item : (List) value) { + E.checkArgument(item instanceof Map && !((Map) item).isEmpty(), + "Invalid edge label link: %s", item); + for (Map.Entry link : ((Map) item).entrySet()) { + links.add(Pair.of(linkId(link.getKey(), P.SOURCE_LABEL), + linkId(link.getValue(), P.TARGET_LABEL))); + } + } + } + + boolean hasSource = map.containsKey(P.SOURCE_LABEL); + boolean hasTarget = map.containsKey(P.TARGET_LABEL); + E.checkArgument(hasSource == hasTarget, + "Edge label sourceLabel and targetLabel must be provided together"); + if (hasSource) { + Pair endpoints = Pair.of(linkId(map.get(P.SOURCE_LABEL), P.SOURCE_LABEL), + linkId(map.get(P.TARGET_LABEL), P.TARGET_LABEL)); + E.checkArgument(links.isEmpty() || links.contains(endpoints), + "Edge label endpoints %s conflict with links %s", endpoints, links); + links.add(endpoints); + } + for (Pair link : links) { + edgeLabel.links(link); + } + } + + private static Id linkId(Object value, String field) { + E.checkArgument(value instanceof String || value instanceof Number, + "Invalid edge label %s: %s", field, value); + try { + return IdGenerator.of(Long.parseLong(value.toString())); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(String.format( + "Invalid edge label %s: %s", field, value), e); + } + } + public static final class P { public static final String ID = "id"; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/IndexLabel.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/IndexLabel.java index 2ef5eb9962..282d9ea79f 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/IndexLabel.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/IndexLabel.java @@ -126,7 +126,7 @@ public SchemaLabel baseLabel() { } public SchemaLabel baseElement() { - return getElement(this.graph, this.baseType, this.baseValue); + return getBaseLabel(this.graph, this.baseType, this.baseValue); } public boolean hasSameContent(IndexLabel other) { @@ -258,44 +258,6 @@ public static SchemaLabel getBaseLabel(HugeGraphSupplier graph, return label; } - public static SchemaLabel getElement(HugeGraphSupplier graph, - HugeType baseType, Object baseValue) { - E.checkNotNull(baseType, "base type", "index label"); - E.checkNotNull(baseValue, "base value", "index label"); - E.checkArgument(baseValue instanceof String || baseValue instanceof Id, - "The base value must be instance of String or Id, " + - "but got %s(%s)", baseValue, - baseValue.getClass().getSimpleName()); - - SchemaLabel label; - switch (baseType) { - case VERTEX_LABEL: - if (baseValue instanceof String) { - label = graph.vertexLabel((String) baseValue); - } else { - assert baseValue instanceof Id; - label = graph.vertexLabel((Id) baseValue); - } - break; - case EDGE_LABEL: - if (baseValue instanceof String) { - label = graph.edgeLabel((String) baseValue); - } else { - assert baseValue instanceof Id; - label = graph.edgeLabel((Id) baseValue); - } - break; - default: - throw new AssertionError(String.format( - "Unsupported base type '%s' of index label", - baseType)); - } - - E.checkArgumentNotNull(label, "Can't find the %s with name '%s'", - baseType.readableName(), baseValue); - return label; - } - public String convert2Groovy(boolean attachIdFlag) { StringBuilder builder = new StringBuilder(SCHEMA_PREFIX); diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/SchemaElement.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/SchemaElement.java index a62532ff79..d2563e34e9 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/SchemaElement.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/SchemaElement.java @@ -76,6 +76,8 @@ public SchemaElement(final HugeGraphSupplier graph, Id id, String name) { } public HugeGraphSupplier graph() { + E.checkState(this.graph != null, + "Graph is null of schema '%s'", this.name); return this.graph; } @@ -103,10 +105,12 @@ public void userdata(String key, Object value) { } public void userdata(Userdata userdata) { + E.checkArgumentNotNull(userdata, "userdata"); this.userdata.putAll(userdata); } public void userdata(Map userdata) { + E.checkArgumentNotNull(userdata, "userdata"); this.userdata.putAll(userdata); } @@ -116,6 +120,7 @@ public void removeUserdata(String key) { } public void removeUserdata(Userdata userdata) { + E.checkArgumentNotNull(userdata, "userdata"); for (String key : userdata.keySet()) { this.userdata.remove(key); } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/Userdata.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/Userdata.java index 1cd0db0e0f..8aef1e5610 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/Userdata.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/Userdata.java @@ -22,6 +22,7 @@ import org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.type.define.Action; +import org.apache.hugegraph.util.DateUtil; public class Userdata extends HashMap { @@ -37,6 +38,51 @@ public Userdata(Map map) { this.putAll(map); } + /** + * Normalizes the value before storing so the {@link #CREATE_TIME}-is-Date + * invariant holds regardless of how entries are added. + */ + @Override + public Object put(String key, Object value) { + return super.put(key, normalizeValue(key, value)); + } + + @Override + public void putAll(Map map) { + for (Map.Entry e : map.entrySet()) { + this.put(e.getKey(), e.getValue()); + } + } + + /** + * Normalize internal userdata values whose runtime type can diverge from + * their serialized form. The only such key today is {@link #CREATE_TIME}: + * it is written as a {@link java.util.Date} but persisted as a formatted + * JSON string by the backend serializers, and Jackson cannot re-type a + * value to {@code Date} when the target is a raw {@code Map}. This method + * restores the original type after deserialization. Idempotent for values + * already of the expected type. + *

+ * An empty string is passed through unchanged: it is the key-only + * placeholder used by the {@code eliminate()}/{@code DELETE} builder flow + * (e.g. {@code .userdata(CREATE_TIME, "").eliminate()}), where the value is + * ignored and only the key drives {@code removeUserdata}. Parsing it would + * fail before the eliminate path can apply its key-only semantics. + */ + public static Object normalizeValue(String key, Object value) { + if (CREATE_TIME.equals(key) && value instanceof String && + !((String) value).isEmpty()) { + try { + return DateUtil.parse((String) value); + } catch (RuntimeException e) { + throw new IllegalArgumentException(String.format( + "Invalid userdata '%s' value: '%s'", + CREATE_TIME, value), e); + } + } + return value; + } + public static void check(Userdata userdata, Action action) { if (userdata == null) { return; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/VertexLabel.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/VertexLabel.java index 2a0002e69d..6b0b3cfe77 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/VertexLabel.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/struct/schema/VertexLabel.java @@ -380,9 +380,9 @@ public static VertexLabel fromMap(Map map, HugeGraphSupplier gra vertexLabel.idStrategy(idStrategy); break; case P.PRIMARY_KEYS: - ids = ((List) entry.getValue()).stream().map( - IdGenerator::of).collect(Collectors.toSet()); - vertexLabel.primaryKeys(ids.toArray(new Id[0])); + List orderedIds = ((List) entry.getValue()).stream().map( + IdGenerator::of).collect(Collectors.toList()); + vertexLabel.primaryKeys(orderedIds.toArray(new Id[0])); break; default: throw new AssertionError(String.format( diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseEdge.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseEdge.java index bafc2b727a..8032be1aff 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseEdge.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseEdge.java @@ -26,7 +26,7 @@ import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.id.EdgeId; import org.apache.hugegraph.id.Id; -import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.struct.schema.SchemaLabel; @@ -73,7 +73,8 @@ public EdgeId idWithDirection() { @Override public String name() { if (this.name == null) { - this.name = SplicingIdGenerator.concatValues(sortValues()); + List values = this.sortValues(); + this.name = values.isEmpty() ? "" : ConditionQuery.concatValues(values); } return this.name; } @@ -98,7 +99,11 @@ public List sortValues() { BaseProperty property = this.getProperty(sk); E.checkState(property != null, "The value of sort key '%s' can't be null", sk); - propValues.add(property.propertyKey().serialValue(property.value(), true)); + Object value = property.serialValue(true); + if ("".equals(value)) { + value = ConditionQuery.INDEX_VALUE_EMPTY; + } + propValues.add(value); } return propValues; } @@ -182,9 +187,9 @@ public void assignId() { if (this.fresh()) { int len = this.id().length(); - E.checkArgument(len <= BytesBuffer.BIG_ID_LEN_MAX, + E.checkArgument(len <= BytesBuffer.EID_LEN_MAX, "The max length of edge id is %s, but got %s {%s}", - BytesBuffer.BIG_ID_LEN_MAX, len, this.id()); + BytesBuffer.EID_LEN_MAX, len, this.id()); } } @Override @@ -238,29 +243,78 @@ public BaseEdge switchOwner() { return edge; } + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof BaseEdge) || this.id() == null) { + return false; + } + return this.id().equals(((BaseEdge) object).id()); + } + + @Override + public int hashCode() { + return this.id() == null ? System.identityHashCode(this) : this.id().hashCode(); + } + public static BaseEdge constructEdge(HugeGraphSupplier graph, BaseVertex ownerVertex, boolean isOutEdge, EdgeLabel edgeLabel, String sortValues, Id otherVertexId) { + BaseEdge edge = createEdge(graph, ownerVertex, isOutEdge, edgeLabel, + sortValues, otherVertexId); + ownerVertex.addEdge(edge); + edge.otherVertex().addEdge(edge.switchOwner()); + return edge; + } + + /** Create endpoint/schema state; adapters own adjacency attachment. */ + public static BaseEdge createEdge(HugeGraphSupplier graph, + BaseVertex ownerVertex, + boolean isOutEdge, + EdgeLabel edgeLabel, + String sortValues, + Id otherVertexId) { Id ownerLabelId = edgeLabel.sourceLabel(); Id otherLabelId = edgeLabel.targetLabel(); VertexLabel srcLabel; VertexLabel tgtLabel; if (graph == null) { - srcLabel = new VertexLabel(null, ownerLabelId, "UNDEF"); - tgtLabel = new VertexLabel(null, otherLabelId, "UNDEF"); + return createEdgeWithoutSchema(null, ownerVertex, isOutEdge, edgeLabel, + sortValues, otherVertexId); + } + if (edgeLabel.general() && + ownerVertex.typeContext() == BaseVertex.TypeContext.STORAGE) { + srcLabel = VertexLabel.GENERAL; + tgtLabel = VertexLabel.GENERAL; } else { - if (edgeLabel.general()) { - srcLabel = VertexLabel.GENERAL; - tgtLabel = VertexLabel.GENERAL; - } else { - srcLabel = graph.vertexLabelOrNone(ownerLabelId); - tgtLabel = graph.vertexLabelOrNone(otherLabelId); - } + srcLabel = graph.vertexLabelOrNone(ownerLabelId); + tgtLabel = graph.vertexLabelOrNone(otherLabelId); } + return createEdge(ownerVertex, isOutEdge, edgeLabel, sortValues, + otherVertexId, srcLabel, tgtLabel); + } + /** Resolve no metadata, while retaining an optional supplier for later engine access. */ + public static BaseEdge createEdgeWithoutSchema(HugeGraphSupplier supplier, + BaseVertex ownerVertex, + boolean isOutEdge, + EdgeLabel edgeLabel, + String sortValues, + Id otherVertexId) { + VertexLabel source = VertexLabel.undefined(supplier, edgeLabel.sourceLabel()); + VertexLabel target = VertexLabel.undefined(supplier, edgeLabel.targetLabel()); + return createEdge(ownerVertex, isOutEdge, edgeLabel, sortValues, + otherVertexId, source, target); + } + + private static BaseEdge createEdge(BaseVertex ownerVertex, boolean isOutEdge, + EdgeLabel edgeLabel, String sortValues, Id otherVertexId, + VertexLabel srcLabel, VertexLabel tgtLabel) { VertexLabel otherVertexLabel; if (isOutEdge) { ownerVertex.correctVertexLabel(srcLabel); @@ -269,6 +323,14 @@ public static BaseEdge constructEdge(HugeGraphSupplier graph, ownerVertex.correctVertexLabel(tgtLabel); otherVertexLabel = srcLabel; } + return createEdge(ownerVertex, isOutEdge, edgeLabel, sortValues, + otherVertexId, otherVertexLabel); + } + + /** Create an edge after the caller has resolved its endpoint labels. */ + public static BaseEdge createEdge(BaseVertex ownerVertex, boolean isOutEdge, + EdgeLabel edgeLabel, String sortValues, + Id otherVertexId, VertexLabel otherVertexLabel) { BaseVertex otherVertex = new BaseVertex(otherVertexId, otherVertexLabel); ownerVertex.propLoaded(false); @@ -279,9 +341,6 @@ public static BaseEdge constructEdge(HugeGraphSupplier graph, edge.vertices(isOutEdge, ownerVertex, otherVertex); edge.assignId(); - ownerVertex.addEdge(edge); - otherVertex.addEdge(edge.switchOwner()); - return edge; } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseElement.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseElement.java index c7353d6b77..b250922d0d 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseElement.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseElement.java @@ -28,6 +28,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import org.apache.hugegraph.id.Id; @@ -41,6 +44,7 @@ import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.util.CollectionUtil; import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.InsertionOrderUtil; import org.apache.hugegraph.util.Log; import org.apache.hugegraph.util.collection.CollectionFactory; import org.eclipse.collections.api.map.primitive.MutableIntObjectMap; @@ -49,16 +53,16 @@ import org.slf4j.Logger; -public abstract class BaseElement implements GraphType, Idfiable, Serializable { +public abstract class BaseElement implements GraphType, Idfiable, Serializable, Cloneable { private static final Logger LOG = Log.logger(BaseElement.class); - public static final MutableIntObjectMap> EMPTY_MAP = - new IntObjectHashMap<>(); + private static final MutableIntObjectMap> EMPTY_MAP = + new IntObjectHashMap>().asUnmodifiable(); - private static final int MAX_PROPERTIES = BytesBuffer.UINT16_MAX; + private static final int MAX_PROPERTIES = BytesBuffer.MAX_PROPERTIES; - MutableIntObjectMap> properties; + private MutableIntObjectMap> properties; Id id; private SchemaLabel schemaLabel; @@ -228,7 +232,15 @@ public V getPropertyValue(Id key) { } return (V) prop.value(); } + /** + * Mutable storage for codecs and owning adapters. The empty map is materialized + * per element before exposure; callers never receive the shared sentinel. + * Direct changes update this container only, without engine transaction callbacks. + */ public MutableIntObjectMap> properties() { + if (this.properties == EMPTY_MAP) { + this.properties = CollectionFactory.newIntObjectMap(); + } return this.properties; } @@ -240,13 +252,14 @@ public BaseProperty getProperty(Id key) { } private BaseProperty addProperty(PropertyKey pkey, V value, - Supplier> supplier) { + Supplier> supplier, + BiFunction> factory) { assert pkey.cardinality().multiple(); BaseProperty> property; if (this.hasProperty(pkey.id())) { property = this.getProperty(pkey.id()); } else { - property = this.newProperty(pkey, supplier.get()); + property = (BaseProperty>) factory.apply(pkey, supplier.get()); this.addProperty(property); } @@ -265,36 +278,47 @@ private BaseProperty addProperty(PropertyKey pkey, V value, values = CollectionUtil.toList(value); } } - property.value().addAll(values); + property.value().addAll(pkey.validValueOrThrow(values)); // Any better ways? return (BaseProperty) property; } public BaseProperty addProperty(PropertyKey pkey, V value) { - BaseProperty prop = null; + return this.addProperty(pkey, value, (key, val) -> this.newProperty(key, val), + prop -> {}, prop -> {}); + } + + /** Single updates notify before replacement; collection updates notify afterwards. */ + @SuppressWarnings("unchecked") + public BaseProperty addProperty( + PropertyKey pkey, V value, + BiFunction> factory, + Consumer> beforeSingle, + Consumer> afterMultiple) { + BaseProperty prop; switch (pkey.cardinality()) { case SINGLE: - prop = this.newProperty(pkey, value); + prop = (BaseProperty) factory.apply(pkey, value); + beforeSingle.accept(prop); this.addProperty(prop); break; case SET: - prop = this.addProperty(pkey, value, HashSet::new); + prop = this.addProperty(pkey, value, HashSet::new, factory); + afterMultiple.accept(prop); break; case LIST: - prop = this.addProperty(pkey, value, ArrayList::new); + prop = this.addProperty(pkey, value, ArrayList::new, factory); + afterMultiple.accept(prop); break; default: - assert false; - break; + throw new AssertionError("Unknown property cardinality: " + pkey.cardinality()); } return prop; } public BaseProperty addProperty(BaseProperty prop) { - if (this.properties == EMPTY_MAP) { - this.properties = new IntObjectHashMap<>(); // change to CollectionFactory.newIntObjectMap(); - } + this.properties(); PropertyKey pkey = prop.propertyKey(); E.checkArgument(this.properties.containsKey(intFromId(pkey.id())) || @@ -311,7 +335,11 @@ public Map> getProperties() { } public BaseProperty removeProperty(Id key) { - return this.properties.remove(intFromId(key)); + int propertyId = intFromId(key); + if (this.properties == EMPTY_MAP) { + return null; + } + return this.properties.remove(propertyId); } /* a util may be should be moved to other place */ @@ -324,7 +352,7 @@ public static int intFromId(Id id) { public abstract Object sysprop(HugeKeys key); public Map getPropertiesMap() { - Map props = new HashMap<>(); + Map props = InsertionOrderUtil.newMap(); for (IntObjectPair> e : this.properties.keyValuesView()) { props.put(IdGenerator.of(e.getOne()), e.getTwo().value()); } @@ -347,6 +375,28 @@ public int sizeOfSubProperties() { return size; } + public void copyProperties(BaseElement source) { + this.properties = source.properties == EMPTY_MAP ? EMPTY_MAP : + CollectionFactory.newIntObjectMap(source.properties); + this.propLoaded(true); + } + + public void updateDefaultValues(Function keys) { + if (this.fresh || this.defaultValueUpdated) { + return; + } + this.defaultValueUpdated = true; + for (Id id : this.schemaLabel().properties()) { + if (!this.hasProperty(id)) { + PropertyKey key = keys.apply(id); + Object value = key.defaultValue(); + if (value != null) { + this.addProperty(newProperty(key, value)); + } + } + } + } + @Override public BaseElement clone() throws CloneNotSupportedException{ return (BaseElement) super.clone(); diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseProperty.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseProperty.java index d70384b631..763812eeea 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseProperty.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseProperty.java @@ -62,7 +62,7 @@ public PropertyKey propertyKey() { } public Object serialValue(boolean encodeNumber) { - return this.propertyKey.serialValue(this.value, encodeNumber); + return this.propertyKey.serialValue(this.value(), encodeNumber); } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseVertex.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseVertex.java index fe9ab832d7..6982d69a2d 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseVertex.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/BaseVertex.java @@ -21,11 +21,13 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; import java.util.List; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.id.SplicingIdGenerator; import org.apache.hugegraph.perf.PerfUtil; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.struct.schema.SchemaLabel; import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.type.HugeType; @@ -42,6 +44,14 @@ public class BaseVertex extends BaseElement implements Cloneable { protected Collection edges; + private boolean encodeNumber = true; + private TypeContext typeContext = TypeContext.STORAGE; + + /** Classification differs for legacy storage-only and engine system labels. */ + public enum TypeContext { + STORAGE, + ENGINE + } public BaseVertex(Id id) { this.edges = EMPTY_LIST; @@ -50,11 +60,29 @@ public BaseVertex(Id id) { public BaseVertex(Id id, SchemaLabel label) { // Note: - // If vertex is OLAP Vertex, id is the id of the vertex that the olap property belongs to, not including the olap property id. + // An OLAP vertex uses the owning vertex ID without the OLAP property ID. this(id); this.schemaLabel(label); } + public BaseVertex(Id id, SchemaLabel label, TypeContext typeContext) { + this(id, label); + this.typeContext(typeContext); + } + + public TypeContext typeContext() { + return this.typeContext; + } + + public void typeContext(TypeContext typeContext) { + E.checkArgumentNotNull(typeContext, "Vertex type context can't be null"); + this.typeContext = typeContext; + } + + public void encodeNumber(boolean encodeNumber) { + this.encodeNumber = encodeNumber; + } + @Override public String name() { E.checkState(this.schemaLabel().idStrategy() == IdStrategy.PRIMARY_KEY, @@ -73,7 +101,7 @@ public String name() { E.checkState(!propValues.isEmpty(), "Primary values must not be empty " + "(has properties %s)", hasProperties()); - name = SplicingIdGenerator.concatValues(propValues); + name = ConditionQuery.concatValues(propValues); E.checkArgument(!name.isEmpty(), "The value of primary key can't be empty"); } @@ -94,18 +122,38 @@ public List primaryValues() { for (Id pk : primaryKeys) { BaseProperty property = this.getProperty(pk); E.checkState(property != null, - "The value of primary key '%s' can't be null" - /*this.graph().propertyKey(pk).name() complete log*/); - propValues.add(property.serialValue(true)); + "The value of primary key '%s' can't be null", pk); + Object value = property.serialValue(this.encodeNumber); + if ("".equals(value)) { + value = ConditionQuery.INDEX_VALUE_EMPTY; + } + propValues.add(value); } return propValues; } - public void addEdge(BaseEdge edge) { + public boolean addEdge(BaseEdge edge) { if (this.edges == EMPTY_LIST) { this.edges = CollectionFactory.newList(CollectionType.EC); } - this.edges.add(edge); + return this.edges.add(edge); + } + + public void resetEdges(boolean unique) { + this.edges = unique ? CollectionFactory.newSet(CollectionType.EC) : + CollectionFactory.newList(CollectionType.EC); + } + + public BaseEdge removeEdge(BaseEdge edge) { + Iterator iterator = this.edges.iterator(); + while (iterator.hasNext()) { + BaseEdge current = iterator.next(); + if (current.equals(edge)) { + iterator.remove(); + return current; + } + } + return null; } public void correctVertexLabel(VertexLabel correctLabel) { @@ -154,14 +202,18 @@ public boolean olap() { } public HugeType type() { - // For Vertex type, when label is task, return TASK type, convenient for getting storage table information based on type - /* Magic: ~task ~taskresult ~variables*/ - if (schemaLabel() != null && - (schemaLabel().name().equals("~task") || - schemaLabel().name().equals("~taskresult") || - schemaLabel().name().equals("~variables"))) { + if (this.schemaLabel() == null) { + return HugeType.VERTEX; + } + String label = this.schemaLabel().name(); + if ("~task".equals(label) || "~taskresult".equals(label) || + this.typeContext == TypeContext.STORAGE && "~variables".equals(label)) { return HugeType.TASK; } + if (this.typeContext == TypeContext.ENGINE && + ("~server".equals(label) || "~role_data".equals(label))) { + return HugeType.SERVER; + } return HugeType.VERTEX; } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/Index.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/Index.java index deffcde1ec..0c7c9e401d 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/Index.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/Index.java @@ -37,6 +37,7 @@ import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.DataType; import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.HashUtil; import org.apache.hugegraph.util.InsertionOrderUtil; import org.apache.hugegraph.util.NumericUtil; @@ -44,6 +45,8 @@ public class Index implements GraphType, Cloneable { + private static final int HUGE_TYPE_CODE_LENGTH = 1; + private final HugeGraphSupplier graph; private Object fieldValues; private final IndexLabel indexLabel; @@ -55,13 +58,7 @@ public class Index implements GraphType, Cloneable { private IdWithExpiredTime elementId; public Index(HugeGraphSupplier graph, IndexLabel indexLabel) { - E.checkNotNull(graph, "graph"); - E.checkNotNull(indexLabel, "label"); - E.checkNotNull(indexLabel.id(), "label id"); - this.graph = graph; - this.indexLabel = indexLabel; - this.elementIds = new LinkedHashSet<>(); - this.fieldValues = null; + this(graph, indexLabel, false); } public Index(HugeGraphSupplier graph, IndexLabel indexLabel, boolean write) { @@ -100,6 +97,10 @@ public Id id() { return formatIndexId(type(), this.indexLabelId(), this.fieldValues()); } + public Id hashId() { + return formatIndexHashId(type(), this.indexLabelId(), this.fieldValues()); + } + public Object fieldValues() { return this.fieldValues; } @@ -118,6 +119,7 @@ public IndexLabel indexLabel() { public IdWithExpiredTime elementIdWithExpiredTime() { if (this.elementIds == null) { + E.checkState(this.elementId != null, "Expect one element id, actual 0"); return this.elementId; } E.checkState(this.elementIds.size() == 1, @@ -144,8 +146,15 @@ public Set elementIds() { public Set expiredElementIds() { long now = this.graph.now(); Set expired = InsertionOrderUtil.newSet(); + if (this.elementIds == null) { + if (this.elementId != null && expired(this.elementId, now)) { + expired.add(this.elementId); + this.elementId = null; + } + return expired; + } for (IdWithExpiredTime id : this.elementIds) { - if (0L < id.expiredTime && id.expiredTime < now) { + if (expired(id, now)) { expired.add(id); } } @@ -153,6 +162,10 @@ public Set expiredElementIds() { return expired; } + private static boolean expired(IdWithExpiredTime id, long now) { + return 0L < id.expiredTime && id.expiredTime < now; + } + public void elementIds(Id elementId) { this.elementIds(elementId, 0L); } @@ -166,7 +179,11 @@ public void elementIds(Id elementId, long expiredTime) { } public void resetElementIds() { - this.elementIds = null; + if (this.elementIds == null) { + this.elementId = null; + } else { + this.elementIds = new LinkedHashSet<>(); + } } public long expiredTime() { @@ -174,17 +191,10 @@ public long expiredTime() { } public boolean hasTtl() { - if ((this.indexLabel() == IndexLabel.label(HugeType.VERTEX) || - this.indexLabel() == IndexLabel.label(HugeType.EDGE)) && - this.expiredTime() > 0) { - // LabelIndex index, if element has expiration time, then index also has TTL - return true; - } - if (this.indexLabel.system()) { return false; } - return this.indexLabel.baseElement().ttl() > 0L; + return this.indexLabel.baseLabel().ttl() > 0L; } public long ttl() { @@ -224,6 +234,14 @@ public String toString() { } + public static Id formatIndexHashId(HugeType type, Id indexLabel, + Object fieldValues) { + E.checkState(!type.isRangeIndex(), + "RangeIndex can't return a hash id"); + String value = fieldValues == null ? "" : fieldValues.toString(); + return formatIndexId(type, indexLabel, HashUtil.hash(value)); + } + public static Id formatIndexId(HugeType type, Id indexLabelId, Object fieldValues) { if (type.isStringIndex()) { @@ -239,12 +257,11 @@ public static Id formatIndexId(HugeType type, Id indexLabelId, */ String strIndexLabelId = IdGenerator.asStoredString(indexLabelId); // Add id prefix according to type - return SplicingIdGenerator.splicing(type.string(), strIndexLabelId, value); + return SplicingIdGenerator.splicing(type.string(), strIndexLabelId, value); } else { assert type.isRangeIndex(); int length = type.isRange4Index() ? 4 : 8; - // 1 is table type, 4 is labelId, length is value - BytesBuffer buffer = BytesBuffer.allocate(1 + 4 + length); + BytesBuffer buffer = BytesBuffer.allocate(HUGE_TYPE_CODE_LENGTH + 4 + length); // Add table type id buffer.write(type.code()); @@ -276,9 +293,7 @@ public static Index parseIndexId(HugeGraphSupplier graph, HugeType type, final int labelLength = 4; E.checkState(id.length > labelLength, "Invalid range index id"); BytesBuffer buffer = BytesBuffer.wrap(id); - // Read the first byte representing the table type - final int hugeTypeCodeLength = 1; - byte[] read = buffer.read(hugeTypeCodeLength); + buffer.read(HUGE_TYPE_CODE_LENGTH); Id label = IdGenerator.of(buffer.readInt()); indexLabel = IndexLabel.label(graph, label); @@ -289,7 +304,8 @@ public static Index parseIndexId(HugeGraphSupplier graph, HugeType type, "Invalid range index field type"); Class clazz = dataType.isNumber() ? dataType.clazz() : DataType.LONG.clazz(); - values = bytes2number(buffer.read(id.length - labelLength - hugeTypeCodeLength), clazz); + values = bytes2number(buffer.read(id.length - labelLength - HUGE_TYPE_CODE_LENGTH), + clazz); } Index index = new Index(graph, indexLabel); index.fieldValues(values); diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/builder/IndexBuilder.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/builder/IndexBuilder.java index cc1483f39e..72c5ebc6a7 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/builder/IndexBuilder.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/builder/IndexBuilder.java @@ -22,7 +22,11 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.HugeGraphSupplier; @@ -39,21 +43,29 @@ import org.apache.hugegraph.structure.BaseProperty; import org.apache.hugegraph.structure.BaseVertex; import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.util.CollectionUtil; import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.InsertionOrderUtil; import org.apache.hugegraph.util.Log; import org.apache.hugegraph.util.NumericUtil; import org.slf4j.Logger; +import com.google.common.collect.ImmutableSet; + public class IndexBuilder { private static final Logger LOG = Log.logger(IndexBuilder.class); private final HugeGraphSupplier graph; private final Analyzer textAnalyzer; + public static final String START_SYMBOL = "("; + public static final String END_SYMBOL = ")"; + public static final String WORD_DELIMITER = "|"; + - public static final String INDEX_SYM_NULL = "\u0001"; - public static final String INDEX_SYM_EMPTY = "\u0002"; - public static final char INDEX_SYM_MAX = '\u0003'; + public static final String INDEX_SYM_NULL = ConditionQuery.INDEX_SYM_NULL; + public static final String INDEX_SYM_EMPTY = ConditionQuery.INDEX_SYM_EMPTY; + public static final char INDEX_SYM_MAX = ConditionQuery.INDEX_SYM_MAX; private static final String TEXT_ANALYZER = "search.text_analyzer"; private static final String TEXT_ANALYZER_MODE = @@ -63,18 +75,21 @@ public class IndexBuilder { private static final String DEFAULT_TEXT_ANALYZER_MODE = "smart"; public IndexBuilder(HugeGraphSupplier graph) { - this.graph = graph; + this(graph, createAnalyzer(graph)); + } - String name = graph.configuration().get(String.class, TEXT_ANALYZER); - String mode = graph.configuration().get(String.class, - TEXT_ANALYZER_MODE); + public IndexBuilder(HugeGraphSupplier graph, Analyzer textAnalyzer) { + this.graph = Objects.requireNonNull(graph, "graph"); + this.textAnalyzer = Objects.requireNonNull(textAnalyzer, "textAnalyzer"); + } + private static Analyzer createAnalyzer(HugeGraphSupplier graph) { + String name = graph.configuration().get(String.class, TEXT_ANALYZER); + String mode = graph.configuration().get(String.class, TEXT_ANALYZER_MODE); name = name == null ? DEFAULT_TEXT_ANALYZER : name; mode = mode == null ? DEFAULT_TEXT_ANALYZER_MODE : mode; - - LOG.debug("Loading text analyzer '{}' with mode '{}' for graph '{}'", - name, mode, graph.name()); - this.textAnalyzer = AnalyzerFactory.analyzer(name, mode); + LOG.debug("Loading text analyzer '{}' with mode '{}' for graph '{}'", name, mode, graph.name()); + return AnalyzerFactory.analyzer(name, mode); } public List buildLabelIndex(BaseElement element) { @@ -107,19 +122,17 @@ public List buildLabelIndex(BaseElement element) { return indexList; } - public List buildVertexOlapIndex(BaseVertex vertex) { - - List indexs = new ArrayList<>(); - - Id pkId = vertex.getProperties().keySet().iterator().next(); - Collection indexLabels = graph.indexLabels(); - for (IndexLabel il : indexLabels) { - if (il.indexFields().contains(pkId)) { - indexs.addAll(this.buildIndex(vertex, il)); + public static void forEachOlapIndexLabelId(BaseVertex vertex, Iterable candidates, + Consumer sink) { + int properties = vertex.sizeOfProperties(); + E.checkArgument(properties == 1, + "Expect only 1 property for olap vertex, but got %s", properties); + Id propertyId = vertex.properties().values().iterator().next().propertyKey().id(); + for (IndexLabel label : candidates) { + if (label.indexFields().contains(propertyId)) { + sink.accept(label.id()); } } - - return indexs; } public List buildVertexIndex(BaseVertex vertex) { @@ -131,9 +144,8 @@ public List buildVertexIndex(BaseVertex vertex) { indexs.addAll(this.buildLabelIndex(vertex)); } - for (Id il : label.indexLabels()) { - indexs.addAll(this.buildIndex(vertex, graph.indexLabel(il))); - } + forEachIndexLabelId(vertex, this.graph::edgeLabel, + id -> this.forEachIndex(vertex, this.graph.indexLabel(id), indexs::add)); return indexs; } @@ -148,13 +160,28 @@ public List buildEdgeIndex(BaseEdge edge) { } - for (Id il : label.indexLabels()) { - indexs.addAll(this.buildIndex(edge, graph.indexLabel(il))); - } + forEachIndexLabelId(edge, this.graph::edgeLabel, + id -> this.forEachIndex(edge, this.graph.indexLabel(id), indexs::add)); return indexs; } + public static void forEachIndexLabelId(BaseElement element, Function edgeLabelLookup, + Consumer sink) { + for (Id id : element.schemaLabel().indexLabels()) { + sink.accept(id); + } + if (element instanceof BaseEdge) { + EdgeLabel label = (EdgeLabel) element.schemaLabel(); + if (label.hasFather()) { + // Preserve child-first order and repeated IDs from the engine's existing loops. + for (Id id : edgeLabelLookup.apply(label.fatherId()).indexLabels()) { + sink.accept(id); + } + } + } + } + /** * Build index(user properties) of vertex or edge * Notice: This method does not use unique index validation to check if the current element already exists @@ -163,10 +190,14 @@ public List buildEdgeIndex(BaseEdge edge) { * @param element the properties owner */ public List buildIndex(BaseElement element, IndexLabel indexLabel) { - E.checkArgument(indexLabel != null, - "Not exist index label with id '%s'", indexLabel.id()); + List indexes = new ArrayList<>(); + this.forEachIndex(element, indexLabel, indexes::add); + return indexes; + } - List indexs = new ArrayList<>(); + public void forEachIndex(BaseElement element, IndexLabel indexLabel, Consumer sink) { + E.checkArgumentNotNull(indexLabel, "Index label cannot be null"); + Objects.requireNonNull(sink, "sink"); // Collect property values of index fields List allPropValues = new ArrayList<>(); @@ -181,18 +212,15 @@ public List buildIndex(BaseElement element, IndexLabel indexLabel) { if (firstNullField == fieldsNum) { firstNullField = allPropValues.size(); } - allPropValues.add(INDEX_SYM_NULL); + allPropValues.add(ConditionQuery.INDEX_VALUE_NULL); } else { - E.checkArgument(!INDEX_SYM_NULL.equals(property.value()), - "Illegal value of index property: '%s'", - INDEX_SYM_NULL); allPropValues.add(property.value()); } } if (firstNullField == 0 && !indexLabel.indexType().isUnique()) { // The property value of first index field is null - return indexs; + return; } // Not build index for record with nullable field (except unique index) List propValues = allPropValues.subList(0, firstNullField); @@ -209,7 +237,7 @@ public List buildIndex(BaseElement element, IndexLabel indexLabel) { E.checkState(propValues.size() == 1, "Expect only one property in range index"); Object value = NumericUtil.convertToNumber(propValues.get(0)); - indexs.add(this.buildIndex(indexLabel, value, element.id(), + sink.accept(this.buildIndex(indexLabel, value, element.id(), expiredTime)); break; case SEARCH: @@ -219,7 +247,7 @@ public List buildIndex(BaseElement element, IndexLabel indexLabel) { Set words = this.segmentWords(propertyValueToString(value)); for (String word : words) { - indexs.add(this.buildIndex(indexLabel, word, element.id(), + sink.accept(this.buildIndex(indexLabel, word, element.id(), expiredTime)); } break; @@ -232,10 +260,9 @@ public List buildIndex(BaseElement element, IndexLabel indexLabel) { */ for (Object propValue : (Collection) propValues.get(0)) { - value = ConditionQuery.concatValuesLimitLength( + value = ConditionQuery.concatValues( propValue); - value = escapeIndexValueIfNeeded((String) value); - indexs.add(this.buildIndex(indexLabel, value, + sink.accept(this.buildIndex(indexLabel, value, element.id(), expiredTime)); } @@ -243,25 +270,23 @@ public List buildIndex(BaseElement element, IndexLabel indexLabel) { for (int i = 0, n = propValues.size(); i < n; i++) { List prefixValues = propValues.subList(0, i + 1); - value = ConditionQuery.concatValuesLimitLength( + value = ConditionQuery.concatValues( prefixValues); - value = escapeIndexValueIfNeeded((String) value); - indexs.add(this.buildIndex(indexLabel, value, + sink.accept(this.buildIndex(indexLabel, value, element.id(), expiredTime)); } } break; case SHARD: - value = ConditionQuery.concatValuesLimitLength(propValues); - value = escapeIndexValueIfNeeded((String) value); - indexs.add(this.buildIndex(indexLabel, value, element.id(), + value = ConditionQuery.concatValues(propValues); + sink.accept(this.buildIndex(indexLabel, value, element.id(), expiredTime)); break; case UNIQUE: - value = ConditionQuery.concatValuesLimitLength(allPropValues); + value = ConditionQuery.concatValues(allPropValues); assert !"".equals(value); - indexs.add(this.buildIndex(indexLabel, value, element.id(), + sink.accept(this.buildIndex(indexLabel, value, element.id(), expiredTime)); break; default: @@ -269,7 +294,6 @@ public List buildIndex(BaseElement element, IndexLabel indexLabel) { "Unknown index type '%s'", indexLabel.indexType())); } - return indexs; } private Index buildIndex(IndexLabel indexLabel, Object propValue, @@ -282,26 +306,6 @@ private Index buildIndex(IndexLabel indexLabel, Object propValue, } - private static String escapeIndexValueIfNeeded(String value) { - for (int i = 0; i < value.length(); i++) { - char ch = value.charAt(i); - if (ch <= INDEX_SYM_MAX) { - /* - * Escape symbols can't be used due to impossible to parse, - * and treat it as illegal value for the origin text property - */ - E.checkArgument(false, "Illegal char '\\u000%s' " + - "in index property: '%s'", (int) ch, - value); - } - } - if (value.isEmpty()) { - // Escape empty String to INDEX_SYM_EMPTY (char `\u0002`) - value = INDEX_SYM_EMPTY; - } - return value; - } - private static boolean hasNullableProp(BaseElement element, Id key) { return element.schemaLabel().nullableKeys().contains(key); } @@ -311,17 +315,30 @@ private static boolean isCollectionIndex(List propValues) { propValues.get(0) instanceof Collection; } - private Set segmentWords(String text) { - return this.textAnalyzer.segment(text); + public Set segmentWords(String text) { + return segmentWords(this.textAnalyzer, text); + } + + public static Predicate searchPredicate(Analyzer analyzer, String text) { + Set words = segmentWords(analyzer, text); + return value -> CollectionUtil.hasIntersection(words, + segmentWords(analyzer, propertyValueToString(value))); + } + + private static Set segmentWords(Analyzer analyzer, String text) { + if (text.startsWith(START_SYMBOL) && text.endsWith(END_SYMBOL)) { + String words = text.substring(1, text.length() - 1); + return words.contains(WORD_DELIMITER) ? + ImmutableSet.copyOf(StringUtils.split(words, WORD_DELIMITER)) : ImmutableSet.of(words); + } + Set segments = InsertionOrderUtil.newSet(); + segments.addAll(analyzer.segment(text)); + segments.add(text); + segments.removeAll(ConditionQuery.IGNORE_SYM_SET); + return segments; } - private static String propertyValueToString(Object value) { - /* - * Join collection items with white space if the value is Collection, - * or else keep the origin value. - */ - return value instanceof Collection ? - StringUtils.join(((Collection) value).toArray(), " ") : - value.toString(); + public static String propertyValueToString(Object value) { + return value instanceof Collection ? StringUtils.join((Iterable) value, " ") : value.toString(); } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/HugeType.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/HugeType.java index 2fac93100a..122036ae38 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/HugeType.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/HugeType.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.type; @@ -123,9 +121,8 @@ public boolean isGraph() { } public boolean isVertex() { - // Consider task vertex variable as the same, all used to store HugeVertex structure - return this == HugeType.VERTEX || this == HugeType.TASK || - this == HugeType.VARIABLE || this == HugeType.SERVER; + return this == HugeType.VERTEX || this == HugeType.TASK || this == HugeType.SERVER || + this == HugeType.VARIABLE; } public boolean isEdge() { diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/Idfiable.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/Idfiable.java index c5a58c0eb1..59b8f1e3a3 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/Idfiable.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/Idfiable.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.type; @@ -23,5 +21,5 @@ public interface Idfiable { - public Id id(); + Id id(); } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Action.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Action.java index 042594c224..9561d52181 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Action.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Action.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.type.define; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/AggregateType.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/AggregateType.java index e949d4af14..31c0f6212d 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/AggregateType.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/AggregateType.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.type.define; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Cardinality.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Cardinality.java index 9f34a96dee..cf10a5acf7 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Cardinality.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Cardinality.java @@ -14,8 +14,6 @@ package org.apache.hugegraph.type.define; -import org.apache.tinkerpop.gremlin.structure.VertexProperty; - /** * The cardinality of the values associated with given key for a particular element. * @@ -39,8 +37,8 @@ public enum Cardinality implements SerialEnum { */ SET(3, "set"); - private byte code = 0; - private String name = null; + private final byte code; + private final String name; static { SerialEnum.register(Cardinality.class); @@ -69,17 +67,4 @@ public boolean multiple() { return this == LIST || this == SET; } - public static Cardinality convert(VertexProperty.Cardinality cardinality) { - switch (cardinality) { - case single: - return SINGLE; - case list: - return LIST; - case set: - return SET; - default: - throw new AssertionError(String.format("Unrecognized cardinality: '%s'", - cardinality)); - } - } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/CollectionType.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/CollectionType.java index e8ff98ec95..e8a4cd3547 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/CollectionType.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/CollectionType.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.type.define; @@ -62,7 +60,7 @@ public static CollectionType fromCode(byte code) { return FU; default: throw new AssertionError( - "Unsupported collection code: " + code); + "Unsupported collection code: " + code); } } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/DataType.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/DataType.java index 6a04a83034..c5c6c35361 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/DataType.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/DataType.java @@ -20,6 +20,7 @@ package org.apache.hugegraph.type.define; import java.nio.ByteBuffer; +import java.time.OffsetDateTime; import java.util.Date; import java.util.List; import java.util.UUID; @@ -155,6 +156,8 @@ public Date valueToDate(V value) { } if (value instanceof Date) { return (Date) value; + } else if (value instanceof OffsetDateTime) { + return Date.from(((OffsetDateTime) value).toInstant()); } else if (value instanceof Integer) { return new Date(((Number) value).intValue()); } else if (value instanceof Long) { diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Frequency.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Frequency.java index 4ebe24867a..3fd1c82332 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Frequency.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Frequency.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.type.define; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/HugeKeys.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/HugeKeys.java index c396e429c5..0e3536b011 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/HugeKeys.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/HugeKeys.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.type.define; @@ -53,7 +51,6 @@ public enum HugeKeys { EDGELABEL_TYPE(87, "edgelabel_type"), PARENT_LABEL(89, "parent_label"), - /* Column names of schema type (PropertyKey) */ DATA_TYPE(120, "data_type"), CARDINALITY(121, "cardinality"), @@ -82,11 +79,10 @@ public enum HugeKeys { SORT_VALUES(206, "sort_values"), PRIMARY_VALUES(207, "primary_values"), EXPIRED_TIME(208, "expired_time"), - SUB_LABEL(211,"sub_label"), + SUB_LABEL(211, "sub_label"), PROPERTY_TYPE(249, "property_type"), - AGGREGATE_PROPERTIES(250, "aggregate_properties"), - ; + AGGREGATE_PROPERTIES(250, "aggregate_properties"); public static final long NORMAL_PROPERTY_ID = 0L; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/IdStrategy.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/IdStrategy.java index 4149c8db91..da9f51313c 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/IdStrategy.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/IdStrategy.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.type.define; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/IndexType.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/IndexType.java index 77e59932e7..019ac98df9 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/IndexType.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/IndexType.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.type.define; diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/SerialEnum.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/SerialEnum.java index 337c981a76..3cb59b0450 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/SerialEnum.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/SerialEnum.java @@ -1,65 +1,65 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.type.define; -import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.type.HugeType; - import org.apache.hugegraph.util.CollectionUtil; import org.apache.hugegraph.util.E; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; public interface SerialEnum { - public byte code(); - -// static Table, Byte, SerialEnum> table = HashBasedTable.create(); + byte code(); - static Map>table =new ConcurrentHashMap<>(); + Table, Byte, SerialEnum> TABLE = HashBasedTable.create(); - public static void register(Class clazz) { + static void register(Class clazz) { Object enums; try { enums = clazz.getMethod("values").invoke(null); } catch (Exception e) { - throw new HugeException("Exception in backend", e); + throw new BackendException(e); } - ConcurrentHashMap map=new ConcurrentHashMap(); - for (SerialEnum e : CollectionUtil.toList(enums)) { - map.put(e.code(), e); + // Resolve enum initialization before locking: class initializers register here too. + synchronized (TABLE) { + for (SerialEnum e : CollectionUtil.toList(enums)) { + TABLE.put(clazz, e.code(), e); + } } - table.put(clazz,map); } - - public static T fromCode(Class clazz, byte code) { - Map clazzMap=table.get(clazz); - if (clazzMap == null) { - SerialEnum.register(clazz); - clazzMap=table.get(clazz); + static T fromCode(Class clazz, byte code) { + boolean registered; + synchronized (TABLE) { + registered = TABLE.containsRow(clazz); + } + if (!registered) { + register(clazz); + } + T value; + synchronized (TABLE) { + @SuppressWarnings("unchecked") + T entry = (T) TABLE.get(clazz, code); + value = entry; } - E.checkArgument(clazzMap != null, "Can't get class registery for %s", - clazz.getSimpleName()); - T value = (T) clazzMap.get(code); if (value == null) { E.checkArgument(false, "Can't construct %s from code %s", clazz.getSimpleName(), code); @@ -67,7 +67,7 @@ public static T fromCode(Class clazz, byte code) { return value; } - public static void registerInternalEnums() { + static void registerInternalEnums() { SerialEnum.register(Action.class); SerialEnum.register(AggregateType.class); SerialEnum.register(Cardinality.class); @@ -78,6 +78,5 @@ public static void registerInternalEnums() { SerialEnum.register(IdStrategy.class); SerialEnum.register(IndexType.class); SerialEnum.register(SchemaStatus.class); -// SerialEnum.register(HugePermission.class); } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/WriteType.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/WriteType.java index 538b5bc40b..ea1018eabd 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/WriteType.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/WriteType.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.type.define; @@ -33,8 +31,8 @@ public enum WriteType implements SerialEnum { // OLAP property key with range index OLAP_RANGE(4, "olap_range"); - private final byte code; - private final String name; + private byte code = 0; + private String name = null; static { SerialEnum.register(WriteType.class); diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/util/Blob.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/Blob.java index 03d82e916e..5b5d8ec6ad 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/util/Blob.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/Blob.java @@ -1,29 +1,24 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.util; import java.util.Arrays; -import org.apache.hugegraph.util.Bytes; -import org.apache.hugegraph.util.E; - public class Blob implements Comparable { public static final Blob EMPTY = new Blob(new byte[0]); diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/util/KryoUtil.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/KryoUtil.java new file mode 100644 index 0000000000..0be4c6641b --- /dev/null +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/KryoUtil.java @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.query.Condition.RelationType; +import org.apache.tinkerpop.shaded.kryo.Kryo; +import org.apache.tinkerpop.shaded.kryo.KryoException; +import org.apache.tinkerpop.shaded.kryo.Registration; +import org.apache.tinkerpop.shaded.kryo.Serializer; +import org.apache.tinkerpop.shaded.kryo.io.Input; +import org.apache.tinkerpop.shaded.kryo.io.Output; +import org.apache.tinkerpop.shaded.kryo.util.DefaultClassResolver; +import org.apache.tinkerpop.shaded.kryo.util.IntMap; +import org.apache.tinkerpop.shaded.kryo.util.MapReferenceResolver; +import org.apache.tinkerpop.shaded.kryo.util.ObjectMap; + +public final class KryoUtil { + + private static final ThreadLocal KRYOS = new ThreadLocal<>(); + + public static Kryo kryo() { + Kryo kryo = KRYOS.get(); + if (kryo != null) { + return kryo; + } + + kryo = new Kryo(new LegacyClassResolver(), new MapReferenceResolver()); + registerSerializers(kryo); + KRYOS.set(kryo); + return kryo; + } + + public static byte[] toKryo(Object value) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + Output output = new Output(bos, 256)) { + kryo().writeObject(output, value); + output.flush(); + return bos.toByteArray(); + } catch (IOException e) { + throw new BackendException("Failed to serialize: %s", e, value); + } + } + + public static T fromKryo(byte[] value, Class clazz) { + E.checkState(value != null, + "Kryo value can't be null for '%s'", + clazz.getSimpleName()); + return kryo().readObject(new Input(value), clazz); + } + + public static byte[] toKryoWithType(Object value) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + Output output = new Output(bos, 256)) { + kryo().writeClassAndObject(output, value); + output.flush(); + return bos.toByteArray(); + } catch (IOException e) { + throw new BackendException("Failed to serialize: %s", e, value); + } + } + + @SuppressWarnings("unchecked") + public static T fromKryoWithType(byte[] value) { + E.checkState(value != null, "Kryo value can't be null for object"); + return (T) kryo().readClassAndObject(new Input(value)); + } + + /** Resolve relocated names only when Kryo decodes a class descriptor. */ + private static class LegacyClassResolver extends DefaultClassResolver { + + private static final String LEGACY_RELATION = + "org.apache.hugegraph.backend.query.Condition$RelationType"; + private final Map legacyRelations = new HashMap<>(); + + @Override + protected Registration readName(Input input) { + // Kryo shaded by Gremlin 3.5.1 names contain a varint ID and, on first use, a string. + // Consume once: streamed Input may refill, making position rewind unsafe. + int nameId = input.readVarInt(true); + if (this.nameIdToClass == null) { + this.nameIdToClass = new IntMap<>(); + } + Class type = this.nameIdToClass.get(nameId); + String name = null; + if (type == null) { + name = input.readString(); + type = this.getTypeByName(name); + if (type == null) { + try { + type = Class.forName(name, false, this.kryo.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new KryoException("Unable to find class: " + name, e); + } + if (this.nameToClass == null) { + this.nameToClass = new ObjectMap<>(); + } + this.nameToClass.put(name, type); + } + this.nameIdToClass.put(nameId, type); + } + Registration registration = this.kryo.getRegistration(type); + Registration legacy = this.legacyRelations.get(nameId); + if (legacy == null && name != null) { + Serializer serializer = null; + if (LEGACY_RELATION.equals(name)) { + serializer = new LegacyRelationSerializer(); + } else if (name.startsWith("[") && name.endsWith("L" + LEGACY_RELATION + ";")) { + serializer = new LegacyRelationArraySerializer(registration.getType()); + } + if (serializer != null) { + // Never register this globally: canonical descriptors use other ordinals. + legacy = new Registration(registration.getType(), serializer, NAME); + this.legacyRelations.put(nameId, legacy); + } + } + return legacy != null ? legacy : registration; + } + + @Override + public void reset() { + super.reset(); + this.legacyRelations.clear(); + } + + @Override + protected Class getTypeByName(String name) { + Class type = LegacyClassNames.lookup(name); + return type != null ? type : super.getTypeByName(name); + } + } + + /** The frozen core enum order differs from the existing shared enum order. */ + private static class LegacyRelationSerializer extends Serializer { + + private static final RelationType[] VALUES = { + RelationType.EQ, RelationType.GT, RelationType.GTE, RelationType.LT, + RelationType.LTE, RelationType.NEQ, RelationType.IN, RelationType.NOT_IN, + RelationType.PREFIX, RelationType.TEXT_CONTAINS, RelationType.TEXT_CONTAINS_ANY, + RelationType.CONTAINS, RelationType.CONTAINS_VALUE, RelationType.CONTAINS_KEY, + RelationType.SCAN + }; + + private LegacyRelationSerializer() { + this.setAcceptsNull(true); + this.setImmutable(true); + } + + @Override + public RelationType read(Kryo kryo, Input input, Class type) { + int ordinal = input.readVarInt(true); + if (ordinal == 0) { + return null; + } + if (ordinal < 0 || ordinal > VALUES.length) { + throw new KryoException("Invalid legacy relation ordinal: " + ordinal); + } + return VALUES[ordinal - 1]; + } + + @Override + public void write(Kryo kryo, Output output, RelationType value) { + kryo.getSerializer(RelationType.class).write(kryo, output, value); + } + } + + /** Typed enum arrays omit element descriptors, so retain the enclosing descriptor's codec. */ + private static class LegacyRelationArraySerializer extends Serializer { + + private final Serializer componentSerializer; + + private LegacyRelationArraySerializer(Class type) { + this.setAcceptsNull(true); + Class component = type.getComponentType(); + this.componentSerializer = component == RelationType.class ? new LegacyRelationSerializer() : + new LegacyRelationArraySerializer(component); + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public Object[] read(Kryo kryo, Input input, Class type) { + int length = input.readVarInt(true); + if (length == 0) { + return null; + } + Class component = type.getComponentType(); + Object[] values = (Object[]) Array.newInstance(component, length - 1); + kryo.reference(values); + for (int i = 0; i < values.length; i++) { + values[i] = kryo.readObjectOrNull(input, component, (Serializer) this.componentSerializer); + } + return values; + } + + @Override + public void write(Kryo kryo, Output output, Object[] values) { + kryo.getSerializer(values.getClass()).write(kryo, output, values); + } + } + + private static void registerSerializers(Kryo kryo) { + kryo.addDefaultSerializer(UUID.class, new Serializer() { + + @Override + public UUID read(Kryo kryo, Input input, Class c) { + return new UUID(input.readLong(), input.readLong()); + } + + @Override + public void write(Kryo kryo, Output output, UUID uuid) { + output.writeLong(uuid.getMostSignificantBits()); + output.writeLong(uuid.getLeastSignificantBits()); + } + }); + } +} diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/util/LZ4Util.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/LZ4Util.java index 98f23b9b29..253d813953 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/util/LZ4Util.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/LZ4Util.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.util; @@ -39,25 +37,20 @@ public static BytesBuffer compress(byte[] bytes, int blockSize) { return compress(bytes, blockSize, DEFAULT_BUFFER_RATIO); } - public static BytesBuffer compress(byte[] bytes, int blockSize, - float bufferRatio) { + public static BytesBuffer compress(byte[] bytes, int blockSize, float bufferRatio) { float ratio = bufferRatio <= 0.0F ? DEFAULT_BUFFER_RATIO : bufferRatio; LZ4Factory factory = LZ4Factory.fastestInstance(); LZ4Compressor compressor = factory.fastCompressor(); int initBufferSize = Math.round(bytes.length / ratio); BytesBuffer buf = new BytesBuffer(initBufferSize); - LZ4BlockOutputStream lz4Output = new LZ4BlockOutputStream( - buf, blockSize, compressor); + LZ4BlockOutputStream lz4Output = new LZ4BlockOutputStream(buf, blockSize, compressor); try { lz4Output.write(bytes); lz4Output.close(); } catch (IOException e) { throw new BackendException("Failed to compress", e); } - /* - * If need to perform reading outside the method, - * remember to call forReadWritten() - */ + // If we need to perform reading outside the method, remember to call forReadWritten() return buf; } @@ -65,17 +58,15 @@ public static BytesBuffer decompress(byte[] bytes, int blockSize) { return decompress(bytes, blockSize, DEFAULT_BUFFER_RATIO); } - public static BytesBuffer decompress(byte[] bytes, int blockSize, - float bufferRatio) { + public static BytesBuffer decompress(byte[] bytes, int blockSize, float bufferRatio) { float ratio = bufferRatio <= 0.0F ? DEFAULT_BUFFER_RATIO : bufferRatio; LZ4Factory factory = LZ4Factory.fastestInstance(); LZ4FastDecompressor decompressor = factory.fastDecompressor(); ByteArrayInputStream bais = new ByteArrayInputStream(bytes); int initBufferSize = Math.min(Math.round(bytes.length * ratio), - BytesBuffer.MAX_BUFFER_CAPACITY); + BytesBuffer.maxBufferCapacity()); BytesBuffer buf = new BytesBuffer(initBufferSize); - LZ4BlockInputStream lzInput = new LZ4BlockInputStream(bais, - decompressor); + LZ4BlockInputStream lzInput = new LZ4BlockInputStream(bais, decompressor); int count; byte[] buffer = new byte[blockSize]; try { @@ -86,10 +77,7 @@ public static BytesBuffer decompress(byte[] bytes, int blockSize, } catch (IOException e) { throw new BackendException("Failed to decompress", e); } - /* - * If need to perform reading outside the method, - * remember to call forReadWritten() - */ + // If we need to perform reading outside the method, remember to call forReadWritten() return buf; } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/util/LegacyClassNames.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/LegacyClassNames.java new file mode 100644 index 0000000000..edb791e519 --- /dev/null +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/LegacyClassNames.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.hugegraph.util; + +import java.lang.reflect.Array; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.backend.BinaryId; +import org.apache.hugegraph.backend.Shard; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.IdUtil; +import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.query.Aggregate; +import org.apache.hugegraph.query.BatchConditionQuery; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdPrefixQuery; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.query.IdRangeQuery; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.query.serializer.AbstractSerializerAdapter; +import org.apache.hugegraph.query.serializer.QueryAdapter; +import org.apache.hugegraph.query.serializer.QueryIdAdapter; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.builder.SchemaBuilder; +import org.apache.hugegraph.type.GraphType; +import org.apache.hugegraph.type.Indexfiable; +import org.apache.hugegraph.type.Namifiable; +import org.apache.hugegraph.type.Propfiable; +import org.apache.hugegraph.type.Typifiable; + +/** Exact relocated class descriptors accepted by legacy JSON and Kryo decoders. */ +public final class LegacyClassNames { + + private static final Map> LEGACY_CLASSES = legacyClasses(); + + private LegacyClassNames() { + } + + /** Return an exact migrated type, or null so the caller retains its normal loader. */ + public static Class lookup(String name) { + Class type = LEGACY_CLASSES.get(name); + if (type != null) { + return type; + } + // Object arrays use JVM descriptors; keep dimensions and remap + // only an exact, known component name. Primitive arrays use the + // unchanged default resolver. + int dimensions = 0; + while (dimensions < name.length() && name.charAt(dimensions) == '[') { + dimensions++; + if (dimensions > 255) { + return null; + } + } + if (dimensions > 0 && dimensions < name.length() && + name.charAt(dimensions) == 'L' && name.endsWith(";")) { + type = LEGACY_CLASSES.get(name.substring(dimensions + 1, name.length() - 1)); + if (type != null) { + return Array.newInstance(type, new int[dimensions]).getClass(); + } + } + return null; + } + + private static Class declaredClass(Class owner, String name) { + for (Class type : owner.getDeclaredClasses()) { + if (type.getSimpleName().equals(name)) { + return type; + } + } + throw new IllegalStateException("Missing legacy target: " + owner.getName() + "$" + name); + } + + private static Map> legacyClasses() { + Map> classes = new HashMap<>(); + classes.put("org.apache.hugegraph.backend.id.Id", Id.class); + classes.put("org.apache.hugegraph.backend.id.IdGenerator", IdGenerator.class); + classes.put("org.apache.hugegraph.backend.id.IdUtil", IdUtil.class); + classes.put("org.apache.hugegraph.backend.id.SplicingIdGenerator", SplicingIdGenerator.class); + classes.put("org.apache.hugegraph.backend.serializer.BytesBuffer", BytesBuffer.class); + classes.put("org.apache.hugegraph.type.Typeable", Typifiable.class); + classes.put("org.apache.hugegraph.type.Nameable", Namifiable.class); + classes.put("org.apache.hugegraph.type.Propertiable", Propfiable.class); + classes.put("org.apache.hugegraph.type.Indexable", Indexfiable.class); + classes.put("org.apache.hugegraph.backend.id.Id$IdType", Id.IdType.class); + classes.put("org.apache.hugegraph.backend.id.IdGenerator$StringId", IdGenerator.StringId.class); + classes.put("org.apache.hugegraph.backend.id.IdGenerator$LongId", IdGenerator.LongId.class); + classes.put("org.apache.hugegraph.backend.id.IdGenerator$UuidId", IdGenerator.UuidId.class); + classes.put("org.apache.hugegraph.backend.id.IdGenerator$ObjectId", IdGenerator.ObjectId.class); + classes.put("org.apache.hugegraph.backend.id.EdgeId", EdgeId.class); + classes.put("org.apache.hugegraph.backend.serializer.BinaryBackendEntry$BinaryId", BinaryId.class); + classes.put("org.apache.hugegraph.backend.store.BackendEntry$BackendColumn", BackendColumn.class); + classes.put("org.apache.hugegraph.backend.store.Shard", Shard.class); + classes.put("org.apache.hugegraph.HugeException", HugeException.class); + classes.put("org.apache.hugegraph.backend.BackendException", BackendException.class); + classes.put("org.apache.hugegraph.structure.GraphType", GraphType.class); + classes.put("org.apache.hugegraph.schema.SchemaElement", SchemaElement.class); + classes.put("org.apache.hugegraph.schema.SchemaLabel", SchemaLabel.class); + classes.put("org.apache.hugegraph.schema.PropertyKey", PropertyKey.class); + classes.put("org.apache.hugegraph.schema.VertexLabel", VertexLabel.class); + classes.put("org.apache.hugegraph.schema.EdgeLabel", EdgeLabel.class); + classes.put("org.apache.hugegraph.schema.IndexLabel", IndexLabel.class); + classes.put("org.apache.hugegraph.schema.Userdata", Userdata.class); + classes.put("org.apache.hugegraph.schema.SchemaElement$TaskWithSchema", SchemaElement.TaskWithSchema.class); + classes.put("org.apache.hugegraph.schema.SchemaElement$P", SchemaElement.P.class); + classes.put("org.apache.hugegraph.schema.PropertyKey$P", declaredClass(PropertyKey.class, "P")); + classes.put("org.apache.hugegraph.schema.VertexLabel$P", declaredClass(VertexLabel.class, "P")); + classes.put("org.apache.hugegraph.schema.EdgeLabel$P", declaredClass(EdgeLabel.class, "P")); + classes.put("org.apache.hugegraph.schema.IndexLabel$P", declaredClass(IndexLabel.class, "P")); + classes.put("org.apache.hugegraph.schema.PropertyKey$Builder", PropertyKey.Builder.class); + classes.put("org.apache.hugegraph.schema.VertexLabel$Builder", VertexLabel.Builder.class); + classes.put("org.apache.hugegraph.schema.EdgeLabel$Builder", EdgeLabel.Builder.class); + classes.put("org.apache.hugegraph.schema.IndexLabel$Builder", IndexLabel.Builder.class); + classes.put("org.apache.hugegraph.structure.HugeIndex", Index.class); + classes.put("org.apache.hugegraph.structure.HugeIndex$IdWithExpiredTime", Index.IdWithExpiredTime.class); + classes.put("org.apache.hugegraph.schema.builder.SchemaBuilder", SchemaBuilder.class); + classes.put("org.apache.hugegraph.backend.query.serializer.AbstractSerializerAdapter", + AbstractSerializerAdapter.class); + classes.put("org.apache.hugegraph.backend.query.serializer.QueryAdapter", QueryAdapter.class); + classes.put("org.apache.hugegraph.backend.query.serializer.QueryIdAdapter", QueryIdAdapter.class); + classes.put("org.apache.hugegraph.backend.query.Query", Query.class); + classes.put("org.apache.hugegraph.backend.query.Query$Order", Query.Order.class); + classes.put("org.apache.hugegraph.backend.query.Query$OrderType", Query.OrderType.class); + classes.put("org.apache.hugegraph.backend.query.IdQuery", IdQuery.class); + classes.put("org.apache.hugegraph.backend.query.IdQuery$OneIdQuery", IdQuery.OneIdQuery.class); + classes.put("org.apache.hugegraph.backend.query.IdPrefixQuery", IdPrefixQuery.class); + classes.put("org.apache.hugegraph.backend.query.IdRangeQuery", IdRangeQuery.class); + classes.put("org.apache.hugegraph.backend.query.ConditionQuery", ConditionQuery.class); + classes.put("org.apache.hugegraph.backend.query.ConditionQuery$OptimizedType", + ConditionQuery.OptimizedType.class); + classes.put("org.apache.hugegraph.backend.query.ConditionQuery$Element2IndexValueMap", + ConditionQuery.Element2IndexValueMap.class); + classes.put("org.apache.hugegraph.backend.query.ConditionQuery$LeftIndex", ConditionQuery.LeftIndex.class); + classes.put("org.apache.hugegraph.backend.query.BatchConditionQuery", BatchConditionQuery.class); + classes.put("org.apache.hugegraph.backend.query.Aggregate", Aggregate.class); + classes.put("org.apache.hugegraph.backend.query.Aggregate$AggregateFunc", Aggregate.AggregateFunc.class); + classes.put("org.apache.hugegraph.backend.query.Condition", Condition.class); + classes.put("org.apache.hugegraph.backend.query.Condition$ConditionType", Condition.ConditionType.class); + classes.put("org.apache.hugegraph.backend.query.Condition$RelationType", Condition.RelationType.class); + classes.put("org.apache.hugegraph.backend.query.Condition$BinCondition", Condition.BinCondition.class); + classes.put("org.apache.hugegraph.backend.query.Condition$And", Condition.And.class); + classes.put("org.apache.hugegraph.backend.query.Condition$Or", Condition.Or.class); + classes.put("org.apache.hugegraph.backend.query.Condition$Not", Condition.Not.class); + classes.put("org.apache.hugegraph.backend.query.Condition$Relation", Condition.Relation.class); + classes.put("org.apache.hugegraph.backend.query.Condition$SyspropRelation", Condition.SyspropRelation.class); + classes.put("org.apache.hugegraph.backend.query.Condition$FlattenSyspropRelation", + Condition.FlattenSyspropRelation.class); + classes.put("org.apache.hugegraph.backend.query.Condition$UserpropRelation", Condition.UserpropRelation.class); + classes.put("org.apache.hugegraph.backend.query.Condition$RangeConditions", Condition.RangeConditions.class); + return Collections.unmodifiableMap(classes); + } + +} diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/util/StringEncoding.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/StringEncoding.java index f4690d430d..fd987a76df 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/util/StringEncoding.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/StringEncoding.java @@ -32,18 +32,16 @@ package org.apache.hugegraph.util; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.UUID; -import org.apache.hugegraph.util.Bytes; -import org.apache.hugegraph.util.E; -import org.mindrot.jbcrypt.BCrypt; - import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.serializer.BytesBuffer; +import org.mindrot.jbcrypt.BCrypt; + import com.google.common.base.CharMatcher; /** @@ -54,8 +52,8 @@ public final class StringEncoding { private static final MessageDigest DIGEST; private static final byte[] BYTES_EMPTY = new byte[0]; + private static final String STRING_EMPTY = ""; private static final int BLOCK_SIZE = 4096; - private static final int BCRYPT_WORK_FACTOR = 10; static { @@ -70,7 +68,7 @@ public final class StringEncoding { private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); - // Similar to {@link StringSerializer} + /** Similar to {@link StringSerializer} */ public static int writeAsciiString(byte[] array, int offset, String value) { E.checkArgument(CharMatcher.ascii().matchesAllOf(value), "'%s' must be ASCII string", value); @@ -86,7 +84,8 @@ public static int writeAsciiString(byte[] array, int offset, String value) { assert c <= 127; byte b = (byte) c; if (++i == len) { - b |= 0x80; // End marker + // End marker + b |= 0x80; } array[offset++] = b; } while (i < len); @@ -96,7 +95,7 @@ public static int writeAsciiString(byte[] array, int offset, String value) { public static String readAsciiString(byte[] array, int offset) { StringBuilder sb = new StringBuilder(); - int c = 0; + int c; do { c = 0xFF & array[offset++]; if (c != 0x80) { @@ -113,27 +112,21 @@ public static int getAsciiByteLength(String value) { } public static byte[] encode(String value) { - try { - return value.getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new HugeException("Failed to encode string", e); - } + return value.getBytes(StandardCharsets.UTF_8); } public static String decode(byte[] bytes) { - try { - return new String(bytes, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new HugeException("Failed to decode string", e); + if (bytes.length == 0) { + return STRING_EMPTY; } + return new String(bytes, StandardCharsets.UTF_8); } public static String decode(byte[] bytes, int offset, int length) { - try { - return new String(bytes, offset, length, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new HugeException("Failed to decode string", e); + if (length == 0) { + return STRING_EMPTY; } + return new String(bytes, offset, length, StandardCharsets.UTF_8); } public static String encodeBase64(byte[] bytes) { @@ -152,8 +145,7 @@ public static byte[] compress(String value) { } public static byte[] compress(String value, float bufferRatio) { - BytesBuffer buf = LZ4Util.compress(encode(value), BLOCK_SIZE, - bufferRatio); + BytesBuffer buf = LZ4Util.compress(encode(value), BLOCK_SIZE, bufferRatio); return buf.bytes(); } @@ -172,8 +164,7 @@ public static String hashPassword(String password) { return BCrypt.hashpw(password, BCrypt.gensalt(BCRYPT_WORK_FACTOR)); } - public static boolean checkPassword(String candidatePassword, - String dbPassword) { + public static boolean checkPassword(String candidatePassword, String dbPassword) { return BCrypt.checkpw(candidatePassword, dbPassword); } @@ -194,8 +185,7 @@ public static UUID uuid(String value) { return UUID.fromString(value); } // UUID represented by hex string - E.checkArgument(value.length() == 32, - "Invalid UUID string: %s", value); + E.checkArgument(value.length() == 32, "Invalid UUID string: %s", value); String high = value.substring(0, 16); String low = value.substring(16); return new UUID(Long.parseUnsignedLong(high, 16), diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Frequency.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/UnsafeUtil.java similarity index 52% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Frequency.java rename to hugegraph-struct/src/main/java/org/apache/hugegraph/util/UnsafeUtil.java index 3fd1c82332..bc6e492faa 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Frequency.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/UnsafeUtil.java @@ -15,35 +15,33 @@ * limitations under the License. */ -package org.apache.hugegraph.type.define; +package org.apache.hugegraph.util; -public enum Frequency implements SerialEnum { +import java.lang.reflect.Field; - DEFAULT(0, "default"), +import sun.misc.Unsafe; - SINGLE(1, "single"), - - MULTIPLE(2, "multiple"); - - private byte code = 0; - private String name = null; +/** + * Shared access to the Unsafe instance used by native memory and concurrent collections. + */ +public final class UnsafeUtil { - static { - SerialEnum.register(Frequency.class); - } + private static final Unsafe UNSAFE = loadUnsafe(); - Frequency(int code, String name) { - assert code < 256; - this.code = (byte) code; - this.name = name; + private UnsafeUtil() { } - @Override - public byte code() { - return this.code; + public static Unsafe getUnsafe() { + return UNSAFE; } - public String string() { - return this.name; + private static Unsafe loadUnsafe() { + try { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } catch (ReflectiveOperationException | RuntimeException cause) { + throw new UnsupportedOperationException("Unsafe is not supported on this platform", cause); + } } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/CollectionFactory.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/CollectionFactory.java index fb42b8416e..d345e26565 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/CollectionFactory.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/CollectionFactory.java @@ -1,20 +1,18 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.util.collection; @@ -27,6 +25,8 @@ import java.util.Map; import java.util.Set; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.type.define.CollectionType; import org.apache.hugegraph.util.E; import org.eclipse.collections.api.map.primitive.IntObjectMap; import org.eclipse.collections.api.map.primitive.MutableIntObjectMap; @@ -35,9 +35,6 @@ import org.eclipse.collections.impl.map.mutable.primitive.IntObjectHashMap; import org.eclipse.collections.impl.set.mutable.UnifiedSet; -import org.apache.hugegraph.id.Id; -import org.apache.hugegraph.type.define.CollectionType; - import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; @@ -76,7 +73,7 @@ public static List newList(CollectionType type) { return new ObjectArrayList<>(); default: throw new AssertionError( - "Unsupported collection type: " + type); + "Unsupported collection type: " + type); } } @@ -91,7 +88,7 @@ public static List newList(CollectionType type, return new ObjectArrayList<>(initialCapacity); default: throw new AssertionError( - "Unsupported collection type: " + type); + "Unsupported collection type: " + type); } } @@ -106,7 +103,7 @@ public static List newList(CollectionType type, return new ObjectArrayList<>(collection); default: throw new AssertionError( - "Unsupported collection type: " + type); + "Unsupported collection type: " + type); } } @@ -132,7 +129,7 @@ public static Set newSet(CollectionType type) { return new ObjectOpenHashSet<>(); default: throw new AssertionError( - "Unsupported collection type: " + type); + "Unsupported collection type: " + type); } } @@ -147,7 +144,7 @@ public static Set newSet(CollectionType type, return new ObjectOpenHashSet<>(initialCapacity); default: throw new AssertionError( - "Unsupported collection type: " + type); + "Unsupported collection type: " + type); } } @@ -162,7 +159,7 @@ public static Set newSet(CollectionType type, return new ObjectOpenHashSet<>(collection); default: throw new AssertionError( - "Unsupported collection type: " + type); + "Unsupported collection type: " + type); } } @@ -192,7 +189,7 @@ public static Map newMap(CollectionType type) { return new Object2ObjectOpenHashMap<>(); default: throw new AssertionError( - "Unsupported collection type: " + type); + "Unsupported collection type: " + type); } } @@ -207,7 +204,7 @@ public static Map newMap(CollectionType type, return new Object2ObjectOpenHashMap<>(initialCapacity); default: throw new AssertionError( - "Unsupported collection type: " + type); + "Unsupported collection type: " + type); } } @@ -222,7 +219,7 @@ public static Map newMap(CollectionType type, return new Object2ObjectOpenHashMap<>(map); default: throw new AssertionError( - "Unsupported collection type: " + type); + "Unsupported collection type: " + type); } } @@ -230,18 +227,19 @@ public static MutableIntObjectMap newIntObjectMap() { return new IntObjectHashMap<>(); } - public static MutableIntObjectMap newIntObjectMap(int initialCapacity) { + public static MutableIntObjectMap newIntObjectMap( + int initialCapacity) { return new IntObjectHashMap<>(initialCapacity); } public static MutableIntObjectMap newIntObjectMap( - IntObjectMap map) { + IntObjectMap map) { return new IntObjectHashMap<>(map); } @SuppressWarnings("unchecked") public static MutableIntObjectMap newIntObjectMap( - Object... objects) { + Object... objects) { IntObjectHashMap map = IntObjectHashMap.newMap(); E.checkArgument(objects.length % 2 == 0, "Must provide even arguments for " + @@ -261,4 +259,22 @@ public IdSet newIdSet() { public static IdSet newIdSet(CollectionType type) { return new IdSet(type); } + + public static IntSet newIntSet() { + /* + * Resume to the old version like this: + * return concurrent ? new IntHashSet().asSynchronized() : + * new IntHashSet(); + */ + return new IntSet.IntSetBySegments(Integer.MAX_VALUE); + } + + public static IntMap newIntMap() { + /* + * Resume to the old version like this: + * return concurrent ? new IntIntHashMap().asSynchronized() : + * new IntIntHashMap(); + */ + return new IntMap.IntMapBySegments(Integer.MAX_VALUE); + } } diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/IdSet.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/IdSet.java index d77ddfb047..7f977b252b 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/IdSet.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/IdSet.java @@ -1,36 +1,33 @@ /* - * Copyright 2017 HugeGraph Authors - * * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.apache.hugegraph.util.collection; +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Set; + import org.apache.hugegraph.id.Id; import org.apache.hugegraph.id.IdGenerator; -import org.apache.hugegraph.type.define.CollectionType; - import org.apache.hugegraph.iterator.ExtendableIterator; +import org.apache.hugegraph.type.define.CollectionType; import org.eclipse.collections.api.iterator.MutableLongIterator; import org.eclipse.collections.impl.set.mutable.primitive.LongHashSet; -import java.util.AbstractSet; -import java.util.Iterator; -import java.util.Set; - public class IdSet extends AbstractSet { private final LongHashSet numberIds; @@ -67,8 +64,8 @@ public boolean contains(Object object) { @Override public Iterator iterator() { return new ExtendableIterator<>( - this.nonNumberIds.iterator(), - new EcIdIterator(this.numberIds.longIterator())); + this.nonNumberIds.iterator(), + new EcLongIdIterator(this.numberIds.longIterator())); } @Override @@ -94,11 +91,11 @@ public void clear() { this.nonNumberIds.clear(); } - private static class EcIdIterator implements Iterator { + private static class EcLongIdIterator implements Iterator { private final MutableLongIterator iterator; - public EcIdIterator(MutableLongIterator iter) { + public EcLongIdIterator(MutableLongIterator iter) { this.iterator = iter; } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IntIterator.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/IntIterator.java similarity index 100% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IntIterator.java rename to hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/IntIterator.java diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IntMap.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/IntMap.java similarity index 100% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IntMap.java rename to hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/IntMap.java diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IntSet.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/IntSet.java similarity index 99% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IntSet.java rename to hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/IntSet.java index c40b731d8c..48cfe75f85 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/collection/IntSet.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/util/collection/IntSet.java @@ -22,11 +22,10 @@ import java.util.function.Function; import org.apache.hugegraph.util.E; +import org.apache.hugegraph.util.UnsafeUtil; import org.eclipse.collections.api.collection.primitive.MutableIntCollection; import org.eclipse.collections.impl.set.mutable.primitive.IntHashSet; -import io.netty.util.internal.shaded.org.jctools.util.UnsafeAccess; - public interface IntSet { boolean add(int key); @@ -549,7 +548,7 @@ public boolean concurrent() { } int CPUS = Runtime.getRuntime().availableProcessors(); - sun.misc.Unsafe UNSAFE = UnsafeAccess.UNSAFE; + sun.misc.Unsafe UNSAFE = UnsafeUtil.getUnsafe(); long MOD64 = 0x3fL; int DIV64 = 6; diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/SchemaDriverTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/SchemaDriverTest.java deleted file mode 100644 index e5398c2fb1..0000000000 --- a/hugegraph-struct/src/test/java/org/apache/hugegraph/SchemaDriverTest.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hugegraph; - -import java.lang.reflect.Field; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -import org.apache.hugegraph.exception.HugeException; -import org.apache.hugegraph.exception.NotAllowException; -import org.apache.hugegraph.pd.client.KvClient; -import org.apache.hugegraph.pd.client.PDConfig; -import org.apache.hugegraph.pd.common.PDException; -import org.apache.hugegraph.pd.grpc.kv.WatchResponse; -import org.junit.Assert; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -public class SchemaDriverTest { - - private AtomicReference instance; - private SchemaDriver previousInstance; - - @Before - public void setUp() throws Exception { - this.instance = instanceReference(); - this.previousInstance = this.instance.getAndSet(null); - } - - @After - public void tearDown() { - this.instance.set(this.previousInstance); - } - - @Test - public void testDestroyClosesOwnedKvClient() { - TrackingKvClient client = new TrackingKvClient(); - SchemaDriver driver = new SchemaDriver(client, 10, 60_000L); - this.instance.set(driver); - - try { - SchemaDriver.destroy(); - - Assert.assertTrue(client.closed); - Assert.assertNull(SchemaDriver.getInstance()); - } finally { - client.close(); - } - } - - @Test - public void testDestroyUnpublishesInstanceBeforeClosingResources() throws Exception { - BlockingCloseKvClient client = new BlockingCloseKvClient(); - SchemaDriver driver = new SchemaDriver(client, 10, 60_000L); - this.instance.set(driver); - Thread destroyThread = new Thread(SchemaDriver::destroy, "schema-driver-destroy"); - try { - destroyThread.start(); - client.awaitCloseStarted(); - - Assert.assertNull(SchemaDriver.getInstance()); - } finally { - client.allowClose.countDown(); - destroyThread.join(5000L); - client.close(); - } - - Assert.assertFalse(destroyThread.isAlive()); - Assert.assertTrue(client.closed); - } - - @Test - public void testInitDoesNotWaitForDestroyCleanup() throws Exception { - BlockingCloseKvClient client = new BlockingCloseKvClient(); - SchemaDriver driver = new SchemaDriver(client, 10, 60_000L); - this.instance.set(driver); - AtomicReference initFailure = new AtomicReference<>(); - CountDownLatch initFinished = new CountDownLatch(1); - Thread destroyThread = new Thread(SchemaDriver::destroy, "schema-driver-destroy"); - Thread initThread = new Thread(() -> { - try { - SchemaDriver.init(PDConfig.of("127.0.0.1:8686"), 10, 60_000L); - } catch (Throwable throwable) { - initFailure.set(throwable); - } finally { - initFinished.countDown(); - } - }, "schema-driver-init"); - boolean initCompletedDuringCleanup; - try { - destroyThread.start(); - client.awaitCloseStarted(); - initThread.start(); - initCompletedDuringCleanup = initFinished.await(1L, TimeUnit.SECONDS); - } finally { - client.allowClose.countDown(); - destroyThread.join(5000L); - initThread.join(5000L); - client.close(); - } - - Assert.assertTrue(initCompletedDuringCleanup); - Assert.assertTrue(initFailure.get() instanceof NotAllowException); - Assert.assertFalse(destroyThread.isAlive()); - Assert.assertFalse(initThread.isAlive()); - } - - @Test - public void testConcurrentDestroyWaitsForCleanup() throws Exception { - BlockingCloseKvClient client = new BlockingCloseKvClient(); - SchemaDriver driver = new SchemaDriver(client, 10, 60_000L); - this.instance.set(driver); - CountDownLatch secondDestroyStarted = new CountDownLatch(1); - CountDownLatch secondDestroyReturned = new CountDownLatch(1); - Thread firstDestroy = new Thread(SchemaDriver::destroy, "schema-driver-destroy-first"); - Thread secondDestroy = new Thread(() -> { - secondDestroyStarted.countDown(); - SchemaDriver.destroy(); - secondDestroyReturned.countDown(); - }, "schema-driver-destroy-second"); - boolean returnedBeforeCleanup; - try { - firstDestroy.start(); - client.awaitCloseStarted(); - secondDestroy.start(); - Assert.assertTrue(secondDestroyStarted.await(5L, TimeUnit.SECONDS)); - returnedBeforeCleanup = secondDestroyReturned.await(500L, TimeUnit.MILLISECONDS); - } finally { - client.allowClose.countDown(); - firstDestroy.join(5000L); - secondDestroy.join(5000L); - client.close(); - } - - Assert.assertFalse(returnedBeforeCleanup); - Assert.assertFalse(firstDestroy.isAlive()); - Assert.assertFalse(secondDestroy.isAlive()); - } - - @Test - public void testConstructorFailureClosesOwnedKvClient() { - FailingListenKvClient client = new FailingListenKvClient(); - - try { - new SchemaDriver(client, 10, 60_000L); - Assert.fail("SchemaDriver construction should fail"); - } catch (HugeException expected) { - Assert.assertEquals(2, client.listenCalls); - Assert.assertTrue(client.closed); - } finally { - client.close(); - } - } - - @SuppressWarnings("unchecked") - private static AtomicReference instanceReference() throws Exception { - Field field = SchemaDriver.class.getDeclaredField("INSTANCE"); - field.setAccessible(true); - return (AtomicReference) field.get(null); - } - - private static class TrackingKvClient extends KvClient { - - protected volatile boolean closed; - - TrackingKvClient() { - super(PDConfig.of("127.0.0.1:8686")); - } - - @Override - public void listen(String key, Consumer consumer) throws PDException { - // Avoid opening a real PD stream while constructing SchemaDriver. - } - - @Override - public void close() { - this.closed = true; - super.close(); - } - } - - private static class BlockingCloseKvClient extends TrackingKvClient { - - private final CountDownLatch closeStarted = new CountDownLatch(1); - private final CountDownLatch allowClose = new CountDownLatch(1); - - @Override - public void close() { - this.closeStarted.countDown(); - try { - this.allowClose.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Client close was interrupted", e); - } - super.close(); - } - - private void awaitCloseStarted() throws InterruptedException { - Assert.assertTrue(this.closeStarted.await(5L, TimeUnit.SECONDS)); - } - } - - private static class FailingListenKvClient extends TrackingKvClient { - - private int listenCalls; - - @Override - public void listen(String key, Consumer consumer) throws PDException { - if (++this.listenCalls == 2) { - throw new PDException(-1, "listener startup failed"); - } - } - } -} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/analyzer/AnalyzerFactoryTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/analyzer/AnalyzerFactoryTest.java new file mode 100644 index 0000000000..7f6a0de155 --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/analyzer/AnalyzerFactoryTest.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.hugegraph.analyzer; + +import org.apache.hugegraph.config.ConfigException; +import org.junit.Assert; +import org.junit.Test; + +public class AnalyzerFactoryTest { + + @Test + public void testWordAnalyzerWithProvidedDependency() { + Analyzer analyzer = AnalyzerFactory.analyzer("word", "PureEnglish"); + Assert.assertFalse(analyzer.segment("hello world").isEmpty()); + } + + @Test + public void testWordAnalyzerKeepsModeValidation() { + ConfigException exception = Assert.assertThrows(ConfigException.class, () -> { + AnalyzerFactory.analyzer("word", "invalid"); + }); + Assert.assertTrue(exception.getMessage().contains("Unsupported segment mode")); + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/backend/BackendColumnTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/backend/BackendColumnTest.java new file mode 100644 index 0000000000..a9f1a8b379 --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/backend/BackendColumnTest.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.hugegraph.backend; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +public class BackendColumnTest { + + @Test + public void testColumnsWithEqualBytesHaveEqualHashes() { + BackendColumn first = BackendColumn.of(new byte[]{1, 2}, new byte[]{3, 4}); + BackendColumn same = BackendColumn.of(new byte[]{1, 2}, new byte[]{3, 4}); + Assert.assertEquals(first, same); + Assert.assertEquals(first.hashCode(), same.hashCode()); + Set columns = new HashSet<>(); + columns.add(first); + Assert.assertTrue(columns.contains(same)); + Assert.assertFalse(columns.add(same)); + Assert.assertNotEquals(first, BackendColumn.of(new byte[]{1, 2}, new byte[]{5})); + } + + @Test + public void testSharedColumnRetainsMutableByteArraysAndNameOrdering() { + byte[] name = new byte[]{1}; + byte[] value = new byte[]{2}; + BackendColumn column = BackendColumn.of(name, value); + Assert.assertSame(name, column.name); + Assert.assertSame(value, column.value); + Assert.assertTrue(column.compareTo(BackendColumn.of(new byte[]{2}, value)) < 0); + Assert.assertEquals(0, column.compareTo(BackendColumn.of(new byte[]{1}, new byte[]{3}))); + Assert.assertTrue(column.compareTo(null) > 0); + name[0] = 3; + Assert.assertEquals(3, column.name[0]); + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/compatibility/LegacyCoreCodecTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/compatibility/LegacyCoreCodecTest.java new file mode 100644 index 0000000000..0e1f4ad525 --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/compatibility/LegacyCoreCodecTest.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.hugegraph.compatibility; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.BufferUnderflowException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.Properties; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.UUID; + +import org.apache.hugegraph.backend.BinaryId; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Aggregate.AggregateFunc; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.serializer.BytesBuffer.IndexColumnName; +import org.apache.hugegraph.serializer.BytesBuffer.IndexExpiryLayout; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.hugegraph.util.Blob; +import org.apache.hugegraph.util.Bytes; +import org.junit.Assert; +import org.junit.Test; + +import com.google.gson.JsonParser; + +/** + * Golden fixtures were written by the pre-consolidation core, not this module. + * See compatibility/2f827d6e8/README.md for provenance and coverage limits. + */ +public class LegacyCoreCodecTest { + + private static final String FIXTURES = "/compatibility/2f827d6e8/"; + + @Test + public void testLegacyLongIdsAndVarLongs() throws IOException { + long[] values = {Long.MIN_VALUE, -16385L, -16384L, -129L, -128L, -1L, + 0L, 1L, 127L, 128L, 16383L, 16384L, Long.MAX_VALUE}; + for (long value : values) { + byte[] idBytes = fixture("id.long." + value); + BytesBuffer reader = BytesBuffer.wrap(idBytes); + Id actual = reader.readId(); + Assert.assertEquals(IdGenerator.of(value), actual); + Assert.assertEquals(0, reader.remaining()); + Assert.assertArrayEquals(idBytes, + BytesBuffer.allocate(64).writeId(actual).bytes()); + + byte[] varLongBytes = fixture("vlong." + value); + reader = BytesBuffer.wrap(varLongBytes); + Assert.assertEquals(value, reader.readVLong()); + Assert.assertEquals(0, reader.remaining()); + Assert.assertArrayEquals(varLongBytes, + BytesBuffer.allocate(64).writeVLong(value).bytes()); + } + } + + @Test + public void testLegacyVarInts() throws IOException { + int[] values = {Integer.MIN_VALUE, -129, -1, 0, 127, 128, 16383, 16384, + Integer.MAX_VALUE}; + for (int value : values) { + byte[] expected = fixture("vint." + value); + BytesBuffer reader = BytesBuffer.wrap(expected); + Assert.assertEquals(value, reader.readVInt()); + Assert.assertEquals(0, reader.remaining()); + Assert.assertArrayEquals(expected, + BytesBuffer.allocate(64).writeVInt(value).bytes()); + } + } + + @Test + public void testLegacyTextAndUuidIds() throws IOException { + assertId("id.text", IdGenerator.of("alice")); + assertId("id.unicode", IdGenerator.of("顶点😀")); + assertId("id.uuid", IdGenerator.of( + UUID.fromString("01234567-89ab-cdef-0123-456789abcdef"))); + } + + @Test + public void testLegacyTextIdWidthBoundaries() throws IOException { + for (int length : new int[]{1, 64, 65, 256, 257, 16384}) { + String key = "id.text.length." + length; + Id expected = IdGenerator.of("x".repeat(length)); + assertId(key, expected); + // The historical struct boolean overload used the same layout. + byte[] legacy = fixture(key); + BytesBuffer reader = BytesBuffer.wrap(legacy); + Assert.assertEquals(expected, reader.readId(true)); + Assert.assertEquals(0, reader.remaining()); + Assert.assertArrayEquals(legacy, + BytesBuffer.allocate(64).writeId(expected, true).bytes()); + } + Assert.assertThrows(IllegalArgumentException.class, + () -> BytesBuffer.allocate(64).writeId(IdGenerator.of(""))); + Assert.assertThrows(IllegalArgumentException.class, + () -> BytesBuffer.allocate(64).writeId(IdGenerator.of("x".repeat(16385)))); + } + + @Test + public void testLegacyEdgeIds() throws IOException { + assertEdge("edge.out", new EdgeId(IdGenerator.of("alice"), Directions.OUT, + IdGenerator.of(3L), IdGenerator.of(4L), "2026!x", IdGenerator.of(99L))); + assertEdge("edge.in", new EdgeId(IdGenerator.of(99L), Directions.IN, + IdGenerator.of(3L), IdGenerator.of(4L), "2026!x", IdGenerator.of("alice"))); + } + + @Test + public void testLegacyEdgeKeyPrefixAndSortSkipping() throws IOException { + byte[] legacy = fixture("edge.out"); + Id owner = IdGenerator.of("alice"); + byte[] withoutOwner = Arrays.copyOfRange(legacy, fixture("id.text").length, legacy.length); + BytesBuffer prefixless = BytesBuffer.wrap(withoutOwner); + EdgeId edge = prefixless.readEdgeId(false, owner); + Assert.assertEquals(owner, edge.ownerVertexId()); + Assert.assertEquals(Directions.OUT, edge.direction()); + Assert.assertEquals(IdGenerator.of(3L), edge.edgeLabelId()); + Assert.assertEquals(IdGenerator.of(4L), edge.subLabelId()); + Assert.assertEquals("2026!x", edge.sortValues()); + Assert.assertEquals(IdGenerator.of(99L), edge.otherVertexId()); + Assert.assertEquals(0, prefixless.remaining()); + BytesBuffer skipped = BytesBuffer.wrap(legacy); + EdgeId withoutSort = (EdgeId) skipped.readEdgeIdSkipSortValues(); + Assert.assertEquals(owner, withoutSort.ownerVertexId()); + Assert.assertEquals("", withoutSort.sortValues()); + Assert.assertEquals(IdGenerator.of(99L), withoutSort.otherVertexId()); + Assert.assertEquals(0, skipped.remaining()); + } + + @Test + public void testLegacyIndexKeyLayoutBoundaries() throws IOException { + Properties rows = new Properties(); + try (InputStream input = this.getClass().getResourceAsStream(FIXTURES + "core-rows.properties")) { + Assert.assertNotNull(input); + rows.load(input); + } + byte[] timed = Bytes.fromHex(rows.getProperty("index.secondary.column.0.name")); + byte[] indexPrefix = Bytes.fromHex(rows.getProperty("index.secondary.id")); + byte[] prefixless = Arrays.copyOfRange(timed, indexPrefix.length, timed.length); + BytesBuffer withPrefix = BytesBuffer.wrap(timed); + IndexColumnName parsed = withPrefix.readIndexColumnName(HugeType.SECONDARY_INDEX, true, + IndexExpiryLayout.REQUIRED); + Assert.assertArrayEquals(Arrays.copyOf(indexPrefix, indexPrefix.length - 1), parsed.indexId().asBytes()); + Assert.assertEquals(IdGenerator.of(1L), parsed.elementId()); + Assert.assertEquals(1609459260000L, parsed.expiredTime()); + Assert.assertTrue(parsed.hasExpiredTime()); + Assert.assertEquals(0, withPrefix.remaining()); + BytesBuffer noPrefix = BytesBuffer.wrap(prefixless); + parsed = noPrefix.readIndexColumnName(HugeType.SECONDARY_INDEX, false, IndexExpiryLayout.REQUIRED); + Assert.assertNull(parsed.indexId()); + Assert.assertEquals(IdGenerator.of(1L), parsed.elementId()); + Assert.assertEquals(1609459260000L, parsed.expiredTime()); + Assert.assertEquals(0, noPrefix.remaining()); + byte[] bare = Bytes.fromHex(rows.getProperty("index.system.column.0.name")); + parsed = BytesBuffer.wrap(bare).readIndexColumnName(HugeType.VERTEX_LABEL_INDEX, true, + IndexExpiryLayout.OPTIONAL); + Assert.assertEquals(0L, parsed.expiredTime()); + Assert.assertFalse(parsed.hasExpiredTime()); + byte[] withZero = Arrays.copyOf(bare, bare.length + 1); + withZero[bare.length] = fixture("vlong.0")[0]; + parsed = BytesBuffer.wrap(withZero).readIndexColumnName(HugeType.VERTEX_LABEL_INDEX, true, + IndexExpiryLayout.OPTIONAL); + Assert.assertEquals(0L, parsed.expiredTime()); + Assert.assertTrue(parsed.hasExpiredTime()); + Assert.assertThrows(BufferUnderflowException.class, + () -> BytesBuffer.wrap(bare).readIndexColumnName( + HugeType.VERTEX_LABEL_INDEX, true, IndexExpiryLayout.REQUIRED)); + } + + @Test + public void testLegacyScalarProperties() throws IOException { + assertProperty(DataType.BOOLEAN, true); + assertProperty(DataType.BYTE, (byte) -7); + assertProperty(DataType.INT, -123456); + assertProperty(DataType.LONG, Long.MIN_VALUE); + assertProperty(DataType.FLOAT, 1.25f); + assertProperty(DataType.DOUBLE, -123.125d); + assertProperty(DataType.DATE, new Date(1609459200123L)); + assertProperty(DataType.TEXT, "属性😀\u0000"); + assertProperty(DataType.BLOB, Blob.wrap(new byte[]{0, -1, 1, 127, -128})); + assertProperty(DataType.UUID, UUID.fromString("01234567-89ab-cdef-0123-456789abcdef")); + } + + @Test + public void testLegacyKryoProperties() throws IOException { + assertProperty(DataType.OBJECT, new ArrayList<>(Arrays.asList("legacy", 7, true))); + assertProperty(DataType.UNKNOWN, "legacy-object"); + } + + @Test + public void testLegacySchemaDrivenProperties() throws IOException { + assertSchemaProperty("single", Cardinality.SINGLE, 7); + assertSchemaProperty("list", Cardinality.LIST, Arrays.asList(7, -1, 7)); + assertSchemaProperty("set", Cardinality.SET, + new LinkedHashSet<>(Arrays.asList(7, -1))); + } + + @Test + public void testLegacyIndexIds() throws IOException { + assertIndex("index.string", HugeType.SECONDARY_INDEX, + "label!alice".getBytes(StandardCharsets.UTF_8)); + assertIndex("index.int", HugeType.RANGE_INT_INDEX, + new byte[]{(byte) 160, 0, 0, 0, 3, (byte) 128, 0, 0, 7}); + assertIndex("index.long", HugeType.RANGE_LONG_INDEX, + new byte[]{(byte) 162, 0, 0, 0, 3, (byte) 128, 0, 0, 0, 0, 0, 0, 7}); + } + + @Test + public void testLegacyTaggedScalarProperties() throws IOException { + assertTaggedScalar(DataType.BOOLEAN, true); + assertTaggedScalar(DataType.BYTE, (byte) -7); + assertTaggedScalar(DataType.INT, -123456); + assertTaggedScalar(DataType.LONG, Long.MIN_VALUE); + assertTaggedScalar(DataType.FLOAT, 1.25f); + assertTaggedScalar(DataType.DOUBLE, -123.125d); + assertTaggedScalar(DataType.DATE, new Date(1609459200123L)); + assertTaggedScalar(DataType.TEXT, "属性😀\u0000"); + assertTaggedScalar(DataType.BLOB, Blob.wrap(new byte[]{0, -1, 1, 127, -128})); + assertTaggedScalar(DataType.UUID, + UUID.fromString("01234567-89ab-cdef-0123-456789abcdef")); + } + + @Test + public void testLegacyTaggedCollectionProperties() throws IOException { + assertTaggedProperty("list", DataType.INT, Cardinality.LIST, + Arrays.asList(7, -1, 7), "schema.list"); + assertTaggedProperty("set", DataType.INT, Cardinality.SET, + new LinkedHashSet<>(Arrays.asList(7, -1)), "schema.set"); + } + + @Test + public void testSkippingLegacyPropertiesPreservesTrailingTtl() throws IOException { + for (DataType type : DataType.values()) { + this.assertSkipProperty("property." + type.name(), type, Cardinality.SINGLE); + byte[] legacy = type == DataType.UNKNOWN || type == DataType.OBJECT ? null : + fixture("struct-tagged-properties.hex", type.name()); + if (legacy != null) { + BytesBuffer buffer = withTrailingMarker(legacy); + buffer.skipProperty(); + assertTrailingMarker(buffer); + } + } + this.assertSkipProperty("schema.list", DataType.INT, Cardinality.LIST); + this.assertSkipProperty("schema.set", DataType.INT, Cardinality.SET); + // An existing core Kryo limitation must not prevent skipping data: + // this collection's baseline writer can encode it, but its reader + // cannot instantiate Arrays$ArrayList. + this.assertSkipProperty("property.OBJECT_UNSUPPORTED_COLLECTION", + DataType.OBJECT, Cardinality.SINGLE); + } + + private void assertSkipProperty(String name, DataType type, Cardinality cardinality) + throws IOException { + PropertyKey key = new PropertyKey(null, IdGenerator.of(101L), "legacy"); + key.dataType(type); + key.cardinality(cardinality); + BytesBuffer buffer = withTrailingMarker(fixture(name)); + buffer.skipSchemaProperty(key); + assertTrailingMarker(buffer); + } + + private static BytesBuffer withTrailingMarker(byte[] bytes) { + BytesBuffer buffer = BytesBuffer.allocate(bytes.length + 16); + buffer.write(bytes); + buffer.writeUInt8(0x55); + buffer.writeVLong(1609459260000L); + return buffer.forReadWritten(); + } + + private static void assertTrailingMarker(BytesBuffer buffer) { + Assert.assertEquals(0x55, buffer.readUInt8()); + Assert.assertEquals(1609459260000L, buffer.readVLong()); + Assert.assertEquals(0, buffer.remaining()); + } + + @Test + public void testLegacyVertexQuery() throws IOException, ParseException { + // Legacy JSON date strings represent wall time in the process time + // zone. Do not mutate it after Gson's date adapter initialization. + ConditionQuery query = ConditionQuery.fromBytes(jsonFixture("core-query.json.txt")); + Assert.assertEquals(HugeType.VERTEX, query.resultType()); + Assert.assertEquals(7L, query.offset()); + Assert.assertEquals(19L, query.limit()); + Assert.assertEquals(IdGenerator.of(3L), query.condition(HugeKeys.LABEL)); + Condition.Relation range = query.relation(IdGenerator.of(11L)); + Assert.assertEquals(Condition.RelationType.GTE, range.relation()); + Assert.assertEquals(18, range.value()); + Assert.assertNull(query.condition(IdGenerator.of(11L))); + Assert.assertEquals(Arrays.asList(20L, 30L), query.condition(IdGenerator.of(12L))); + Date expectedDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.ROOT) + .parse("2021-01-01 00:00:00.123"); + Assert.assertEquals(expectedDate, query.condition(IdGenerator.of(13L))); + Assert.assertEquals(5, query.conditions().size()); + Condition disjunction = query.conditions().stream() + .filter(c -> c instanceof Condition.Or) + .findFirst().orElseThrow(AssertionError::new); + Assert.assertTrue(disjunction.test("alice")); + Assert.assertTrue(disjunction.test("bob")); + Assert.assertFalse(disjunction.test("charlie")); + } + + @Test + public void testLegacyEdgeQuery() throws IOException { + ConditionQuery query = ConditionQuery.fromBytes(jsonFixture("core-edge-query.json.txt")); + Assert.assertEquals(HugeType.EDGE, query.resultType()); + Assert.assertEquals(IdGenerator.of("alice"), query.condition(HugeKeys.OWNER_VERTEX)); + Assert.assertEquals(Directions.OUT, query.condition(HugeKeys.DIRECTION)); + Assert.assertEquals(IdGenerator.of(3L), query.condition(HugeKeys.LABEL)); + Assert.assertEquals("AQIDBA==", query.page()); + Assert.assertEquals(5L, query.limit()); + Assert.assertEquals(3, query.conditions().size()); + } + + @Test + public void testLegacyCountAggregateQuery() throws IOException { + byte[] payload = jsonFixture("core-count-query.json.txt"); + ConditionQuery query = ConditionQuery.fromBytes(payload); + Assert.assertEquals(AggregateFunc.COUNT, query.aggregateNotNull().func()); + Assert.assertTrue(query.aggregateNotNull().countAll()); + Assert.assertNull(query.aggregateNotNull().column()); + Assert.assertEquals(5L, query.aggregateNotNull() + .reduce(Arrays.asList(2L, 3L).iterator()).longValue()); + assertAggregateJson(payload, query.bytes()); + } + + @Test + public void testLegacyAvgAggregateQuery() throws IOException { + byte[] payload = jsonFixture("core-avg-query.json.txt"); + ConditionQuery query = ConditionQuery.fromBytes(payload); + Assert.assertEquals(AggregateFunc.AVG, query.aggregateNotNull().func()); + Assert.assertFalse(query.aggregateNotNull().countAll()); + Assert.assertEquals("age", query.aggregateNotNull().column()); + Assert.assertEquals(25d, query.aggregateNotNull() + .reduce(Arrays.asList(20, 30).iterator()).doubleValue(), + 0d); + Assert.assertEquals(IdGenerator.of(3L), query.condition(HugeKeys.LABEL)); + assertAggregateJson(payload, query.bytes()); + } + + private static void assertAggregateJson(byte[] legacy, byte[] current) { + Assert.assertEquals(JsonParser.parseString(new String(legacy, StandardCharsets.UTF_8)) + .getAsJsonObject().get("aggregate"), + JsonParser.parseString(new String(current, StandardCharsets.UTF_8)) + .getAsJsonObject().get("aggregate")); + } + + private static void assertId(String key, Id expected) throws IOException { + byte[] legacy = fixture(key); + BytesBuffer reader = BytesBuffer.wrap(legacy); + Assert.assertEquals(expected, reader.readId()); + Assert.assertEquals(0, reader.remaining()); + Assert.assertArrayEquals(legacy, BytesBuffer.allocate(64).writeId(expected).bytes()); + } + + private static void assertEdge(String key, EdgeId expected) throws IOException { + byte[] legacy = fixture(key); + BytesBuffer reader = BytesBuffer.wrap(legacy); + EdgeId actual = (EdgeId) reader.readEdgeId(); + Assert.assertEquals(expected.ownerVertexId(), actual.ownerVertexId()); + Assert.assertEquals(expected.direction(), actual.direction()); + Assert.assertEquals(expected.edgeLabelId(), actual.edgeLabelId()); + Assert.assertEquals(expected.subLabelId(), actual.subLabelId()); + Assert.assertEquals(expected.sortValues(), actual.sortValues()); + Assert.assertEquals(expected.otherVertexId(), actual.otherVertexId()); + Assert.assertEquals(0, reader.remaining()); + Assert.assertArrayEquals(legacy, BytesBuffer.allocate(128).writeEdgeId(expected).bytes()); + } + + private static void assertProperty(DataType type, Object expected) throws IOException { + byte[] legacy = fixture("property." + type.name()); + BytesBuffer reader = BytesBuffer.wrap(legacy); + Assert.assertEquals(expected, reader.readProperty(type)); + Assert.assertEquals(0, reader.remaining()); + BytesBuffer writer = BytesBuffer.allocate(128); + writer.writeProperty(type, expected); + Assert.assertArrayEquals(legacy, writer.bytes()); + } + + private static void assertSchemaProperty(String name, Cardinality cardinality, + Object expected) throws IOException { + PropertyKey key = new PropertyKey(null, IdGenerator.of(101L), "legacy"); + key.dataType(DataType.INT); + key.cardinality(cardinality); + byte[] legacy = fixture("schema." + name); + BytesBuffer reader = BytesBuffer.wrap(legacy); + Assert.assertEquals(expected, reader.readSchemaProperty(key)); + Assert.assertEquals(0, reader.remaining()); + Assert.assertEquals(cardinality, key.cardinality()); + Assert.assertEquals(DataType.INT, key.dataType()); + Assert.assertArrayEquals(legacy, + BytesBuffer.allocate(64).writeSchemaProperty(key, expected).bytes()); + } + + private static void assertIndex(String name, HugeType type, byte[] expected) + throws IOException { + byte[] legacy = fixture(name); + BytesBuffer reader = BytesBuffer.wrap(legacy); + Assert.assertArrayEquals(expected, reader.readIndexId(type).asBytes()); + Assert.assertEquals(0, reader.remaining()); + Assert.assertArrayEquals(legacy, BytesBuffer.allocate(64) + .writeIndexId(new BinaryId(expected, null), type) + .bytes()); + } + + private static void assertTaggedScalar(DataType type, Object expected) throws IOException { + assertTaggedProperty(type.name(), type, Cardinality.SINGLE, + expected, "property." + type.name()); + } + + private static void assertTaggedProperty(String name, DataType type, + Cardinality cardinality, Object expected, + String rawKey) throws IOException { + byte[] legacy = fixture("struct-tagged-properties.hex", name); + byte[] raw = fixture(rawKey); + Assert.assertEquals(raw.length + 1, legacy.length); + Assert.assertArrayEquals(raw, Arrays.copyOfRange(legacy, 1, legacy.length)); + PropertyKey key = new PropertyKey(null, IdGenerator.of(101L), "legacy"); + // Tagged readers must recover metadata, even when the caller starts + // with metadata different from the encoded property. + key.dataType(DataType.UNKNOWN); + key.cardinality(Cardinality.SET); + BytesBuffer reader = BytesBuffer.wrap(legacy); + Assert.assertEquals(expected, reader.readProperty(key)); + Assert.assertEquals(0, reader.remaining()); + Assert.assertEquals(type, key.dataType()); + Assert.assertEquals(cardinality, key.cardinality()); + Assert.assertArrayEquals(legacy, + BytesBuffer.allocate(128).writeProperty(key, expected).bytes()); + } + + private static byte[] fixture(String key) throws IOException { + return fixture("core-codecs.hex", key); + } + + private static byte[] fixture(String file, String key) throws IOException { + Properties properties = new Properties(); + try (InputStream input = LegacyCoreCodecTest.class.getResourceAsStream( + FIXTURES + file)) { + Assert.assertNotNull("Missing codec fixture resource", input); + properties.load(input); + } + String hex = properties.getProperty(key); + Assert.assertNotNull("Missing legacy fixture " + key, hex); + return Bytes.fromHex(hex); + } + + private static byte[] jsonFixture(String name) throws IOException { + try (InputStream input = LegacyCoreCodecTest.class.getResourceAsStream(FIXTURES + name)) { + Assert.assertNotNull("Missing legacy fixture " + name, input); + String text = new String(input.readAllBytes(), StandardCharsets.UTF_8); + Assert.assertTrue("Missing fixture license prolog", text.startsWith("/*")); + int payloadStart = text.indexOf("*/\n") + 3; + Assert.assertTrue("Missing fixture payload", payloadStart > 3); + Assert.assertTrue("Missing fixture file newline", text.endsWith("\n")); + return text.substring(payloadStart, text.length() - 1).getBytes(StandardCharsets.UTF_8); + } + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/compatibility/LegacyCoreRowCodecTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/compatibility/LegacyCoreRowCodecTest.java new file mode 100644 index 0000000000..0e48381070 --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/compatibility/LegacyCoreRowCodecTest.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.hugegraph.compatibility; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.hugegraph.HugeGraphSupplier; +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.serializer.BinaryElementSerializer; +import org.apache.hugegraph.serializer.DirectBinarySerializer; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.BaseEdge; +import org.apache.hugegraph.structure.BaseElement; +import org.apache.hugegraph.structure.BaseVertex; +import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.Frequency; +import org.apache.hugegraph.type.define.IndexType; +import org.apache.hugegraph.type.define.WriteType; +import org.apache.hugegraph.util.Bytes; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** Test complete rows written by the pinned pre-consolidation BinarySerializer. */ +public class LegacyCoreRowCodecTest { + + private final BinaryElementSerializer serializer = new BinaryElementSerializer(); + private final DirectBinarySerializer direct = new DirectBinarySerializer(); + private final Map properties = new HashMap<>(); + private final Map vertices = new HashMap<>(); + private final Map edges = new HashMap<>(); + private final Map indexes = new HashMap<>(); + private Properties rows; + private HugeGraphSupplier graph; + private long expiry; + + @Before + public void prepare() throws IOException { + this.rows = new Properties(); + try (InputStream input = this.getClass().getResourceAsStream( + "/compatibility/2f827d6e8/core-rows.properties")) { + Assert.assertNotNull(input); + this.rows.load(input); + } + this.expiry = Long.parseLong(this.rows.getProperty("expiry")); + this.graph = (HugeGraphSupplier) Proxy.newProxyInstance( + HugeGraphSupplier.class.getClassLoader(), new Class[]{HugeGraphSupplier.class}, + (proxy, method, args) -> { + switch (method.getName()) { + case "now": return Long.parseLong(this.rows.getProperty("now")); + case "name": + case "spaceGraphName": return "legacy-fixture"; + case "propertyKey": return this.properties.get(((Id) args[0]).asLong()); + case "vertexLabel": + case "vertexLabelOrNone": return this.vertices.get(((Id) args[0]).asLong()); + case "edgeLabel": + case "edgeLabelOrNone": return this.edges.get(((Id) args[0]).asLong()); + case "indexLabel": return this.indexes.get(((Id) args[0]).asLong()); + default: throw new AssertionError("Unexpected supplier call: " + method); + } + }); + this.property(1L, "age", DataType.INT, Cardinality.SINGLE); + this.property(2L, "scores", DataType.INT, Cardinality.LIST); + this.property(3L, "groups", DataType.INT, Cardinality.SET); + this.property(9L, "rank", DataType.INT, Cardinality.SINGLE) + .writeType(WriteType.OLAP_COMMON); + this.property(16L, "weight", DataType.LONG, Cardinality.SINGLE); + this.property(17L, "name", DataType.TEXT, Cardinality.SINGLE); + this.vertexLabel(4L, "person", 60000L); + this.vertexLabel(10L, "timeless", 0L); + this.edgeLabel(5L, "knows", 60000L); + this.edgeLabel(11L, "untimed", 0L); + this.indexLabel(6L, IndexType.SECONDARY, 4L); + this.indexLabel(7L, IndexType.RANGE_INT, 4L); + this.indexLabel(8L, IndexType.RANGE_LONG, 4L); + this.indexLabel(12L, IndexType.SECONDARY, 10L); + this.indexLabel(13L, IndexType.RANGE_INT, 10L); + this.indexLabel(14L, IndexType.RANGE_LONG, 10L); + this.indexLabel(15L, IndexType.SECONDARY, 4L); + } + + @Test + public void testLegacyVertexRowsAndDirectTtl() { + for (String name : new String[]{"vertex", "vertex.untimed"}) { + BackendColumn column = this.column(name); + BaseVertex vertex = this.serializer.parseSchemaVertex(this.graph, column, null); + this.assertProperties(vertex); + Assert.assertEquals(name.endsWith("untimed") ? 3L : 1L, vertex.id().asLong()); + long expectedExpiry = name.endsWith("untimed") ? 0L : this.expiry; + Assert.assertEquals(expectedExpiry, vertex.expiredTime()); + Assert.assertArrayEquals(column.value, this.serializer.formatSchemaVertexValue(vertex)); + Assert.assertEquals(expectedExpiry, this.direct.parseSchemaVertex( + this.graph, column.name, column.value).expiredTime()); + } + } + + @Test + public void testLegacyEdgeRowsAndDirectTtl() { + for (String name : new String[]{"edge.out", "edge.in", "edge.untimed"}) { + BackendColumn column = this.column(name); + BaseEdge edge = this.serializer.parseSchemaEdge(this.graph, column, null, true); + this.assertProperties(edge); + Assert.assertEquals(!name.equals("edge.in"), edge.isOutEdge()); + Assert.assertEquals(name.equals("edge.in") ? 2L : 1L, edge.ownerVertexId().asLong()); + long expectedExpiry = name.endsWith("untimed") ? 0L : this.expiry; + Assert.assertEquals(expectedExpiry, edge.expiredTime()); + Assert.assertArrayEquals(column.name, this.serializer.formatEdgeName(edge)); + Assert.assertArrayEquals(column.value, this.serializer.formatSchemaEdgeValue(edge)); + Assert.assertEquals(expectedExpiry, this.direct.parseSchemaEdge( + this.graph, column.name, column.value).expiredTime()); + } + } + + @Test + public void testLegacyVertexWithOlapMergedRow() { + BaseVertex vertex = this.serializer.parseSchemaVertexFromCols( + this.graph, this.column("vertex"), this.column("olap")); + Assert.assertEquals(1L, vertex.id().asLong()); + Assert.assertEquals(this.expiry, vertex.expiredTime()); + this.assertProperties(vertex); + Assert.assertEquals(42, (int) vertex.getPropertyValue(IdGenerator.of(9L))); + } + + @Test + public void testLegacyStandaloneOlapRow() { + BaseVertex vertex = this.serializer.parseSchemaVertexOlap( + this.graph, this.column("olap"), null); + Assert.assertEquals(1L, vertex.id().asLong()); + Assert.assertEquals(42, (int) vertex.getPropertyValue(IdGenerator.of(9L))); + Assert.assertEquals(0L, vertex.expiredTime()); + } + + @Test + public void testSharedIndexWriterRetainsLegacyCoreBytes() { + for (String name : new String[]{"index.secondary", "index.int", "index.long", + "index.secondary.untimed", "index.int.untimed", + "index.long.untimed", "index.hashed", "index.system", "index.system.edge"}) { + BackendColumn legacy = this.column(name); + Index decoded = this.serializer.parseSchemaIndex(this.graph, legacy); + BackendColumn current = this.serializer.writeSchemaIndex(decoded); + Assert.assertArrayEquals(name + " key", legacy.name, current.name); + Assert.assertNotNull(name + " persistent value", current.value); + Assert.assertArrayEquals(name + " value", legacy.value, current.value); + } + } + + @Test + public void testOriginalStoreIndexTtlRows() throws IOException { + Properties legacy = new Properties(); + try (InputStream input = this.getClass().getResourceAsStream( + "/compatibility/2f827d6e8/store-index-rows.properties")) { + Assert.assertNotNull(input); + legacy.load(input); + } + for (String name : new String[]{"secondary", "int", "long", "system", "system.edge"}) { + BackendColumn column = BackendColumn.of(Bytes.fromHex(legacy.getProperty(name + ".name")), + Bytes.fromHex(legacy.getProperty(name + ".value"))); + Index decoded = this.serializer.parseSchemaIndex(this.graph, column); + Id expectedElement = name.equals("system.edge") ? edgeElementId() : IdGenerator.of(1L); + Assert.assertEquals(expectedElement, decoded.elementId()); + Assert.assertEquals(this.expiry, decoded.expiredTime()); + Assert.assertEquals(this.expiry, this.direct.parseSchemaIndex( + this.graph, column.name, column.value).expiredTime()); + // The still-supported tagged writer must also retain old Store bytes. + BackendColumn tagged = this.serializer.writeIndex(decoded); + Assert.assertArrayEquals(column.name, tagged.name); + Assert.assertArrayEquals(column.value, tagged.value); + if (name.startsWith("system")) { + BackendColumn current = this.serializer.writeSchemaIndex(decoded); + Assert.assertArrayEquals(column.name, current.name); + Assert.assertArrayEquals(column.value, current.value); + Assert.assertFalse(decoded.hasTtl()); + } + } + } + + @Test + public void testPositiveLabelExpiryUsesStableStorageKeyAndCoreFlag() throws IOException { + Properties storage = new Properties(); + try (InputStream input = this.getClass().getResourceAsStream( + "/compatibility/2f827d6e8/store-index-rows.properties")) { + Assert.assertNotNull(input); + storage.load(input); + } + for (String name : new String[]{"system", "system.edge"}) { + HugeType base = name.endsWith("edge") ? HugeType.EDGE : HugeType.VERTEX; + Id elementId = name.endsWith("edge") ? edgeElementId() : IdGenerator.of(1L); + Index index = new Index(this.graph, IndexLabel.label(base), true); + index.fieldValues(name.endsWith("edge") ? 5L : 4L); + index.elementIds(elementId, this.expiry); + Assert.assertFalse(index.hasTtl()); + BackendColumn oldCore = this.column("index." + name); + BackendColumn core = this.serializer.formatSchemaIndex(index.type(), index.indexLabelId(), + index.fieldValues(), elementId, true, false, this.expiry); + Assert.assertArrayEquals(oldCore.name, core.name); + Assert.assertNotNull(core.value); + Assert.assertArrayEquals(oldCore.value, core.value); + BackendColumn stored = this.serializer.writeSchemaIndex(index); + Assert.assertArrayEquals(Bytes.fromHex(storage.getProperty(name + ".name")), stored.name); + Assert.assertArrayEquals(Bytes.fromHex(storage.getProperty(name + ".value")), stored.value); + Assert.assertArrayEquals("Storage append retains Core elimination key", core.name, stored.name); + Assert.assertEquals(this.expiry, this.direct.parseSchemaIndex( + this.graph, stored.name, stored.value).expiredTime()); + Assert.assertEquals(Long.valueOf(this.expiry), this.serializer.storedLabelIndexExpiredTime( + index.type(), true, stored.value)); + Assert.assertNull(this.serializer.storedLabelIndexExpiredTime(index.type(), false, stored.value)); + Assert.assertNull(this.serializer.storedLabelIndexExpiredTime( + HugeType.SECONDARY_INDEX, true, stored.value)); + Assert.assertNull(this.serializer.storedLabelIndexExpiredTime( + index.type(), true, Arrays.copyOf(stored.value, stored.value.length + 1))); + } + } + + private static Id edgeElementId() { + return new EdgeId(IdGenerator.of(1L), Directions.OUT, IdGenerator.of(5L), + IdGenerator.of(5L), "2026!x", IdGenerator.of(2L)); + } + + @Test + public void testLegacyIndexRowsAndDirectTtl() { + for (String name : new String[]{"index.secondary", "index.int", "index.long", + "index.secondary.untimed", "index.int.untimed", + "index.long.untimed", "index.hashed", "index.system", "index.system.edge"}) { + BackendColumn column = this.column(name); + Index index = this.serializer.parseSchemaIndex(this.graph, column); + Assert.assertEquals(name.equals("index.system.edge") ? edgeElementId() : IdGenerator.of(1L), + index.elementId()); + long expectedExpiry = name.endsWith("untimed") || name.startsWith("index.system") ? + 0L : this.expiry; + Assert.assertEquals(expectedExpiry, index.expiredTime()); + Assert.assertEquals(expectedExpiry, this.direct.parseSchemaIndex( + this.graph, column.name, column.value).expiredTime()); + if (name.equals("index.hashed")) { + Assert.assertEquals("long-field-".repeat(20), index.fieldValues()); + } else if (name.startsWith("index.secondary")) { + Assert.assertEquals("alice", index.fieldValues()); + } else if (name.startsWith("index.int")) { + Assert.assertEquals(7, index.fieldValues()); + } else if (name.startsWith("index.long")) { + Assert.assertEquals(7L, index.fieldValues()); + } + } + } + + private void assertProperties(BaseElement element) { + Assert.assertEquals(7, (int) element.getPropertyValue(IdGenerator.of(1L))); + Assert.assertEquals(Arrays.asList(7, -1, 7), element.getPropertyValue(IdGenerator.of(2L))); + Assert.assertEquals(new HashSet<>(Arrays.asList(7, -1)), + element.getPropertyValue(IdGenerator.of(3L))); + } + + private BackendColumn column(String name) { + return BackendColumn.of(Bytes.fromHex(this.rows.getProperty(name + ".column.0.name")), + Bytes.fromHex(this.rows.getProperty(name + ".column.0.value"))); + } + + private PropertyKey property(long id, String name, DataType type, Cardinality cardinality) { + PropertyKey key = new PropertyKey(this.graph, IdGenerator.of(id), name); + key.dataType(type); + key.cardinality(cardinality); + this.properties.put(id, key); + return key; + } + + private void vertexLabel(long id, String name, long ttl) { + VertexLabel label = new VertexLabel(this.graph, IdGenerator.of(id), name); + label.ttl(ttl); + label.properties(IdGenerator.of(1L), IdGenerator.of(2L), IdGenerator.of(3L)); + this.vertices.put(id, label); + } + + private void edgeLabel(long id, String name, long ttl) { + EdgeLabel label = new EdgeLabel(this.graph, IdGenerator.of(id), name); + label.ttl(ttl); + label.frequency(Frequency.MULTIPLE); + label.links(Pair.of(IdGenerator.of(4L), IdGenerator.of(4L))); + label.properties(IdGenerator.of(1L), IdGenerator.of(2L), IdGenerator.of(3L)); + this.edges.put(id, label); + } + + private void indexLabel(long id, IndexType type, long baseId) { + IndexLabel label = new IndexLabel(this.graph, IdGenerator.of(id), "index-" + id); + label.indexType(type); + label.baseType(HugeType.VERTEX_LABEL); + label.baseValue(IdGenerator.of(baseId)); + label.indexFields(IdGenerator.of(type == IndexType.RANGE_LONG ? 16L : + type == IndexType.SECONDARY ? 17L : 1L)); + this.indexes.put(id, label); + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/compatibility/LegacyKryoCompatibilityTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/compatibility/LegacyKryoCompatibilityTest.java new file mode 100644 index 0000000000..b9b56b44c8 --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/compatibility/LegacyKryoCompatibilityTest.java @@ -0,0 +1,448 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.compatibility; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.backend.BinaryId; +import org.apache.hugegraph.backend.Shard; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.IdUtil; +import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.query.Aggregate.AggregateFunc; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.query.serializer.AbstractSerializerAdapter; +import org.apache.hugegraph.query.serializer.QueryAdapter; +import org.apache.hugegraph.query.serializer.QueryIdAdapter; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.SchemaElement; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.struct.schema.builder.SchemaBuilder; +import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.type.GraphType; +import org.apache.hugegraph.type.Indexfiable; +import org.apache.hugegraph.type.Namifiable; +import org.apache.hugegraph.type.Propfiable; +import org.apache.hugegraph.type.Typifiable; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.util.Bytes; +import org.apache.hugegraph.util.KryoUtil; +import org.apache.hugegraph.util.LegacyClassNames; +import org.apache.tinkerpop.shaded.kryo.Kryo; +import org.apache.tinkerpop.shaded.kryo.KryoException; +import org.apache.tinkerpop.shaded.kryo.io.Input; +import org.apache.tinkerpop.shaded.kryo.io.Output; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** Original core OBJECT payloads and original default-reader receipts. */ +public class LegacyKryoCompatibilityTest { + + private Properties fixtures; + + @Before + public void prepare() throws IOException { + this.fixtures = new Properties(); + try (InputStream input = this.getClass().getResourceAsStream( + "/compatibility/2f827d6e8/core-kryo-objects.properties")) { + Assert.assertNotNull(input); + this.fixtures.load(input); + } + } + + @Test + public void testReadableRelocatedEnumsAndClassLiteral() { + this.assertValue("aggregate_func", AggregateFunc.AVG); + this.assertValue("relation_type", Condition.RelationType.GTE); + this.assertValue("condition_type", Condition.ConditionType.OR); + this.assertValue("query_order", Query.Order.DESC); + this.assertValue("query_order_type", Query.OrderType.ORDER_STRICT); + this.assertValue("optimized_type", ConditionQuery.OptimizedType.INDEX_FILTER); + this.assertValue("id_type", Id.IdType.UUID); + this.assertValue("graph_type_class", GraphType.class); + } + + @Test + public void testReadableRelocatedUserdata() { + Object decoded = this.read("userdata"); + Assert.assertEquals(Userdata.class, decoded.getClass()); + Assert.assertEquals(expectedUserdata(), decoded); + Assert.assertEquals(decoded, roundTrip(decoded)); + } + + @Test + public void testReadableRelocatedArrayTypes() { + Object ids = this.read("id_array"); + Assert.assertEquals(Id[].class, ids.getClass()); + Assert.assertArrayEquals(new Id[]{null}, (Id[]) ids); + Assert.assertArrayEquals((Id[]) ids, (Id[]) roundTrip(ids)); + Object matrix = this.read("id_matrix"); + Assert.assertEquals(Id[][].class, matrix.getClass()); + Assert.assertArrayEquals(new Id[][]{new Id[]{null}, new Id[0]}, (Id[][]) matrix); + Assert.assertArrayEquals((Id[][]) matrix, (Id[][]) roundTrip(matrix)); + Object shards = this.read("shard_array"); + Assert.assertEquals(Shard[].class, shards.getClass()); + Assert.assertArrayEquals(new Shard[]{null}, (Shard[]) shards); + Assert.assertArrayEquals((Shard[]) shards, (Shard[]) roundTrip(shards)); + Object schemas = this.read("schema_array"); + Assert.assertEquals(PropertyKey[].class, schemas.getClass()); + Assert.assertArrayEquals(new PropertyKey[]{null}, (PropertyKey[]) schemas); + Assert.assertArrayEquals((PropertyKey[]) schemas, (PropertyKey[]) roundTrip(schemas)); + } + + @Test + public void testReadableNestedClassAndArrayGraphs() { + Object classes = this.read("class_list"); + Assert.assertEquals(expectedClasses(), classes); + Assert.assertEquals(classes, roundTrip(classes)); + Object nested = this.read("nested"); + assertNested(nested); + assertNested(roundTrip(nested)); + } + + @Test + public void testReadableRenamedContractAndUtilityClassLiterals() { + List> expected = Arrays.asList(Namifiable.class, Typifiable.class, + Indexfiable.class, Propfiable.class, IdUtil.class, + SplicingIdGenerator.class, SchemaBuilder.class, + AbstractSerializerAdapter.class, QueryAdapter.class, + QueryIdAdapter.class); + Object decoded = this.read("contract_classes"); + Assert.assertEquals(expected, decoded); + Assert.assertEquals(expected, roundTrip(decoded)); + } + + @Test + public void testReadableLegacyBackendColumn() { + this.assertValue("backend_column_class", BackendColumn.class); + Object decoded = this.read("backend_column"); + assertColumn(decoded); + assertColumn(roundTrip(decoded)); + Object array = this.read("backend_column_array"); + Assert.assertEquals(BackendColumn[].class, array.getClass()); + Assert.assertEquals(2, ((BackendColumn[]) array).length); + assertColumn(((BackendColumn[]) array)[0]); + Assert.assertNull(((BackendColumn[]) array)[1]); + assertColumn(((BackendColumn[]) roundTrip(array))[0]); + } + + @Test + public void testReadableNestedSchemaClassLiteralsAndArrays() { + List> expected = Arrays.asList(SchemaElement.TaskWithSchema.class, + nestedClass(PropertyKey.class, "P"), nestedClass(VertexLabel.class, "P"), + nestedClass(EdgeLabel.class, "P"), nestedClass(IndexLabel.class, "P"), + SchemaElement.P.class, PropertyKey.Builder.class, VertexLabel.Builder.class, + EdgeLabel.Builder.class, IndexLabel.Builder.class); + Object classes = this.read("schema_nested_classes"); + Assert.assertEquals(expected, classes); + Assert.assertEquals(expected, roundTrip(classes)); + List arrays = (List) this.read("schema_nested_arrays"); + List written = (List) roundTrip(arrays); + for (int i = 0; i < expected.size(); i++) { + Class arrayClass = Array.newInstance(expected.get(i), 1).getClass(); + Assert.assertEquals(arrayClass, arrays.get(i).getClass()); + Assert.assertEquals(arrayClass, written.get(i).getClass()); + Assert.assertEquals(1, Array.getLength(arrays.get(i))); + Assert.assertNull(Array.get(arrays.get(i), 0)); + } + } + + @Test + public void testReadableRelocatedIndexClassLiteralsAndArrays() { + List> expected = Arrays.asList(Index.class, Index.IdWithExpiredTime.class); + Object classes = this.read("index_classes"); + Assert.assertEquals(expected, classes); + Assert.assertEquals(expected, roundTrip(classes)); + Object indexes = this.read("index_array"); + Assert.assertEquals(Index[].class, indexes.getClass()); + Assert.assertArrayEquals(new Index[]{null}, (Index[]) indexes); + Assert.assertArrayEquals((Index[]) indexes, (Index[]) roundTrip(indexes)); + Object expires = this.read("expiry_id_array"); + Assert.assertEquals(Index.IdWithExpiredTime[].class, expires.getClass()); + Assert.assertArrayEquals(new Index.IdWithExpiredTime[]{null}, (Index.IdWithExpiredTime[]) expires); + Assert.assertArrayEquals((Index.IdWithExpiredTime[]) expires, + (Index.IdWithExpiredTime[]) roundTrip(expires)); + } + + @Test + public void testLegacyArrayDescriptorDimensionBoundary() { + String legacy = "org.apache.hugegraph.backend.id.Id"; + Assert.assertEquals(Id.class, LegacyClassNames.lookup(legacy)); + Assert.assertEquals(Id[].class, LegacyClassNames.lookup("[L" + legacy + ";")); + Class limit = LegacyClassNames.lookup("[".repeat(255) + "L" + legacy + ";"); + int dimensions = 0; + while (limit.isArray()) { + dimensions++; + limit = limit.getComponentType(); + } + Assert.assertEquals(255, dimensions); + Assert.assertEquals(Id.class, limit); + Assert.assertNull(LegacyClassNames.lookup("[".repeat(256) + "L" + legacy + ";")); + Assert.assertNull(LegacyClassNames.lookup("[".repeat(5000) + "L" + legacy + ";")); + Assert.assertNull(LegacyClassNames.lookup("[Lexample.UserClass;")); + Assert.assertNull(LegacyClassNames.lookup("[I")); + } + + @Test + public void testOriginalUnsupportedInstancesKeepConstructorPolicy() { + for (String name : new String[]{"long_id", "string_id", "uuid_id", "edge_id", "binary_id", + "shard", "property_key", "vertex_label", "edge_label", + "index_label", "expiry_id", "unsupported_arrays_list"}) { + Assert.assertEquals(name, "false", this.fixtures.getProperty(name + ".baseline_readable")); + Assert.assertTrue(this.fixtures.getProperty(name + ".baseline_error") + .contains("missing no-arg constructor")); + // Reset failure state before and after a read, keeping independent + // fixtures independent without changing the instantiation strategy. + KryoUtil.kryo().reset(); + try { + KryoException error = Assert.assertThrows(KryoException.class, + () -> this.decode(name)); + Assert.assertTrue(name + ": " + error.getMessage(), + error.getMessage().contains("missing no-arg constructor")); + } finally { + KryoUtil.kryo().reset(); + } + } + } + + @Test + public void testEveryLegacyRelationOrdinal() throws IOException { + Properties relations = this.relationFixtures(); + String[] names = relations.getProperty("legacy.names").split(","); + Assert.assertEquals(15, names.length); + for (String name : names) { + Condition.RelationType expected = Condition.RelationType.valueOf(name); + Assert.assertEquals(name, expected, readRelationFixture(relations, "enum." + name)); + } + } + + @Test + public void testLegacyRelationDescriptorsInContainersAndArrays() throws IOException { + Properties relations = this.relationFixtures(); + Condition.RelationType[] expected = Arrays.stream(relations.getProperty("legacy.names").split(",")) + .map(Condition.RelationType::valueOf) + .toArray(Condition.RelationType[]::new); + Assert.assertArrayEquals(expected, (Object[]) readRelationFixture(relations, "array")); + Object matrix = readRelationFixture(relations, "matrix"); + Assert.assertEquals(Condition.RelationType[][].class, matrix.getClass()); + Assert.assertArrayEquals(new Object[]{expected, null, new Condition.RelationType[0]}, (Object[]) matrix); + Map nested = (Map) readRelationFixture(relations, "nested"); + Assert.assertEquals(Arrays.asList(expected), nested.get("values")); + Assert.assertArrayEquals(expected, (Object[]) nested.get("array")); + Assert.assertEquals(Condition.RelationType.SCAN, nested.get("last")); + } + + @Test + public void testInterleavedLegacyAndCanonicalRelationDescriptors() throws IOException { + List values = (List) readRelationFixture(this.relationFixtures(), "interleaved"); + assertInterleavedRelations(values); + List written = (List) roundTrip(values); + Assert.assertEquals(values.subList(0, 4), written.subList(0, 4)); + Assert.assertArrayEquals((Object[]) values.get(4), (Object[]) written.get(4)); + Assert.assertArrayEquals((Object[]) values.get(5), (Object[]) written.get(5)); + Assert.assertSame(written.get(4), written.get(6)); + Assert.assertSame(written.get(5), written.get(7)); + } + + @Test + public void testCanonicalRelationWriterRetainsDefaultOrdinals() { + for (Condition.RelationType relation : Condition.RelationType.values()) { + Assert.assertEquals(relation, roundTrip(relation)); + Output output = new Output(128); + new Kryo().writeClassAndObject(output, relation); + Assert.assertArrayEquals(output.toBytes(), KryoUtil.toKryoWithType(relation)); + } + Condition.RelationType[] values = Condition.RelationType.values(); + Assert.assertArrayEquals(values, (Object[]) roundTrip(values)); + } + + @Test + public void testCanonicalRelationsWithStreamedInput() { + for (int bufferSize : new int[]{2, 7, 16}) { + for (Condition.RelationType relation : Condition.RelationType.values()) { + for (Object value : readStreamedTwice(KryoUtil.toKryoWithType(relation), bufferSize)) { + Assert.assertEquals(relation, value); + } + } + Condition.RelationType[] relations = {Condition.RelationType.TEXT_PREFIX, null, + Condition.RelationType.TEXT_REGEX}; + for (Object value : readStreamedTwice(KryoUtil.toKryoWithType(relations), bufferSize)) { + Assert.assertArrayEquals(relations, (Object[]) value); + } + } + } + + @Test + public void testLegacyRelationsWithStreamedInput() throws IOException { + Properties relations = this.relationFixtures(); + Condition.RelationType[] expected = Arrays.stream(relations.getProperty("legacy.names").split(",")) + .map(Condition.RelationType::valueOf) + .toArray(Condition.RelationType[]::new); + for (int bufferSize : new int[]{2, 7, 16}) { + for (Condition.RelationType relation : expected) { + byte[] bytes = Bytes.fromHex(relations.getProperty("enum." + relation + ".hex")); + for (Object value : readStreamedTwice(bytes, bufferSize)) { + Assert.assertEquals(relation, value); + } + } + for (Object value : readStreamedTwice(Bytes.fromHex(relations.getProperty("array.hex")), bufferSize)) { + Assert.assertArrayEquals(expected, (Object[]) value); + } + for (Object value : readStreamedTwice(Bytes.fromHex(relations.getProperty("matrix.hex")), bufferSize)) { + Assert.assertArrayEquals(new Object[]{expected, null, new Condition.RelationType[0]}, (Object[]) value); + } + byte[] interleaved = Bytes.fromHex(relations.getProperty("interleaved.hex")); + for (Object value : readStreamedTwice(interleaved, bufferSize)) { + assertInterleavedRelations((List) value); + } + } + } + + private static Object[] readStreamedTwice(byte[] payload, int bufferSize) { + // Start a descriptor at the last buffered byte and cross refills; the second + // top-level object also verifies reset of descriptor and reference caches. + int prefix = bufferSize - 1; + byte[] bytes = new byte[prefix + 2 * payload.length + 1]; + System.arraycopy(payload, 0, bytes, prefix, payload.length); + System.arraycopy(payload, 0, bytes, prefix + payload.length, payload.length); + bytes[bytes.length - 1] = 93; + try (Input input = new Input(new ByteArrayInputStream(bytes), bufferSize)) { + for (int i = 0; i < prefix; i++) { + Assert.assertEquals(0, input.readByte()); + } + Object[] values = {KryoUtil.kryo().readClassAndObject(input), KryoUtil.kryo().readClassAndObject(input)}; + Assert.assertEquals(93, input.readByte()); + return values; + } finally { + KryoUtil.kryo().reset(); + } + } + + private static void assertInterleavedRelations(List values) { + Assert.assertEquals(Condition.RelationType.SCAN, values.get(0)); + Assert.assertEquals(Condition.RelationType.TEXT_PREFIX, values.get(1)); + Assert.assertEquals(Condition.RelationType.TEXT_CONTAINS, values.get(2)); + Assert.assertEquals(Condition.RelationType.TEXT_REGEX, values.get(3)); + Assert.assertArrayEquals(new Condition.RelationType[]{Condition.RelationType.SCAN, null, + Condition.RelationType.TEXT_CONTAINS}, + (Object[]) values.get(4)); + Assert.assertArrayEquals(new Condition.RelationType[]{Condition.RelationType.TEXT_PREFIX, null, + Condition.RelationType.TEXT_REGEX}, + (Object[]) values.get(5)); + Assert.assertSame(values.get(4), values.get(6)); + Assert.assertSame(values.get(5), values.get(7)); + } + + private Properties relationFixtures() throws IOException { + Properties relations = new Properties(); + try (InputStream input = this.getClass().getResourceAsStream( + "/compatibility/2f827d6e8/core-relation-ordinals.properties")) { + Assert.assertNotNull(input); + relations.load(input); + } + return relations; + } + + private static Object readRelationFixture(Properties relations, String name) { + return KryoUtil.fromKryoWithType(Bytes.fromHex(relations.getProperty(name + ".hex"))); + } + + private void assertValue(String name, Object expected) { + Object decoded = this.read(name); + Assert.assertEquals(expected, decoded); + // Java API names may change. New writes need semantic compatibility, + // rather than continuing to emit removed core class names. + Assert.assertEquals(expected, roundTrip(decoded)); + } + + private Object read(String name) { + Assert.assertEquals(name, "true", this.fixtures.getProperty(name + ".baseline_readable")); + return this.decode(name); + } + + private Object decode(String name) { + BytesBuffer reader = BytesBuffer.wrap(Bytes.fromHex(this.fixtures.getProperty(name + ".hex"))); + Object decoded = reader.readProperty(DataType.OBJECT); + Assert.assertEquals(0, reader.remaining()); + return decoded; + } + + private static Object roundTrip(Object value) { + BytesBuffer writer = BytesBuffer.allocate(128); + writer.writeProperty(DataType.OBJECT, value); + return BytesBuffer.wrap(writer.bytes()).readProperty(DataType.OBJECT); + } + + private static Userdata expectedUserdata() { + Userdata userdata = new Userdata(); + userdata.put("tag", "legacy"); + userdata.put("count", 7); + userdata.put(Userdata.CREATE_TIME, new Date(1609459200123L)); + return userdata; + } + + private static List> expectedClasses() { + return new ArrayList<>(Arrays.asList(IdGenerator.LongId.class, + IdGenerator.StringId.class, IdGenerator.UuidId.class, + EdgeId.class, BinaryId.class, Shard.class, PropertyKey.class, + VertexLabel.class, EdgeLabel.class, IndexLabel.class)); + } + + private static Class nestedClass(Class owner, String name) { + return Arrays.stream(owner.getDeclaredClasses()) + .filter(type -> type.getSimpleName().equals(name)) + .findFirst().orElseThrow(AssertionError::new); + } + + private static void assertColumn(Object value) { + Assert.assertEquals(BackendColumn.class, value.getClass()); + BackendColumn column = (BackendColumn) value; + Assert.assertArrayEquals(new byte[]{1, 2}, column.name); + Assert.assertArrayEquals(new byte[]{3, 4}, column.value); + } + + private static void assertNested(Object value) { + Assert.assertTrue(value instanceof Map); + Map nested = (Map) value; + Assert.assertEquals(expectedClasses(), nested.get("classes")); + Assert.assertEquals(expectedUserdata(), nested.get("metadata")); + Assert.assertEquals(Id[][].class, nested.get("ids").getClass()); + Assert.assertArrayEquals(new Id[][]{new Id[]{null}, new Id[0]}, (Id[][]) nested.get("ids")); + Assert.assertEquals(Condition.RelationType.GTE, nested.get("predicate")); + assertColumn(nested.get("column")); + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/id/EdgeIdTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/id/EdgeIdTest.java new file mode 100644 index 0000000000..ceceec87bc --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/id/EdgeIdTest.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.hugegraph.id; + +import java.util.UUID; + +import org.apache.hugegraph.backend.BinaryId; +import org.apache.hugegraph.exception.NotFoundException; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.Directions; +import org.junit.Assert; +import org.junit.Test; + +/** + * Text form and parsing rules of the struct EdgeId and SplicingIdGenerator. + * Server unit tests cover successful parsing and equality; this class covers + * the directed form, escaping and the rejection paths. + */ +public class EdgeIdTest { + + private static final Id OWNER = IdGenerator.of("alice"); + private static final Id OTHER = IdGenerator.of(42L); + private static final Id LABEL = IdGenerator.of(3L); + private static final Id SUB_LABEL = IdGenerator.of(4L); + + @Test + public void testUndirectedTextIsWrittenFromSourceToTarget() { + EdgeId in = new EdgeId(OWNER, Directions.IN, LABEL, SUB_LABEL, "s", OTHER); + // An IN edge stored under alice is the OUT edge 42 -> alice + Assert.assertEquals("L42>3>4>s>Salice", in.asString()); + + EdgeId out = in.switchDirection(); + Assert.assertEquals(OTHER, out.ownerVertexId()); + Assert.assertEquals(Directions.OUT, out.direction()); + Assert.assertEquals(OWNER, out.otherVertexId()); + Assert.assertEquals(in.asString(), out.asString()); + Assert.assertEquals(in, out); + Assert.assertEquals(in.hashCode(), out.hashCode()); + Assert.assertEquals(0, in.compareTo(out)); + + EdgeId parsed = EdgeId.parse(in.asString()); + Assert.assertEquals(Directions.OUT, parsed.direction()); + Assert.assertEquals(OTHER, parsed.ownerVertexId()); + Assert.assertEquals(in, parsed); + } + + @Test + public void testDirectedTextKeepsOwnerAndDirection() { + EdgeId directed = new EdgeId(OWNER, Directions.IN, LABEL, SUB_LABEL, "s", OTHER, true); + Assert.assertEquals("Salice>I>3>4>s>L42", directed.asString()); + // Directed identity distinguishes the two sides of the same edge + Assert.assertNotEquals(directed, directed.switchDirection()); + Assert.assertEquals(directed, directed.directed(false).directed(true)); + + EdgeId parsed = EdgeId.parse(directed.asString()); + Assert.assertEquals(OWNER, parsed.ownerVertexId()); + Assert.assertEquals(Directions.IN, parsed.direction()); + Assert.assertEquals(LABEL, parsed.edgeLabelId()); + Assert.assertEquals(SUB_LABEL, parsed.subLabelId()); + Assert.assertEquals("s", parsed.sortValues()); + Assert.assertEquals(OTHER, parsed.otherVertexId()); + Assert.assertEquals(directed.directed(false), parsed); + } + + @Test + public void testSeparatorsInsidePartsSurviveTextRoundTrip() { + Id owner = IdGenerator.of("a>b"); + EdgeId edge = new EdgeId(owner, Directions.OUT, LABEL, SUB_LABEL, "x>y`z", OTHER); + Assert.assertEquals("Sa`>b>3>4>x`>y`z>L42", edge.asString()); + Assert.assertEquals(5, EdgeId.split(edge).length); + + EdgeId parsed = EdgeId.parse(edge.asString()); + Assert.assertEquals(owner, parsed.ownerVertexId()); + Assert.assertEquals("x>y`z", parsed.sortValues()); + Assert.assertEquals(edge, parsed); + } + + @Test + public void testParseRejectsMalformedText() { + String[] invalid = { + "Salice>3>4>L42", // too few parts + "Salice>O>3>4>s>L42>extra", // too many parts + "Salice>x>4>s>L42", // non numeric label + "Salice>3>4>s>Lnan", // non numeric long vertex + "Salice>Z>3>4>s>L42", // unknown direction + "Salice>VL>3>4>s>L42" // a type that is not a direction + }; + for (String text : invalid) { + Assert.assertThrows(text, NotFoundException.class, () -> EdgeId.parse(text)); + Assert.assertNull(text, EdgeId.parse(text, true)); + } + NotFoundException e = Assert.assertThrows(NotFoundException.class, + () -> EdgeId.parse("a>b")); + Assert.assertTrue(e.getMessage().contains("5~6 parts, but got 2 parts")); + e = Assert.assertThrows(NotFoundException.class, () -> EdgeId.parse("Salice>x>4>s>L42")); + Assert.assertTrue(e.getCause() instanceof NumberFormatException); + } + + @Test + public void testStoredStringRequiresFiveParts() { + EdgeId edge = new EdgeId(OWNER, Directions.IN, LABEL, SUB_LABEL, "s", OTHER); + String stored = EdgeId.asStoredString(edge); + Assert.assertEquals(edge, EdgeId.parseStoredString(stored)); + + Assert.assertThrows(IllegalArgumentException.class, + () -> EdgeId.parseStoredString("Salice>D>E>s")); + Assert.assertThrows(IllegalArgumentException.class, + () -> EdgeId.parseStoredString(stored + ">extra")); + } + + @Test + public void testDirectionCodes() { + Assert.assertEquals(HugeType.EDGE_OUT.code(), EdgeId.directionToCode(Directions.OUT)); + Assert.assertEquals(HugeType.EDGE_IN.code(), EdgeId.directionToCode(Directions.IN)); + Assert.assertTrue(EdgeId.isOutDirectionFromCode(HugeType.EDGE_OUT.code())); + Assert.assertFalse(EdgeId.isOutDirectionFromCode(HugeType.EDGE_IN.code())); + Assert.assertThrows(IllegalArgumentException.class, + () -> EdgeId.directionFromCode(HugeType.VERTEX.code())); + Assert.assertThrows(IllegalArgumentException.class, + () -> EdgeId.directionToCode(Directions.BOTH)); + Assert.assertThrows(UnsupportedOperationException.class, + () -> new EdgeId(OWNER, Directions.OUT, LABEL, SUB_LABEL, "", + OTHER).asLong()); + } + + @Test + public void testSplicingEscapesOnlyItsOwnSeparator() { + Id spliced = SplicingIdGenerator.splicing("1", "a:b", "c>d"); + Assert.assertEquals("1:a`:b:c>d", spliced.asString()); + Assert.assertArrayEquals(new String[]{"1", "a:b", "c>d"}, + SplicingIdGenerator.parse(spliced)); + + Id raw = SplicingIdGenerator.splicingWithNoEscape("1", "a:b"); + Assert.assertArrayEquals(new String[]{"1", "a", "b"}, SplicingIdGenerator.parse(raw)); + + Assert.assertEquals("v`!1!2!", SplicingIdGenerator.concatValues("v!1", 2, "")); + Assert.assertArrayEquals(new String[]{"", ""}, SplicingIdGenerator.split(">")); + } + + @Test + public void testGenerateBinaryIdMatchesBinaryIdLayout() { + Id[] ids = {IdGenerator.of("v1"), IdGenerator.of(1L << 40), IdGenerator.of(-1L), + IdGenerator.of(UUID.fromString("00112233-4455-6677-8899-aabbccddeeff"))}; + for (Id id : ids) { + Id binary = SplicingIdGenerator.generateBinaryId(id); + Assert.assertTrue(binary instanceof BinaryId); + Assert.assertArrayEquals(BytesBuffer.allocate(32).writeId(id).bytes(), binary.asBytes()); + Assert.assertEquals(id, ((BinaryId) binary).origin()); + Assert.assertEquals(id, BytesBuffer.wrap(binary.asBytes()).readId()); + // Already binary ids are returned unchanged + Assert.assertSame(binary, SplicingIdGenerator.generateBinaryId(binary)); + } + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/id/IdCompatibilityTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/id/IdCompatibilityTest.java new file mode 100644 index 0000000000..96eff3ba4b --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/id/IdCompatibilityTest.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.hugegraph.id; + +import java.util.UUID; + +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.util.StringEncoding; +import org.junit.Assert; +import org.junit.Test; + +public class IdCompatibilityTest { + + @Test + public void testNumericIdsRejectTextAndNullWithoutParsing() { + Id id = IdGenerator.of(42L); + Assert.assertFalse(id.equals(null)); + Assert.assertFalse(id.equals("42")); + Assert.assertFalse(id.equals("42.0")); + Assert.assertFalse(id.equals("not-a-number")); + Assert.assertFalse(id.equals(IdGenerator.of("42"))); + Id same = IdGenerator.of(new byte[]{0, 0, 0, 0, 0, 0, 0, 42}, Id.IdType.LONG); + Assert.assertEquals(id, same); + Assert.assertEquals(id.hashCode(), same.hashCode()); + } + + @Test + public void testUtf8IdsAgreeAcrossStringAndByteRepresentations() { + String text = "北京`>vertex"; + Id fromString = IdGenerator.of(text); + Id fromBytes = IdGenerator.of(StringEncoding.encode(text), Id.IdType.STRING); + Assert.assertEquals(fromString, fromBytes); + Assert.assertEquals(fromBytes, fromString); + Assert.assertEquals(fromString.hashCode(), fromBytes.hashCode()); + Assert.assertEquals(0, fromString.compareTo(fromBytes)); + Assert.assertEquals(0, fromBytes.compareTo(fromString)); + Assert.assertEquals(text, fromBytes.asObject()); + Assert.assertArrayEquals(StringEncoding.encode(text), fromString.asBytes()); + } + + @Test + public void testStoredIdsPreserveTypesAndEscapedEdgeParts() { + Id[] ids = { + IdGenerator.of(Long.MIN_VALUE), + IdGenerator.of("北京`>vertex"), + IdGenerator.of(UUID.fromString("00112233-4455-6677-8899-aabbccddeeff")), + new EdgeId(IdGenerator.of("owner>vertex"), Directions.OUT, + IdGenerator.of(1), IdGenerator.of(2), "value>with`escape", + IdGenerator.of(42)) + }; + for (Id id : ids) { + Id stored = IdUtil.readStoredString(IdUtil.writeStoredString(id)); + Assert.assertEquals(id.type(), stored.type()); + Assert.assertEquals(id, stored); + Assert.assertEquals(id.hashCode(), stored.hashCode()); + } + Assert.assertEquals(Directions.OUT, EdgeId.directionFromCode(HugeType.EDGE_OUT.code())); + Assert.assertEquals(Directions.IN, EdgeId.directionFromCode(HugeType.EDGE_IN.code())); + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/id/StringIdComparisonTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/id/StringIdComparisonTest.java new file mode 100644 index 0000000000..7a22f22434 --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/id/StringIdComparisonTest.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.hugegraph.id; + +import org.apache.hugegraph.util.StringEncoding; +import org.junit.Assert; +import org.junit.Test; + +public class StringIdComparisonTest { + + @Test + public void testUtf16OrderAcrossRepresentations() { + String[] values = {"a", "aa", "北京", "\uD7FF", "\uD800\uDC00", "\uE000", "\uFFFF"}; + for (String left : values) { + for (String right : values) { + int expected = Integer.signum(left.compareTo(right)); + for (boolean leftBytes : new boolean[]{false, true}) { + for (boolean rightBytes : new boolean[]{false, true}) { + // Fresh IDs keep both directions independent of lazy decoding. + int forward = compare(left, leftBytes, right, rightBytes); + int reverse = compare(right, rightBytes, left, leftBytes); + Assert.assertEquals(expected, forward); + Assert.assertEquals(-forward, reverse); + } + } + } + } + } + + @Test + public void testOrderDoesNotChangeAfterLazyConversion() { + Id supplementary = newId("\uD800\uDC00", true); + Id privateUse = newId("\uE000", true); + int before = Integer.signum(supplementary.compareTo(privateUse)); + Assert.assertEquals(-1, before); + + supplementary.asString(); + Assert.assertEquals(before, Integer.signum(supplementary.compareTo(privateUse))); + privateUse.asString(); + Assert.assertEquals(before, Integer.signum(supplementary.compareTo(privateUse))); + supplementary.asBytes(); + privateUse.asBytes(); + Assert.assertEquals(before, Integer.signum(supplementary.compareTo(privateUse))); + Assert.assertEquals(-before, Integer.signum(privateUse.compareTo(supplementary))); + } + + @Test + public void testMixedUnicodeOrderIsTransitive() { + String beforeSurrogates = "\uD7FF"; + String supplementary = "\uD800\uDC00"; + String privateUse = "\uE000"; + for (boolean firstBytes : new boolean[]{false, true}) { + for (boolean secondBytes : new boolean[]{false, true}) { + for (boolean thirdBytes : new boolean[]{false, true}) { + Assert.assertTrue(compare(beforeSurrogates, firstBytes, + supplementary, secondBytes) < 0); + Assert.assertTrue(compare(supplementary, secondBytes, + privateUse, thirdBytes) < 0); + Assert.assertTrue(compare(beforeSurrogates, firstBytes, + privateUse, thirdBytes) < 0); + } + } + } + } + + private static int compare(String left, boolean leftBytes, String right, boolean rightBytes) { + return Integer.signum(newId(left, leftBytes).compareTo(newId(right, rightBytes))); + } + + private static Id newId(String value, boolean bytes) { + return bytes ? IdGenerator.of(StringEncoding.encode(value), Id.IdType.STRING) : + IdGenerator.of(value); + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/query/SharedQueryTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/query/SharedQueryTest.java new file mode 100644 index 0000000000..f01bdb2d3c --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/query/SharedQueryTest.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.hugegraph.query; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Aggregate.AggregateFunc; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.structure.BaseVertex; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.HugeKeys; +import org.junit.Assert; +import org.junit.Test; + +public class SharedQueryTest { + + @Test + public void testOffsetAdvancesOnlyByCurrentBatchSize() { + Query query = new Query(HugeType.VERTEX); + query.offset(5L); + query.limit(2L); + Assert.assertTrue(query.skipOffsetIfNeeded( + new LinkedHashSet<>(Arrays.asList(1, 2))).isEmpty()); + Assert.assertEquals(2L, query.actualOffset()); + Assert.assertTrue(query.skipOffsetIfNeeded( + new LinkedHashSet<>(Arrays.asList(3, 4))).isEmpty()); + Assert.assertEquals(4L, query.actualOffset()); + Assert.assertEquals(new LinkedHashSet<>(Arrays.asList(6, 7)), + query.skipOffsetIfNeeded( + new LinkedHashSet<>(Arrays.asList(5, 6, 7)))); + Assert.assertEquals(5L, query.actualOffset()); + } + + @Test + public void testContradictoryIntersectionsStayEmpty() { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.query(Condition.eq(HugeKeys.LABEL, 1)); + query.query(Condition.eq(HugeKeys.LABEL, 2)); + query.query(Condition.in(HugeKeys.LABEL, Arrays.asList(2, 3))); + Assert.assertNull(query.condition(HugeKeys.LABEL)); + + ConditionQuery emptyFirst = new ConditionQuery(HugeType.VERTEX); + emptyFirst.query(Condition.in(HugeKeys.LABEL, Collections.emptyList())); + emptyFirst.query(Condition.in(HugeKeys.LABEL, Arrays.asList(2, 3))); + Assert.assertNull(emptyFirst.condition(HugeKeys.LABEL)); + } + + @Test + public void testExplicitConditionResolutionPreservesLegacyInList() { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + Assert.assertFalse(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertTrue(query.conditionValues(HugeKeys.LABEL).isEmpty()); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); + + List labels = Arrays.asList(1, 2, 2); + query.query(Condition.in(HugeKeys.LABEL, labels)); + Assert.assertSame(labels, query.condition(HugeKeys.LABEL)); + Assert.assertEquals(new LinkedHashSet<>(labels), query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.singleConditionValueOrNull(HugeKeys.LABEL)); + Assert.assertThrows(IllegalStateException.class, () -> query.conditionValue(HugeKeys.LABEL)); + + query.eq(HugeKeys.LABEL, 2); + Assert.assertEquals(Integer.valueOf(2), query.conditionValue(HugeKeys.LABEL)); + Assert.assertEquals(Integer.valueOf(2), query.singleConditionValueOrNull(HugeKeys.LABEL)); + query.eq(HugeKeys.LABEL, 3); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertTrue(query.conditionValues(HugeKeys.LABEL).isEmpty()); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); + } + + @Test + public void testEmptyInAndNegativeConditionAreDistinct() { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.neq(HugeKeys.LABEL, 1); + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertFalse(query.containsConditionValues(HugeKeys.LABEL)); + query.query(Condition.in(HugeKeys.LABEL, Collections.emptyList())); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertEquals(Collections.emptyList(), query.condition(HugeKeys.LABEL)); + Assert.assertNull(query.singleConditionValueOrNull(HugeKeys.LABEL)); + } + + @Test + public void testSingleIdAndCopy() { + Id id = IdGenerator.of(123L); + IdQuery.OneIdQuery query = new IdQuery.OneIdQuery(HugeType.VERTEX, id); + BaseVertex vertex = new BaseVertex(id); + Assert.assertTrue(query.test(vertex)); + Assert.assertFalse(query.test(new BaseVertex(IdGenerator.of(124L)))); + IdQuery.OneIdQuery copy = (IdQuery.OneIdQuery) query.copy(); + query.resetIds(); + Assert.assertEquals(0, query.idsSize()); + Assert.assertTrue(query.test(vertex)); + Assert.assertEquals(id, copy.id()); + Assert.assertEquals(1, copy.idsSize()); + } + + @Test + public void testRangeIndexValueConsumedOncePerProperty() { + Id key = IdGenerator.of(10L); + Id id = IdGenerator.of(123L); + PropertyKey property = new PropertyKey(null, key, "age"); + property.dataType(DataType.INT); + BaseVertex vertex = new BaseVertex(id); + vertex.addProperty(property, 25); + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.query(Condition.gte(key, 20)); + query.query(Condition.lte(key, 30)); + query.recordIndexValue(key, id, 25); + query.recordIndexValue(key, id, 17); + query.selectedIndexField(key); + Assert.assertTrue(query.test(vertex)); + Assert.assertEquals(1, query.getLeftIndexOfElement(id).size()); + Assert.assertEquals(Collections.singleton(17), + query.getLeftIndexOfElement(id).iterator().next() + .indexFieldValues()); + } + + @Test + public void testExplicitFilterAndFreshElements() { + BaseVertex vertex = new BaseVertex(IdGenerator.of(123L)); + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + AtomicInteger calls = new AtomicInteger(); + query.registerResultsFilter(element -> { + calls.incrementAndGet(); + return false; + }); + Assert.assertFalse(query.test(vertex)); + Assert.assertEquals(1, calls.get()); + vertex.fresh(true); + Assert.assertTrue(query.test(vertex)); + Assert.assertEquals(1, calls.get()); + vertex.fresh(false); + Assert.assertTrue(query.test(vertex, element -> true)); + Assert.assertEquals(1, calls.get()); + } + + @Test + public void testAggregateDescriptorRetainsNumericStrategy() { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.aggregate(AggregateFunc.AVG, "age"); + byte[] encoded = query.bytes(); + Assert.assertTrue(new String(encoded, StandardCharsets.UTF_8) + .contains("\"aggregate\":{\"func\":\"AVG\",\"column\":\"age\"}")); + ConditionQuery decoded = ConditionQuery.fromBytes(encoded); + Assert.assertEquals(AggregateFunc.AVG, decoded.aggregateNotNull().func()); + Assert.assertEquals("age", decoded.aggregateNotNull().column()); + Assert.assertEquals(25D, decoded.aggregateNotNull() + .reduce(Arrays.asList(20, 30).iterator()) + .doubleValue(), 0D); + query.aggregate(AggregateFunc.COUNT, null); + decoded = ConditionQuery.fromBytes(query.bytes()); + Assert.assertTrue(decoded.aggregateNotNull().countAll()); + Assert.assertEquals(0L, decoded.aggregateNotNull().defaultValue()); + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/serializer/BytesBufferTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/serializer/BytesBufferTest.java new file mode 100644 index 0000000000..588fad41db --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/serializer/BytesBufferTest.java @@ -0,0 +1,281 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.serializer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.UUID; + +import org.apache.hugegraph.backend.BinaryId; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.util.Bytes; +import org.junit.Assert; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; + +/** + * Behaviour of the struct BytesBuffer that the legacy golden fixtures in + * compatibility/LegacyCoreCodecTest do not reach: encoding width boundaries, + * malformed input rejection and buffer capacity rules. + */ +public class BytesBufferTest { + + @Test + public void testReadVarIntRejectsMalformedEncodings() { + // Five continuation bytes never terminate within the int width + assertIllegalArgument(() -> wrap(0x81, 0x81, 0x81, 0x81, 0x81).readVInt(), + "too many bytes"); + // A five byte varint can carry only four payload bits in its lead + assertIllegalArgument(() -> wrap(0x90, 0x80, 0x80, 0x80, 0x00).readVInt(), + "leading byte"); + Assert.assertEquals(Integer.MAX_VALUE, wrap(0x87, 0xff, 0xff, 0xff, 0x7f).readVInt()); + } + + @Test + public void testReadVarLongRejectsMalformedEncodings() { + int[] neverEnding = new int[10]; + Arrays.fill(neverEnding, 0x81); + assertIllegalArgument(() -> wrap(neverEnding).readVLong(), "too many bytes"); + + byte[] minusOne = BytesBuffer.allocate(16).writeVLong(-1L).bytes(); + Assert.assertEquals(-1L, BytesBuffer.wrap(minusOne).readVLong()); + // A ten byte varlong can carry only one payload bit in its lead + byte[] overflow = minusOne.clone(); + overflow[0] = (byte) 0x83; + assertIllegalArgument(() -> BytesBuffer.wrap(overflow).readVLong(), "leading byte"); + } + + @Test + public void testUnsignedIntegersRoundTripAtTheirLimits() { + BytesBuffer buffer = BytesBuffer.allocate(16); + buffer.writeUInt8(BytesBuffer.UINT8_MAX).writeUInt8(0); + buffer.writeUInt16(BytesBuffer.UINT16_MAX).writeUInt16(0); + buffer.writeUInt32(BytesBuffer.UINT32_MAX).writeUInt32(0L); + Assert.assertEquals(14, buffer.position()); + Assert.assertEquals("ff00ffff0000ffffffff00000000", Bytes.toHex(buffer.bytes())); + + buffer.forReadWritten(); + Assert.assertEquals(255, buffer.readUInt8()); + Assert.assertEquals(0, buffer.readUInt8()); + Assert.assertEquals(65535, buffer.readUInt16()); + Assert.assertEquals(0, buffer.readUInt16()); + Assert.assertEquals(4294967295L, buffer.readUInt32()); + Assert.assertEquals(0L, buffer.readUInt32()); + } + + @Test + public void testBytesAreLengthPrefixedAndLimited() { + BytesBuffer buffer = BytesBuffer.allocate(4); + buffer.writeBytes(BytesBuffer.BYTES_EMPTY); + byte[] big = new byte[300]; + Arrays.fill(big, (byte) 0x5a); + buffer.writeBigBytes(big); + // Empty payload is a single zero length; 300 needs a two byte vint + Assert.assertEquals(1 + 2 + 300, buffer.position()); + + buffer.forReadWritten(); + Assert.assertArrayEquals(BytesBuffer.BYTES_EMPTY, buffer.readBytes()); + Assert.assertArrayEquals(big, buffer.readBigBytes()); + Assert.assertEquals(0, buffer.remaining()); + + byte[] tooLong = new byte[(int) BytesBuffer.BYTES_LEN_MAX + 1]; + BytesBuffer writer = BytesBuffer.allocate(8); + assertIllegalArgument(() -> writer.writeBytes(tooLong), "max length of bytes"); + Assert.assertEquals("Rejected bytes must not be partially written", 0, writer.position()); + } + + @Test + public void testStringsWithEndingAreSequentialAndOrderPreserving() { + BytesBuffer buffer = BytesBuffer.allocate(8); + buffer.writeStringWithEnding("ab").writeStringWithEnding("").writeStringWithEnding("顶点"); + buffer.forReadWritten(); + Assert.assertEquals("ab", buffer.readStringWithEnding()); + Assert.assertEquals("", buffer.readStringWithEnding()); + Assert.assertEquals("顶点", buffer.readStringWithEnding()); + Assert.assertEquals(0, buffer.remaining()); + + // The 0x00 ending keeps a prefix ordered before its extensions + byte[] prefix = BytesBuffer.allocate(8).writeStringWithEnding("ab").bytes(); + byte[] longer = BytesBuffer.allocate(8).writeStringWithEnding("ab!").bytes(); + Assert.assertTrue(Bytes.compare(prefix, longer) < 0); + + assertIllegalArgument(() -> wrap('a', 'b').readStringWithEnding(), "Not found ending"); + } + + @Test + public void testEdgeIdIsMarkedAndValidatedOnRead() { + EdgeId edge = new EdgeId(IdGenerator.of("alice"), Directions.IN, IdGenerator.of(3L), + IdGenerator.of(4L), "2026", IdGenerator.of(99L)); + byte[] bytes = BytesBuffer.allocate(8).writeId(edge).bytes(); + Assert.assertEquals((byte) 0x7e, bytes[0]); + BytesBuffer reader = BytesBuffer.wrap(bytes); + EdgeId actual = (EdgeId) reader.readId(); + Assert.assertEquals(edge, actual); + Assert.assertEquals(Directions.IN, actual.direction()); + Assert.assertEquals(0, reader.remaining()); + + EdgeId nulSort = new EdgeId(IdGenerator.of("alice"), Directions.OUT, IdGenerator.of(3L), + IdGenerator.of(4L), "a\u0000", IdGenerator.of(99L)); + assertIllegalArgument(() -> BytesBuffer.allocate(8).writeId(nulSort), "0x00"); + + // A column type byte that is neither EDGE_OUT nor EDGE_IN is corrupt + BytesBuffer corrupt = BytesBuffer.allocate(32); + corrupt.writeId(IdGenerator.of("alice")); + corrupt.write(HugeType.VERTEX.code()); + corrupt.writeId(IdGenerator.of(3L)).writeId(IdGenerator.of(4L)); + corrupt.writeStringWithEnding("").writeId(IdGenerator.of(99L)); + Assert.assertThrows(IllegalStateException.class, + () -> corrupt.forReadWritten().readEdgeId()); + + byte[] withoutOwner = Arrays.copyOfRange(bytes, 1 + 6, bytes.length); + assertIllegalArgument(() -> BytesBuffer.wrap(withoutOwner).readEdgeId(false, null), + "owner"); + } + + @Test + public void testIndexIdEndingDependsOnIndexType() { + byte[] label = {(byte) 150, 1, 2, 3, 4, 'v'}; + byte[] ended = BytesBuffer.allocate(8) + .writeIndexId(new BinaryId(label, null), HugeType.SECONDARY_INDEX) + .bytes(); + Assert.assertEquals(label.length + 1, ended.length); + Assert.assertEquals(BytesBuffer.STRING_ENDING_BYTE, ended[label.length]); + byte[] open = BytesBuffer.allocate(8) + .writeIndexId(new BinaryId(label, null), + HugeType.SECONDARY_INDEX, false) + .bytes(); + Assert.assertArrayEquals(label, open); + + byte[] range = {(byte) 160, 0, 0, 0, 3, (byte) 0x80, 0, 0, 7}; + byte[] written = BytesBuffer.allocate(8) + .writeIndexId(new BinaryId(range, null), + HugeType.RANGE_INT_INDEX) + .bytes(); + Assert.assertArrayEquals("Range index ids have a fixed width and no ending", + range, written); + // Range ids may legally contain 0x00 while string index ids may not + byte[] nul = {(byte) 150, 0, 'v'}; + assertIllegalArgument(() -> BytesBuffer.allocate(8).writeIndexId( + new BinaryId(nul, null), HugeType.SECONDARY_INDEX), "0x00"); + assertIllegalArgument(() -> BytesBuffer.allocate(8).writeIndexId( + new BinaryId(new byte[0], null), HugeType.RANGE_INT_INDEX), "empty id"); + } + + @Test + public void testParseIdSkipsPartitionAndOlapPrefixes() { + Id vertex = IdGenerator.of("v1"); + byte[] vertexBytes = BytesBuffer.allocate(8).writeId(vertex).bytes(); + + BytesBuffer partitioned = BytesBuffer.allocate(16); + partitioned.writeShort((short) 7).writeId(vertex).forReadWritten(); + BinaryId parsed = partitioned.parseId(HugeType.VERTEX, true); + Assert.assertArrayEquals(vertexBytes, parsed.asBytes()); + Assert.assertEquals(vertex, parsed.origin()); + Assert.assertEquals(0, partitioned.remaining()); + + Id propertyKey = IdGenerator.of(5L); + BytesBuffer olap = BytesBuffer.allocate(16); + olap.writeId(propertyKey).writeId(vertex).forReadWritten(); + BinaryId olapId = olap.parseOlapId(HugeType.VERTEX, true); + // The OLAP binary id keeps its property prefix but resolves to the vertex + byte[] keyBytes = BytesBuffer.allocate(8).writeId(propertyKey).bytes(); + Assert.assertEquals(keyBytes.length + vertexBytes.length, olapId.length()); + Assert.assertEquals(vertex, olapId.origin()); + } + + @Test + public void testTaggedCollectionsRestoreContainerType() { + PropertyKey key = new PropertyKey(null, IdGenerator.of(1L), "k"); + UUID first = UUID.fromString("00000000-0000-0000-0000-000000000002"); + UUID second = UUID.fromString("00000000-0000-0000-0000-000000000001"); + BytesBuffer buffer = BytesBuffer.allocate(8); + buffer.writeProperty(Cardinality.LIST, DataType.TEXT, Arrays.asList("b", "a", "b")); + buffer.writeProperty(Cardinality.SET, DataType.UUID, + new LinkedHashSet<>(Arrays.asList(first, second))); + buffer.writeProperty(Cardinality.LIST, DataType.LONG, new ArrayList()); + buffer.forReadWritten(); + + Object list = buffer.readProperty(key); + Assert.assertTrue(list instanceof ArrayList); + Assert.assertEquals(Arrays.asList("b", "a", "b"), list); + Assert.assertEquals(Cardinality.LIST, key.cardinality()); + Assert.assertEquals(DataType.TEXT, key.dataType()); + + Object set = buffer.readProperty(key); + Assert.assertTrue(set instanceof LinkedHashSet); + Assert.assertEquals(Arrays.asList(first, second), new ArrayList<>((LinkedHashSet) set)); + Assert.assertEquals(DataType.UUID, key.dataType()); + + Assert.assertEquals(new ArrayList<>(), buffer.readProperty(key)); + Assert.assertEquals(DataType.LONG, key.dataType()); + Assert.assertEquals(0, buffer.remaining()); + } + + @Test + public void testSkipRejectsTruncatedOrNegativeLengths() { + PropertyKey text = new PropertyKey(null, IdGenerator.of(1L), "text"); + text.dataType(DataType.TEXT); + text.cardinality(Cardinality.SINGLE); + BytesBuffer truncated = BytesBuffer.allocate(8).writeVInt(10).writeStringRaw("abc"); + int start = truncated.forReadWritten().position(); + assertIllegalArgument(() -> truncated.skipSchemaProperty(text), "skipped byte length"); + Assert.assertTrue("Rejected skip must not move past the buffer", + truncated.position() <= start + 1); + + PropertyKey list = new PropertyKey(null, IdGenerator.of(2L), "list"); + list.dataType(DataType.INT); + list.cardinality(Cardinality.LIST); + BytesBuffer negative = BytesBuffer.allocate(8).writeVInt(-1).forReadWritten(); + assertIllegalArgument(() -> negative.skipSchemaProperty(list), "collection size"); + } + + @Test + public void testAllocatedBufferGrowsButWrappedBufferDoesNot() { + BytesBuffer buffer = BytesBuffer.allocate(2); + buffer.writeShort((short) 0x0102).writeLong(0x0304050607080910L).writeString("xyz"); + Assert.assertEquals(2 + 8 + 1 + 3, buffer.position()); + Assert.assertEquals("010203040506070809100378797a", Bytes.toHex(buffer.bytes())); + + BytesBuffer wrapped = BytesBuffer.wrap(new byte[3]); + wrapped.writeShort((short) 1); + Assert.assertThrows(IllegalStateException.class, () -> wrapped.writeShort((short) 2)); + Assert.assertEquals(2, wrapped.position()); + } + + private static BytesBuffer wrap(int... values) { + byte[] bytes = new byte[values.length]; + for (int i = 0; i < values.length; i++) { + bytes[i] = (byte) values[i]; + } + return BytesBuffer.wrap(bytes); + } + + private static void assertIllegalArgument(ThrowingRunnable runnable, String message) { + IllegalArgumentException e = Assert.assertThrows(IllegalArgumentException.class, runnable); + Assert.assertTrue("Unexpected message: " + e.getMessage(), + e.getMessage().contains(message)); + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/serializer/OlapKeyTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/serializer/OlapKeyTest.java new file mode 100644 index 0000000000..7fb4eaecd0 --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/serializer/OlapKeyTest.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.hugegraph.serializer; + +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.util.Bytes; +import org.junit.Assert; +import org.junit.Test; + +public class OlapKeyTest { + + @Test + public void testMergedKeyMatchesObservedRuntimeRequest() { + Assert.assertArrayEquals(Bytes.fromHex("080586313a616c696365"), + OlapKey.format(IdGenerator.of(5), IdGenerator.of("1:alice"))); + BytesBuffer key = BytesBuffer.wrap(OlapKey.format(IdGenerator.of(130), IdGenerator.of(1000))); + Assert.assertEquals(IdGenerator.of(130), key.readId()); + Assert.assertEquals(IdGenerator.of(1000), key.readId()); + Assert.assertEquals(0, key.remaining()); + } + + @Test + public void testValueIdentityUsesWholeVariableLengthPropertyId() { + byte[] observed = Bytes.fromHex("0563"); + Assert.assertTrue(OlapKey.matchesProperty(observed, IdGenerator.of(5))); + Assert.assertFalse(OlapKey.matchesProperty(observed, IdGenerator.of(8))); + byte[] largeProperty = BytesBuffer.allocate(16).writeVInt(130).writeVInt(99).bytes(); + Assert.assertTrue(OlapKey.matchesProperty(largeProperty, IdGenerator.of(130))); + Assert.assertFalse(OlapKey.matchesProperty(largeProperty, IdGenerator.of(1))); + Assert.assertFalse(OlapKey.matchesProperty(null, IdGenerator.of(5))); + Assert.assertFalse(OlapKey.matchesProperty(new byte[0], IdGenerator.of(5))); + Assert.assertFalse(OlapKey.matchesProperty(new byte[]{(byte) 0x81}, IdGenerator.of(5))); + Assert.assertFalse(OlapKey.matchesProperty(new byte[]{(byte) 0x80}, IdGenerator.of(5))); + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/EdgeLabelMapTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/EdgeLabelMapTest.java new file mode 100644 index 0000000000..1d1ca47931 --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/EdgeLabelMapTest.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.hugegraph.struct.schema; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; + +public class EdgeLabelMapTest { + + @Test + public void testSingleLinkMetadataRegardlessOfOrder() throws Exception { + // Exact field shapes emitted by the pre-consolidation Server JsonUtil. + Map wire = map("{\"id\":3,\"name\":\"knows\",\"links\":[{\"1\":2}]," + + "\"sourceLabel\":\"1\",\"targetLabel\":\"2\"}"); + EdgeLabel decoded = EdgeLabel.fromMap(wire, null); + Assert.assertEquals(Collections.singleton(Pair.of(IdGenerator.of(1), IdGenerator.of(2))), + decoded.links()); + Map endpointsFirst = new LinkedHashMap<>(); + endpointsFirst.put(EdgeLabel.P.SOURCE_LABEL, wire.get(EdgeLabel.P.SOURCE_LABEL)); + endpointsFirst.put(EdgeLabel.P.TARGET_LABEL, wire.get(EdgeLabel.P.TARGET_LABEL)); + endpointsFirst.putAll(wire); + Assert.assertEquals(decoded.links(), EdgeLabel.fromMap(endpointsFirst, null).links()); + } + + @Test + public void testLegacyEndpointsWithoutLinks() throws Exception { + Map wire = map("{\"id\":3,\"name\":\"knows\",\"sourceLabel\":\"1\"," + + "\"targetLabel\":\"2\"}"); + EdgeLabel decoded = EdgeLabel.fromMap(wire, null); + Assert.assertEquals(IdGenerator.of(1), decoded.sourceLabel()); + Assert.assertEquals(IdGenerator.of(2), decoded.targetLabel()); + Assert.assertEquals(1, decoded.links().size()); + } + + @Test + public void testMultiLinksAcceptConsistentRedundantEndpoints() throws Exception { + Map wire = map("{\"id\":3,\"name\":\"knows\",\"links\":[{\"1\":2},{\"2\":4}]," + + "\"sourceLabel\":\"1\",\"targetLabel\":\"2\"}"); + EdgeLabel decoded = EdgeLabel.fromMap(wire, null); + Assert.assertEquals(2, decoded.links().size()); + Assert.assertTrue(decoded.checkLinkEqual(IdGenerator.of(1), IdGenerator.of(2))); + Assert.assertTrue(decoded.checkLinkEqual(IdGenerator.of(2), IdGenerator.of(4))); + } + + @Test + public void testConflictingEndpointsRejected() throws Exception { + Map wire = map("{\"id\":3,\"name\":\"knows\",\"links\":[{\"1\":2}]," + + "\"sourceLabel\":\"1\",\"targetLabel\":\"4\"}"); + IllegalArgumentException error = Assert.assertThrows(IllegalArgumentException.class, + () -> EdgeLabel.fromMap(wire, null)); + Assert.assertTrue(error.getMessage().contains("conflict")); + } + + @Test + public void testIncompleteEndpointsRejected() throws Exception { + Map wire = map("{\"id\":3,\"name\":\"knows\",\"sourceLabel\":\"1\"}"); + Assert.assertThrows(IllegalArgumentException.class, () -> EdgeLabel.fromMap(wire, null)); + } + + @Test + public void testMalformedLinksRejected() throws Exception { + for (String value : new String[]{"null", "{}", "[{}]", "[{\"1\":null}]", + "[{\"invalid\":2}]"}) { + Map wire = map("{\"id\":3,\"name\":\"knows\",\"links\":" + value + "}"); + Assert.assertThrows(IllegalArgumentException.class, () -> EdgeLabel.fromMap(wire, null)); + } + } + + @SuppressWarnings("unchecked") + private static Map map(String json) throws Exception { + return new ObjectMapper().readValue(json, LinkedHashMap.class); + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/OrderedSchemaFieldsTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/OrderedSchemaFieldsTest.java new file mode 100644 index 0000000000..2937e182ec --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/OrderedSchemaFieldsTest.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.hugegraph.struct.schema; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.IdQuery; +import org.apache.hugegraph.structure.BaseEdge; +import org.apache.hugegraph.structure.BaseVertex; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; + +public class OrderedSchemaFieldsTest { + + @Test + public void testPrimaryKeyOrderDrivesVertexIdAndQuery() throws Exception { + VertexLabel label = VertexLabel.fromMap(map("{\"id\":1,\"name\":\"person\"," + + "\"idStrategy\":\"PRIMARY_KEY\"," + + "\"primaryKeys\":[2,17]}"), null); + Assert.assertEquals(Arrays.asList(id(2), id(17)), label.primaryKeys()); + BaseVertex vertex = new BaseVertex(null, label); + vertex.addProperty(key(17), "first"); + vertex.addProperty(key(2), "second"); + Assert.assertEquals(Arrays.asList("second", "first"), vertex.primaryValues()); + Assert.assertEquals("second!first", vertex.name()); + Id expected = IdGenerator.of("1:second!first"); + vertex.id(SplicingIdGenerator.instance().generate(vertex)); + Assert.assertEquals(expected, vertex.id()); + IdQuery query = new IdQuery(HugeType.VERTEX); + query.query(expected); + Assert.assertTrue(query.test(vertex)); + } + + @Test + public void testSortKeyOrderDrivesEdgeIdAndQuery() throws Exception { + EdgeLabel label = EdgeLabel.fromMap(map("{\"id\":3,\"name\":\"knows\"," + + "\"sortKeys\":[2,17],\"links\":[{\"1\":2}]}"), null); + Assert.assertEquals(Arrays.asList(id(2), id(17)), label.sortKeys()); + BaseEdge edge = new BaseEdge(label, true); + edge.addProperty(key(17), "first"); + edge.addProperty(key(2), "second"); + edge.vertices(true, new BaseVertex(id(1)), new BaseVertex(id(2))); + edge.assignId(); + Assert.assertEquals(Arrays.asList("second", "first"), edge.sortValues()); + Assert.assertEquals("second!first", ((EdgeId) edge.id()).sortValues()); + Assert.assertTrue(Condition.eq(HugeKeys.SORT_VALUES, "second!first").test(edge)); + Assert.assertFalse(Condition.eq(HugeKeys.SORT_VALUES, "first!second").test(edge)); + } + + @Test + public void testWriterAcceptedRepeatedKeysAreNotSilentlyDiscarded() throws Exception { + VertexLabel vertex = VertexLabel.fromMap(map("{\"id\":1,\"name\":\"person\"," + + "\"primaryKeys\":[2,17,2]}"), null); + EdgeLabel edge = EdgeLabel.fromMap(map("{\"id\":3,\"name\":\"knows\"," + + "\"sortKeys\":[2,17,2]}"), null); + List expected = Arrays.asList(id(2), id(17), id(2)); + Assert.assertEquals(expected, vertex.primaryKeys()); + Assert.assertEquals(expected, edge.sortKeys()); + } + + private static Id id(int value) { + return IdGenerator.of(value); + } + + private static PropertyKey key(int value) { + return new PropertyKey(null, id(value), "key" + value); + } + + @SuppressWarnings("unchecked") + private static Map map(String json) throws Exception { + return new ObjectMapper().readValue(json, Map.class); + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/PropertyKeyTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/PropertyKeyTest.java index d8441144f2..077594148d 100644 --- a/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/PropertyKeyTest.java +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/PropertyKeyTest.java @@ -17,19 +17,67 @@ package org.apache.hugegraph.struct.schema; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.Set; import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.structure.BaseEdge; +import org.apache.hugegraph.structure.BaseVertex; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.IdStrategy; import org.apache.hugegraph.util.DateUtil; +import org.apache.hugegraph.util.LongEncoding; import org.junit.Assert; import org.junit.Test; public class PropertyKeyTest { + @Test + public void testOffsetDateTimeNormalizedToDate() { + OffsetDateTime value = OffsetDateTime.parse("2026-05-14T10:11:12.345678+08:00"); + OffsetDateTime utc = value.withOffsetSameInstant(ZoneOffset.UTC); + Date expected = new Date(value.toInstant().toEpochMilli()); + PropertyKey propertyKey = new PropertyKey(null, IdGenerator.of(1), "joinDate"); + propertyKey.dataType(DataType.DATE); + + Object normalized = propertyKey.validValue(value); + Assert.assertEquals(expected, normalized); + Assert.assertEquals(expected, propertyKey.validValue(utc)); + Assert.assertEquals(345L, ((Date) normalized).getTime() % 1000L); + Assert.assertNull(DataType.TEXT.valueToDate(value)); + Assert.assertNull(DataType.DATE.valueToDate(value.toLocalDateTime())); + } + + @Test + public void testOffsetDateTimeSerializedAsDateNavigationKeys() { + OffsetDateTime value = OffsetDateTime.parse("2026-05-14T10:11:12.345678+08:00"); + Date expected = new Date(value.toInstant().toEpochMilli()); + PropertyKey propertyKey = new PropertyKey(null, IdGenerator.of(1), "joinDate"); + propertyKey.dataType(DataType.DATE); + Object encoded = LongEncoding.encodeNumber(expected); + + Assert.assertEquals(encoded, propertyKey.serialValue(value, true)); + Assert.assertEquals(expected.toString(), propertyKey.serialValue(value, false)); + + VertexLabel vertexLabel = new VertexLabel(null, IdGenerator.of(2), "person"); + vertexLabel.idStrategy(IdStrategy.PRIMARY_KEY); + vertexLabel.primaryKey(propertyKey.id()); + BaseVertex vertex = new BaseVertex(null, vertexLabel); + vertex.addProperty(propertyKey, value); + Assert.assertEquals(Collections.singletonList(encoded), vertex.primaryValues()); + + EdgeLabel edgeLabel = new EdgeLabel(null, IdGenerator.of(3), "joined"); + edgeLabel.sortKey(propertyKey.id()); + BaseEdge edge = new BaseEdge(null, edgeLabel); + edge.addProperty(propertyKey, value.withOffsetSameInstant(ZoneOffset.UTC)); + Assert.assertEquals(Collections.singletonList(encoded), edge.sortValues()); + } + @Test public void testDefaultValueNormalizedToDate() { // Userdata reloaded from JSON keeps ~default_value as a String; diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/SchemaElementTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/SchemaElementTest.java new file mode 100644 index 0000000000..97c965f808 --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/struct/schema/SchemaElementTest.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.hugegraph.struct.schema; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.type.define.SchemaStatus; +import org.apache.hugegraph.util.DateUtil; +import org.junit.Assert; +import org.junit.Test; + +public class SchemaElementTest { + + private static final String CREATE_TIME = "2026-05-14 10:11:12.345"; + + @Test + public void testCreateTimeNormalizationAtSharedBoundary() { + PropertyKey key = new PropertyKey(null, IdGenerator.of(1), "created"); + key.userdata(Userdata.CREATE_TIME, CREATE_TIME); + Assert.assertEquals(DateUtil.parse(CREATE_TIME), + key.userdata().get(Userdata.CREATE_TIME)); + + Date date = DateUtil.now(); + key.userdata(Userdata.CREATE_TIME, date); + Assert.assertSame(date, key.userdata().get(Userdata.CREATE_TIME)); + key.userdata(Userdata.CREATE_TIME, ""); + Assert.assertEquals("", key.userdata().get(Userdata.CREATE_TIME)); + key.userdata("note", CREATE_TIME); + Assert.assertEquals(CREATE_TIME, key.userdata().get("note")); + } + + @Test + public void testPersistedSchemaUserdataRestoresDate() { + Map userdata = new HashMap<>(); + userdata.put(Userdata.CREATE_TIME, CREATE_TIME); + userdata.put("count", 42); + Map serialized = new HashMap<>(); + serialized.put(VertexLabel.P.ID, 1); + serialized.put(VertexLabel.P.NAME, "person"); + serialized.put(VertexLabel.P.STATUS, SchemaStatus.CREATED.string()); + serialized.put(VertexLabel.P.USERDATA, userdata); + + VertexLabel label = VertexLabel.fromMap(serialized, null); + Assert.assertEquals(DateUtil.parse(CREATE_TIME), + label.userdata().get(Userdata.CREATE_TIME)); + Assert.assertEquals(42, label.userdata().get("count")); + Assert.assertEquals(SchemaStatus.CREATED, label.status()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidCreateTimeRejected() { + Userdata userdata = new Userdata(); + userdata.put(Userdata.CREATE_TIME, "not-a-date"); + } + + @Test(expected = IllegalArgumentException.class) + public void testNullBulkUserdataRejected() { + new PropertyKey(null, IdGenerator.of(1), "created").userdata((Userdata) null); + } + + @Test + public void testCopyPreservesIdentityAndStatus() { + PropertyKey key = new PropertyKey(null, IdGenerator.of(1), "created"); + key.userdata("note", "unchanged"); + key.status(SchemaStatus.CREATING); + SchemaElement copy = key.copy(); + Assert.assertNotSame(key, copy); + Assert.assertEquals(key, copy); + Assert.assertEquals(key.hashCode(), copy.hashCode()); + Assert.assertEquals(key.status(), copy.status()); + Assert.assertEquals(key.userdata(), copy.userdata()); + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/SharedElementTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/SharedElementTest.java new file mode 100644 index 0000000000..2cf661aa02 --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/SharedElementTest.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.hugegraph.structure; + +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.Userdata; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.IdStrategy; +import org.junit.Assert; +import org.junit.Test; + +public class SharedElementTest { + + @Test + public void testSingleUpdateCallbackSeesPreviousValue() { + BaseVertex vertex = vertex(1); + PropertyKey key = key(Cardinality.SINGLE); + vertex.addProperty(key, "before"); + AtomicBoolean notified = new AtomicBoolean(); + vertex.addProperty(key, "after", BaseProperty::new, property -> { + Assert.assertEquals("before", vertex.getPropertyValue(key.id())); + Assert.assertEquals("after", property.value()); + notified.set(true); + }, property -> Assert.fail("Single update notified after replacement")); + Assert.assertTrue(notified.get()); + Assert.assertEquals("after", vertex.getPropertyValue(key.id())); + } + + @Test + public void testCollectionUpdateSharesPropertyAndNotifiesAfterMutation() { + BaseVertex vertex = vertex(1); + PropertyKey key = key(Cardinality.SET); + BaseProperty property = vertex.addProperty(key, "first"); + vertex.addProperty(key, Arrays.asList("first", "second"), BaseProperty::new, + value -> Assert.fail("Collection invoked single callback"), value -> { + Assert.assertSame(property, value); + Assert.assertEquals(2, ((Collection) value.value()).size()); + }); + Assert.assertSame(property, vertex.getProperty(key.id())); + } + + @Test + public void testCloneStateAndCopyPropertyMapAreIndependent() throws Exception { + BaseVertex original = vertex(1); + PropertyKey key = key(Cardinality.SINGLE); + original.addProperty(key, "original"); + BaseVertex copy = (BaseVertex) original.clone(); + copy.copyProperties(original); + copy.removed(true); + copy.expiredTime(100); + copy.addProperty(key, "replacement"); + Assert.assertFalse(original.removed()); + Assert.assertEquals(0, original.expiredTime()); + Assert.assertEquals("original", original.getPropertyValue(key.id())); + Assert.assertEquals("replacement", copy.getPropertyValue(key.id())); + } + + @Test + public void testSwitchOwnerPreservesSharedPropertiesAndOriginalDirection() { + EdgeLabel label = new EdgeLabel(null, IdGenerator.of(3), "link"); + BaseVertex source = vertex(1); + BaseVertex target = vertex(2); + BaseEdge original = new BaseEdge(label, true); + original.vertices(true, source, target); + original.name(""); + original.assignId(); + PropertyKey key = key(Cardinality.SINGLE); + original.addProperty(key, "value"); + BaseEdge reverse = original.switchOwner(); + Assert.assertTrue(original.isOutEdge()); + Assert.assertFalse(reverse.isOutEdge()); + Assert.assertEquals(source.id(), original.ownerVertexId()); + Assert.assertEquals(target.id(), reverse.ownerVertexId()); + Assert.assertSame(original.getProperty(key.id()), reverse.getProperty(key.id())); + reverse.resetProperties(); + Assert.assertTrue(original.hasProperty(key.id())); + Assert.assertFalse(reverse.hasProperty(key.id())); + } + + @Test + public void testLazyPropertyReadsAreUsedByValueMapAndEncoding() { + BaseVertex vertex = vertex(1); + PropertyKey key = key(Cardinality.SINGLE); + BaseProperty lazy = new BaseProperty(key, null) { + @Override + public String value() { + return "loaded"; + } + }; + vertex.addProperty(lazy); + Assert.assertEquals("loaded", vertex.getPropertiesMap().get(key.id())); + Assert.assertEquals(key.serialValue("loaded", true), lazy.serialValue(true)); + Assert.assertTrue(Condition.eq(key.id(), "loaded").test(vertex)); + } + + @Test + public void testQueryDoesNotLoadPropertiesOrInsertDefaultsImplicitly() { + BaseVertex vertex = vertex(1); + PropertyKey key = key(Cardinality.SINGLE); + key.userdata(Userdata.DEFAULT_VALUE, "default"); + vertex.schemaLabel().property(key.id()); + vertex.propLoaded(false); + Assert.assertFalse(Condition.eq(key.id(), "default").test(vertex)); + Assert.assertFalse(vertex.propLoaded()); + Assert.assertFalse(vertex.defaultValueUpdated()); + + // Engine accessors explicitly load backend data before updating defaults. + vertex.propLoaded(true); + vertex.updateDefaultValues(id -> key); + Assert.assertTrue(Condition.eq(key.id(), "default").test(vertex)); + Assert.assertTrue(vertex.defaultValueUpdated()); + vertex.addProperty(key, "explicit"); + vertex.updateDefaultValues(id -> key); + Assert.assertEquals("explicit", vertex.getPropertyValue(key.id())); + } + + @Test + public void testFreshPropertyMapsCannotAffectOtherElements() throws Exception { + BaseVertex first = vertex(1); + BaseVertex second = vertex(2); + BaseVertex clonedEmpty = (BaseVertex) first.clone(); + PropertyKey key = key(Cardinality.SINGLE); + Assert.assertNull(first.removeProperty(key.id())); + first.properties().put(BaseElement.intFromId(key.id()), new BaseProperty<>(key, "first")); + Assert.assertEquals("first", first.getPropertyValue(key.id())); + Assert.assertFalse(second.hasProperties()); + Assert.assertFalse(clonedEmpty.hasProperties()); + second.properties().put(BaseElement.intFromId(key.id()), new BaseProperty<>(key, "second")); + first.properties().clear(); + Assert.assertEquals("second", second.getPropertyValue(key.id())); + Assert.assertFalse(clonedEmpty.hasProperties()); + BaseVertex copied = (BaseVertex) second.clone(); + copied.copyProperties(second); + copied.properties().clear(); + Assert.assertEquals("second", second.getPropertyValue(key.id())); + } + + @Test + public void testSharedPrimaryNameUsesReservedValueContract() { + PropertyKey key = key(Cardinality.SINGLE); + VertexLabel label = new VertexLabel(null, IdGenerator.of(10), "person"); + label.idStrategy(IdStrategy.PRIMARY_KEY); + label.primaryKeys(key.id()); + BaseVertex vertex = new BaseVertex(null, label); + vertex.addProperty(key, ""); + Assert.assertThrows(IllegalArgumentException.class, vertex::name); + for (char prefix = 0; prefix <= 3; prefix++) { + vertex.addProperty(key, prefix + "value"); + Assert.assertThrows(IllegalArgumentException.class, vertex::name); + } + vertex.addProperty(key, "a!b"); + Assert.assertEquals("a`!b", vertex.name()); + } + + @Test + public void testSharedEdgeNamePreservesSortValueNormalization() { + PropertyKey key = key(Cardinality.SINGLE); + EdgeLabel label = new EdgeLabel(null, IdGenerator.of(3), "link"); + BaseEdge edge = new BaseEdge(null, label); + Assert.assertEquals("", edge.name()); + label.sortKeys(key.id()); + edge.name(null); + edge.addProperty(key, ""); + Assert.assertEquals("", edge.name()); + edge.name(null); + edge.addProperty(key, "a!b"); + Assert.assertEquals("a`!b", edge.name()); + edge.name(null); + edge.addProperty(key, "\u0000value"); + Assert.assertThrows(IllegalArgumentException.class, edge::name); + } + + @Test + public void testStandaloneUnknownSchemaFactoryRequiresNoSupplier() { + BaseVertex owner = vertex(1); + EdgeLabel label = new EdgeLabel(null, IdGenerator.of(3), "link"); + label.sourceLabel(IdGenerator.of(10)); + label.targetLabel(IdGenerator.of(11)); + BaseEdge edge = BaseEdge.createEdge(null, owner, true, label, "sort", IdGenerator.of(2)); + Assert.assertSame(owner, edge.ownerVertex()); + Assert.assertTrue(owner.schemaLabel().undefined()); + Assert.assertTrue(edge.otherVertex().schemaLabel().undefined()); + Assert.assertEquals(IdGenerator.of(10), owner.schemaLabel().id()); + Assert.assertEquals(IdGenerator.of(11), edge.otherVertex().schemaLabel().id()); + Assert.assertEquals("sort", edge.name()); + Assert.assertEquals(IdGenerator.of(2), edge.otherVertexId()); + } + + @Test + public void testUniqueAdjacencyUsesSharedEdgeIdEquality() { + BaseVertex owner = vertex(1); + BaseVertex other = vertex(2); + EdgeLabel label = new EdgeLabel(null, IdGenerator.of(3), "link"); + owner.resetEdges(true); + BaseEdge first = new BaseEdge(label, true); + first.vertices(true, owner, other); + first.name(""); + first.assignId(); + BaseEdge duplicate = first.clone(); + Assert.assertTrue(owner.addEdge(first)); + Assert.assertFalse(owner.addEdge(duplicate)); + Assert.assertEquals(1, owner.edges().size()); + Assert.assertSame(first, owner.removeEdge(duplicate)); + Assert.assertTrue(owner.edges().isEmpty()); + } + + @Test + public void testClassificationPreservesStorageAndEngineContracts() { + VertexLabel variables = new VertexLabel(null, IdGenerator.of(10), "~variables"); + BaseVertex stored = new BaseVertex(IdGenerator.of(1), variables); + BaseVertex engine = new BaseVertex(IdGenerator.of(1), variables, + BaseVertex.TypeContext.ENGINE); + Assert.assertEquals(HugeType.TASK, stored.type()); + Assert.assertEquals(HugeType.VERTEX, engine.type()); + + VertexLabel server = new VertexLabel(null, IdGenerator.of(11), "~server"); + stored.schemaLabel(server); + engine.schemaLabel(server); + Assert.assertEquals(HugeType.VERTEX, stored.type()); + Assert.assertEquals(HugeType.SERVER, engine.type()); + } + + private static BaseVertex vertex(long value) { + Id id = IdGenerator.of(value); + return new BaseVertex(id, new VertexLabel(null, IdGenerator.of(10), "person")); + } + + private static PropertyKey key(Cardinality cardinality) { + PropertyKey key = new PropertyKey(null, IdGenerator.of(1), "name"); + key.dataType(DataType.TEXT); + key.cardinality(cardinality); + return key; + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/SharedIndexTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/SharedIndexTest.java new file mode 100644 index 0000000000..643fe4939e --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/SharedIndexTest.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.hugegraph.structure; + +import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.hugegraph.HugeGraphSupplier; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.IndexType; +import org.junit.Assert; +import org.junit.Test; + +public class SharedIndexTest { + + @Test + public void testLegacyIndexIdAndHashBytes() { + // Fixed Server bytes from compatibility/2f827d6e8 fixtures, not a + // round-trip assertion using the same writer and reader. + Assert.assertArrayEquals(new byte[]{(byte) 160, 0, 0, 0, 3, (byte) 128, 0, 0, 7}, + Index.formatIndexId(HugeType.RANGE_INT_INDEX, + id(3), 7).asBytes()); + Assert.assertArrayEquals(new byte[]{(byte) 162, 0, 0, 0, 3, + (byte) 128, 0, 0, 0, 0, 0, 0, 7}, + Index.formatIndexId(HugeType.RANGE_LONG_INDEX, + id(3), 7L).asBytes()); + // Frozen Server index-builder-contract.properties stores this hash. + Assert.assertArrayEquals("SI:F:224054a2".getBytes(StandardCharsets.UTF_8), + Index.formatIndexHashId(HugeType.SECONDARY_INDEX, id(15), + "long-field-".repeat(20)).asBytes()); + Assert.assertThrows(IllegalStateException.class, + () -> Index.formatIndexHashId(HugeType.RANGE_INT_INDEX, id(3), 7)); + } + + @Test + public void testLegacyRangeIdParsesWithSchemaType() { + IndexLabel label = label(null); + label.indexType(IndexType.RANGE_INT); + label.indexFields(id(2)); + HugeGraphSupplier graph = graph(label, 100); + Index parsed = Index.parseIndexId(graph, HugeType.RANGE_INT_INDEX, + new byte[]{(byte) 160, 0, 0, 0, 3, + (byte) 128, 0, 0, 7}); + Assert.assertSame(label, parsed.indexLabel()); + Assert.assertEquals(7, parsed.fieldValues()); + } + + @Test + public void testCloneResetKeepsEveryExpirationEntry() { + IndexLabel label = label(null); + Index original = new Index(graph(label, 100), label); + original.fieldValues("value"); + original.elementIds(id(1), 90); + original.elementIds(id(2), 95); + original.elementIds(id(3), 110); + Set expired = original.expiredElementIds(); + Assert.assertEquals(2, expired.size()); + + Index deletion = original.clone(); + Assert.assertEquals(original.id(), deletion.id()); + deletion.resetElementIds(); + for (Index.IdWithExpiredTime entry : expired) { + deletion.elementIds(entry.id(), entry.expiredTime()); + } + Assert.assertEquals(ids(1, 2), deletion.elementIds()); + Assert.assertEquals(ids(3), original.elementIds()); + Set copied = deletion.expiredElementIds(); + long[] times = copied.stream().mapToLong(Index.IdWithExpiredTime::expiredTime).toArray(); + Assert.assertArrayEquals(new long[]{90, 95}, times); + Assert.assertEquals(ids(3), original.elementIds()); + } + + @Test + public void testExpirationBoundaryAndTtl() { + IndexLabel label = label(null); + HugeGraphSupplier graph = graph(label, 100); + Index index = new Index(graph, label(graph)); + index.elementIds(id(1), 0); + index.elementIds(id(2), 99); + index.elementIds(id(3), 100); + index.elementIds(id(4), 101); + Assert.assertEquals(1, index.expiredElementIds().size()); + Assert.assertEquals(ids(1, 3, 4), index.elementIds()); + index.resetElementIds(); + index.elementIds(id(4), 101); + Assert.assertTrue(index.hasTtl()); + Assert.assertEquals(1, index.ttl()); + Assert.assertEquals(101, index.expiredTime()); + } + + @Test + public void testSystemIndexDoesNotAcquirePropertyTtl() { + HugeGraphSupplier graph = graph(label(null), 100); + Index index = new Index(graph, IndexLabel.label(HugeType.VERTEX)); + index.elementIds(id(1), 120); + Assert.assertFalse(index.hasTtl()); + Assert.assertEquals(120, index.expiredTime()); + } + + @Test + public void testWriteModeResetAndExpiration() { + IndexLabel label = label(null); + Index index = new Index(graph(label, 100), label, true); + index.elementIds(id(1), 99); + Assert.assertEquals(1, index.expiredElementIds().size()); + Assert.assertThrows(IllegalStateException.class, index::elementId); + index.elementIds(id(2), 150); + Index clone = index.clone(); + clone.resetElementIds(); + clone.elementIds(id(3), 160); + Assert.assertEquals(id(2), index.elementId()); + Assert.assertEquals(150, index.expiredTime()); + Assert.assertEquals(id(3), clone.elementId()); + Assert.assertEquals(160, clone.expiredTime()); + Assert.assertTrue(clone.expiredElementIds().isEmpty()); + } + + private static Id id(long value) { + return IdGenerator.of(value); + } + + private static Set ids(long... values) { + Set result = new LinkedHashSet<>(); + Arrays.stream(values).mapToObj(SharedIndexTest::id).forEach(result::add); + return result; + } + + private static IndexLabel label(HugeGraphSupplier graph) { + IndexLabel label = new IndexLabel(graph, id(3), "by_value"); + label.baseType(HugeType.VERTEX_LABEL); + label.baseValue(id(1)); + label.indexType(IndexType.SECONDARY); + return label; + } + + private static HugeGraphSupplier graph(IndexLabel label, long now) { + final HugeGraphSupplier[] holder = new HugeGraphSupplier[1]; + holder[0] = (HugeGraphSupplier) Proxy.newProxyInstance( + HugeGraphSupplier.class.getClassLoader(), new Class[]{HugeGraphSupplier.class}, + (proxy, method, args) -> { + switch (method.getName()) { + case "now": + return now; + case "indexLabel": + return label; + case "vertexLabel": + VertexLabel vertexLabel = new VertexLabel(holder[0], id(1), "person"); + vertexLabel.ttl(1000); + return vertexLabel; + case "propertyKey": + PropertyKey key = new PropertyKey(holder[0], id(2), "value"); + key.dataType(DataType.INT); + return key; + default: + throw new UnsupportedOperationException(method.getName()); + } + }); + return holder[0]; + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/builder/IndexBuilderTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/builder/IndexBuilderTest.java new file mode 100644 index 0000000000..d902019b29 --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/builder/IndexBuilderTest.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.hugegraph.structure.builder; + +import java.lang.reflect.Proxy; +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.atomic.AtomicInteger; + +import org.apache.hugegraph.HugeGraphSupplier; +import org.apache.hugegraph.analyzer.Analyzer; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.MatchedIndex; +import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.BaseEdge; +import org.apache.hugegraph.structure.BaseVertex; +import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.EdgeLabelType; +import org.apache.hugegraph.type.define.IndexType; +import org.apache.hugegraph.type.define.WriteType; +import org.junit.Assert; +import org.junit.Test; + +public class IndexBuilderTest { + + @Test + public void testSearchIncludesOriginalTextAndIgnoresReservedSymbols() { + IndexBuilder builder = new IndexBuilder(supplier(), text -> Set.of("alpha", "beta", "\u0001", "\u0002")); + Assert.assertEquals(Set.of("alpha", "beta", "alpha beta"), builder.segmentWords("alpha beta")); + } + + @Test + public void testExplicitSearchWordsBypassAnalyzer() { + AtomicInteger calls = new AtomicInteger(); + IndexBuilder builder = new IndexBuilder(supplier(), text -> { + calls.incrementAndGet(); + return Set.of("analyzed"); + }); + Assert.assertEquals(Set.of("alpha", "beta"), builder.segmentWords("(alpha|beta|alpha)")); + Assert.assertEquals(Set.of("alpha"), builder.segmentWords("(alpha)")); + Assert.assertEquals(Set.of(""), builder.segmentWords("()")); + Assert.assertEquals(0, calls.get()); + } + + @Test + public void testSearchExpansionForCollectionIncludesJoinedText() { + HugeGraphSupplier graph = supplier(); + PropertyKey property = property(graph, 1, DataType.TEXT, Cardinality.LIST); + BaseVertex vertex = vertex(graph, property); + vertex.addProperty(property, List.of("alpha", "beta")); + IndexLabel label = index(graph, 1, IndexType.SEARCH, property); + IndexBuilder builder = new IndexBuilder(graph, text -> Set.of("alpha", "beta")); + Set words = new HashSet<>(); + builder.forEachIndex(vertex, label, index -> words.add(index.fieldValues())); + Assert.assertEquals(Set.of("alpha", "beta", "alpha beta"), words); + Assert.assertEquals("alpha beta", IndexBuilder.propertyValueToString(List.of("alpha", "beta"))); + } + + @Test + public void testSearchPredicateUsesSharedAnalysisForScalarAndCollection() { + Analyzer analyzer = text -> Set.of(text.split(" ")); + Assert.assertTrue(IndexBuilder.searchPredicate(analyzer, "(alpha)").test("alpha beta")); + Assert.assertTrue(IndexBuilder.searchPredicate(analyzer, "(alpha|gamma)").test(List.of("beta", "gamma"))); + Assert.assertTrue(IndexBuilder.searchPredicate(analyzer, "alpha delta").test("alpha beta")); + Assert.assertFalse(IndexBuilder.searchPredicate(analyzer, "(gamma)").test("alpha beta")); + Assert.assertFalse(IndexBuilder.searchPredicate(analyzer, "(alpha)").test("alphabet")); + } + + @Test + public void testNullablePrefixAndUniqueNullExpansion() { + HugeGraphSupplier graph = supplier(); + PropertyKey first = property(graph, 1, DataType.TEXT, Cardinality.SINGLE); + PropertyKey missing = property(graph, 2, DataType.TEXT, Cardinality.SINGLE); + PropertyKey last = property(graph, 3, DataType.TEXT, Cardinality.SINGLE); + BaseVertex vertex = vertex(graph, first, missing, last); + vertex.schemaLabel().nullableKeys(missing.id()); + vertex.addProperty(first, "first"); + vertex.addProperty(last, "last"); + IndexBuilder builder = new IndexBuilder(graph, text -> Set.of(text)); + IndexLabel secondary = index(graph, 1, IndexType.SECONDARY, first, missing, last); + List indexes = builder.buildIndex(vertex, secondary); + Assert.assertEquals(1, indexes.size()); + Assert.assertEquals("first", indexes.get(0).fieldValues()); + IndexLabel unique = index(graph, 2, IndexType.UNIQUE, first, missing, last); + Assert.assertEquals("first!\u0001!last", builder.buildIndex(vertex, unique).get(0).fieldValues()); + } + + @Test + public void testCollectionSecondaryExpansionPreservesDuplicateValuesAndExpiry() { + HugeGraphSupplier graph = supplier(); + PropertyKey property = property(graph, 1, DataType.INT, Cardinality.LIST); + BaseVertex vertex = vertex(graph, property); + vertex.addProperty(property, List.of(7, -1, 7)); + vertex.expiredTime(123L); + IndexLabel label = index(graph, 1, IndexType.SECONDARY, property); + List indexes = new ArrayList<>(); + new IndexBuilder(graph, text -> Set.of(text)).forEachIndex(vertex, label, indexes::add); + Assert.assertEquals(3, indexes.size()); + Assert.assertEquals(indexes.get(0).fieldValues(), indexes.get(2).fieldValues()); + Assert.assertNotEquals(indexes.get(0).fieldValues(), indexes.get(1).fieldValues()); + for (Index index : indexes) { + Assert.assertEquals(vertex.id(), index.elementId()); + Assert.assertEquals(123L, index.expiredTime()); + } + } + + @Test + public void testStoreRebuildIncludesParentIndexesAndPreservesSelectorOrderAndDuplicates() { + Map labels = new HashMap<>(); + Map indexes = new HashMap<>(); + HugeGraphSupplier graph = supplier(labels, indexes); + PropertyKey inherited = property(graph, 1, DataType.TEXT, Cardinality.SINGLE); + PropertyKey own = property(graph, 2, DataType.TEXT, Cardinality.SINGLE); + EdgeLabel parent = new EdgeLabel(graph, IdGenerator.of(3), "parent"); + parent.enableLabelIndex(false); + EdgeLabel child = new EdgeLabel(graph, IdGenerator.of(4), "child"); + child.edgeLabelType(EdgeLabelType.SUB); + child.fatherId(parent.id()); + child.enableLabelIndex(false); + labels.put(parent.id(), parent); + IndexLabel parentIndex = index(graph, 11, IndexType.SECONDARY, inherited); + parentIndex.baseType(HugeType.EDGE_LABEL); + parentIndex.baseValue(parent.id()); + IndexLabel childIndex = index(graph, 12, IndexType.SECONDARY, own); + childIndex.baseType(HugeType.EDGE_LABEL); + childIndex.baseValue(child.id()); + indexes.put(parentIndex.id(), parentIndex); + indexes.put(childIndex.id(), childIndex); + parent.indexLabels(parentIndex.id()); + child.indexLabels(childIndex.id()); + BaseEdge edge = new BaseEdge(new EdgeId(IdGenerator.of(1), Directions.OUT, parent.id(), child.id(), + "", IdGenerator.of(2)), child); + edge.isOutEdge(true); + edge.addProperty(inherited, "parent value"); + edge.addProperty(own, "child value"); + IndexBuilder builder = new IndexBuilder(graph, text -> Set.of(text)); + List rebuilt = builder.buildEdgeIndex(edge); + Assert.assertEquals(2, rebuilt.size()); + Assert.assertEquals(childIndex.id(), rebuilt.get(0).indexLabelId()); + Assert.assertEquals("child value", rebuilt.get(0).fieldValues()); + Assert.assertEquals(parentIndex.id(), rebuilt.get(1).indexLabelId()); + Assert.assertEquals("parent value", rebuilt.get(1).fieldValues()); + + child.indexLabels(parentIndex.id()); + List selected = new ArrayList<>(); + IndexBuilder.forEachIndexLabelId(edge, labels::get, selected::add); + List expected = new ArrayList<>(child.indexLabels()); + expected.addAll(parent.indexLabels()); + Assert.assertEquals(expected, selected); + Assert.assertEquals(2L, selected.stream().filter(parentIndex.id()::equals).count()); + Assert.assertEquals(3, builder.buildEdgeIndex(edge).size()); + } + + @Test + public void testOlapSelectorUsesProvidedStagedCandidates() { + HugeGraphSupplier graph = supplier(); + PropertyKey property = property(graph, 1, DataType.INT, Cardinality.SINGLE); + property.writeType(WriteType.OLAP_COMMON); + PropertyKey unrelated = property(graph, 2, DataType.INT, Cardinality.SINGLE); + BaseVertex vertex = new BaseVertex(IdGenerator.of(1), VertexLabel.OLAP_VL); + vertex.addProperty(property, 42); + IndexLabel staged = index(graph, 11, IndexType.RANGE_INT, property); + IndexLabel other = index(graph, 12, IndexType.RANGE_INT, unrelated); + List selected = new ArrayList<>(); + // The supplier has no index list; the caller's transaction candidates are authoritative. + IndexBuilder.forEachOlapIndexLabelId(vertex, List.of(other, staged), selected::add); + Assert.assertEquals(List.of(staged.id()), selected); + } + + @Test + public void testOlapSelectorRejectsZeroOrMultiplePropertiesBeforeCandidateTraversal() { + HugeGraphSupplier graph = supplier(); + BaseVertex vertex = new BaseVertex(IdGenerator.of(1), VertexLabel.OLAP_VL); + AtomicInteger traversals = new AtomicInteger(); + Iterable candidates = () -> { + traversals.incrementAndGet(); + return List.of().iterator(); + }; + assertInvalidOlap(vertex, candidates); + vertex.addProperty(property(graph, 1, DataType.INT, Cardinality.SINGLE), 42); + vertex.addProperty(property(graph, 2, DataType.INT, Cardinality.SINGLE), 43); + assertInvalidOlap(vertex, candidates); + Assert.assertEquals(0, traversals.get()); + } + + private static void assertInvalidOlap(BaseVertex vertex, Iterable candidates) { + try { + IndexBuilder.forEachOlapIndexLabelId(vertex, candidates, id -> Assert.fail("Unexpected selected index")); + Assert.fail("Olap selector must require exactly one property"); + } catch (IllegalArgumentException expected) { + Assert.assertTrue(expected.getMessage().contains("Expect only 1 property")); + } + } + + @Test + public void testMatchedIndexRetainsEngineEqualityAndReadOnlyLabels() { + HugeGraphSupplier graph = supplier(); + VertexLabel first = new VertexLabel(graph, IdGenerator.of(1), "first"); + VertexLabel second = new VertexLabel(graph, IdGenerator.of(2), "second"); + PropertyKey property = property(graph, 1, DataType.TEXT, Cardinality.SINGLE); + IndexLabel search = index(graph, 1, IndexType.SEARCH, property); + MatchedIndex match = new MatchedIndex(first, Set.of(search)); + MatchedIndex equal = new MatchedIndex(second, Set.of(search)); + Assert.assertEquals(match, equal); + Assert.assertEquals(match.hashCode(), equal.hashCode()); + Assert.assertTrue(match.containsSearchIndex()); + try { + match.indexLabels().clear(); + Assert.fail("Matched labels must remain read-only"); + } catch (UnsupportedOperationException expected) { + Assert.assertEquals(Set.of(search), match.indexLabels()); + } + } + + private static HugeGraphSupplier supplier() { + return supplier(Map.of(), Map.of()); + } + + private static HugeGraphSupplier supplier(Map labels, Map indexes) { + return (HugeGraphSupplier) Proxy.newProxyInstance(HugeGraphSupplier.class.getClassLoader(), + new Class[]{HugeGraphSupplier.class}, (proxy, method, args) -> { + switch (method.getName()) { + case "name": + case "toString": + return "index-builder-test"; + case "now": + return 0L; + case "edgeLabel": + return labels.get(args[0]); + case "indexLabel": + return indexes.get(args[0]); + case "hashCode": + return System.identityHashCode(proxy); + case "equals": + return proxy == args[0]; + default: + return null; + } + }); + } + + private static PropertyKey property(HugeGraphSupplier graph, int value, DataType type, Cardinality cardinality) { + PropertyKey property = new PropertyKey(graph, IdGenerator.of(value), "property" + value); + property.dataType(type); + property.cardinality(cardinality); + return property; + } + + private static BaseVertex vertex(HugeGraphSupplier graph, PropertyKey... properties) { + VertexLabel label = new VertexLabel(graph, IdGenerator.of(1), "vertex"); + for (PropertyKey property : properties) { + label.properties(Set.of(property.id())); + } + return new BaseVertex(IdGenerator.of(1), label); + } + + private static IndexLabel index(HugeGraphSupplier graph, int value, IndexType type, PropertyKey... fields) { + IndexLabel label = new IndexLabel(graph, IdGenerator.of(value), "index" + value); + label.baseType(HugeType.VERTEX_LABEL); + label.baseValue(IdGenerator.of(1)); + label.indexType(type); + for (PropertyKey property : fields) { + label.indexField(property.id()); + } + return label; + } +} diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/type/SharedTypeCompatibilityTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/type/SharedTypeCompatibilityTest.java new file mode 100644 index 0000000000..a1f08ef3ea --- /dev/null +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/type/SharedTypeCompatibilityTest.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.hugegraph.type; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.EdgeLabelType; +import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.hugegraph.type.define.SchemaStatus; +import org.apache.hugegraph.type.define.SerialEnum; +import org.junit.Assert; +import org.junit.Test; + +public class SharedTypeCompatibilityTest { + + @Test + public void testStoredTypeAndSchemaCodesRemainReadable() { + Assert.assertEquals(HugeType.VERTEX, HugeType.fromCode((byte) 101)); + Assert.assertEquals(HugeType.EDGE_OUT, HugeType.fromCode((byte) 130)); + Assert.assertEquals(HugeType.EDGE_IN, HugeType.fromCode((byte) 140)); + Assert.assertEquals(HugeType.OLAP, HugeType.fromCode((byte) 106)); + Assert.assertEquals(HugeType.UNIQUE_INDEX, HugeType.fromCode((byte) 178)); + Assert.assertEquals(HugeType.SYS_SCHEMA, HugeType.fromCode((byte) 250)); + Assert.assertEquals(HugeType.MAX_TYPE, HugeType.fromCode((byte) 255)); + Assert.assertEquals((byte) 211, HugeKeys.SUB_LABEL.code()); + Assert.assertEquals((byte) 250, HugeKeys.AGGREGATE_PROPERTIES.code()); + Assert.assertEquals(EdgeLabelType.GENERAL, + SerialEnum.fromCode(EdgeLabelType.class, (byte) 4)); + Assert.assertEquals(SchemaStatus.CLEARING, + SerialEnum.fromCode(SchemaStatus.class, (byte) 7)); + Assert.assertEquals(Cardinality.SET, + SerialEnum.fromCode(Cardinality.class, (byte) 3)); + Assert.assertEquals(Directions.IN, + SerialEnum.fromCode(Directions.class, (byte) 2)); + } + + @Test + public void testLazyAndConcurrentEnumRegistration() throws Exception { + // This extension does not call register() in a static initializer. + Assert.assertEquals(ExtensionType.VALUE, + SerialEnum.fromCode(ExtensionType.class, (byte) 42)); + ExecutorService executor = Executors.newFixedThreadPool(4); + try { + List> tasks = new ArrayList<>(); + for (int thread = 0; thread < 4; thread++) { + tasks.add(() -> { + for (int iteration = 0; iteration < 1000; iteration++) { + SerialEnum.register(Cardinality.class); + SerialEnum.register(ExtensionType.class); + Assert.assertEquals(Cardinality.LIST, + SerialEnum.fromCode(Cardinality.class, (byte) 2)); + Assert.assertEquals(ExtensionType.VALUE, + SerialEnum.fromCode(ExtensionType.class, (byte) 42)); + } + return null; + }); + } + List> results = executor.invokeAll(tasks, 10, TimeUnit.SECONDS); + for (Future result : results) { + Assert.assertFalse("Enum registration timed out", result.isCancelled()); + result.get(); + } + } finally { + executor.shutdownNow(); + } + } + + public enum ExtensionType implements SerialEnum { + VALUE; + + @Override + public byte code() { + return 42; + } + } +} diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/RedirectFilterDynamicFeature.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/util/UnsafeUtilTest.java similarity index 54% rename from hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/RedirectFilterDynamicFeature.java rename to hugegraph-struct/src/test/java/org/apache/hugegraph/util/UnsafeUtilTest.java index 9a90084dde..b948ea89b3 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/RedirectFilterDynamicFeature.java +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/util/UnsafeUtilTest.java @@ -15,21 +15,30 @@ * limitations under the License. */ -package org.apache.hugegraph.api.filter; +package org.apache.hugegraph.util; -import jakarta.ws.rs.Priorities; -import jakarta.ws.rs.container.DynamicFeature; -import jakarta.ws.rs.container.ResourceInfo; -import jakarta.ws.rs.core.FeatureContext; -import jakarta.ws.rs.ext.Provider; +import org.apache.hugegraph.util.collection.IntSet; +import org.junit.Assert; +import org.junit.Test; -@Provider -public class RedirectFilterDynamicFeature implements DynamicFeature { +import sun.misc.Unsafe; - @Override - public void configure(ResourceInfo resourceInfo, FeatureContext context) { - if (resourceInfo.getResourceMethod().isAnnotationPresent(RedirectFilter.RedirectMasterRole.class)) { - context.register(RedirectFilter.class, Priorities.USER + 1); +public class UnsafeUtilTest { + + @Test + public void testCollectionsShareUnsafeInstance() { + Assert.assertSame(UnsafeUtil.getUnsafe(), IntSet.UNSAFE); + } + + @Test + public void testNativeMemoryReadWrite() { + Unsafe unsafe = UnsafeUtil.getUnsafe(); + long address = unsafe.allocateMemory(Long.BYTES); + try { + unsafe.putLong(address, 0x0123456789abcdefL); + Assert.assertEquals(0x0123456789abcdefL, unsafe.getLong(address)); + } finally { + unsafe.freeMemory(address); } } } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NotAllowException.java b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/CanonicalRelationEnum.java.txt similarity index 66% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NotAllowException.java rename to hugegraph-struct/src/test/resources/compatibility/2f827d6e8/CanonicalRelationEnum.java.txt index 24bb495d7a..deba9a81f2 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/exception/NotAllowException.java +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/CanonicalRelationEnum.java.txt @@ -14,20 +14,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package org.apache.hugegraph.exception; - -import org.apache.hugegraph.HugeException; - -public class NotAllowException extends HugeException { - - private static final long serialVersionUID = -1407924451828873200L; - - public NotAllowException(String message) { - super(message); - } - - public NotAllowException(String message, Object... args) { - super(message, args); - } -} +package org.apache.hugegraph.query; public class Condition { public enum RelationType {EQ,GT,GTE,LT,LTE,NEQ,IN,NOT_IN,PREFIX,TEXT_ANALYZER_CONTAINS,TEXT_CONTAINS,TEXT_MATCH_REGEX,TEXT_MATCH_EDIT_DISTANCE,TEXT_NOT_CONTAINS,TEXT_PREFIX,TEXT_NOT_PREFIX,TEXT_SUFFIX,TEXT_NOT_SUFFIX,TEXT_CONTAINS_ANY,CONTAINS,CONTAINS_VALUE,CONTAINS_KEY,TEXT_CONTAINS_FUZZY,TEXT_FUZZY,TEXT_CONTAINS_REGEX,TEXT_REGEX,SCAN} } diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyCodecFixtureWriter.java.txt b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyCodecFixtureWriter.java.txt new file mode 100644 index 0000000000..f53b5b36af --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyCodecFixtureWriter.java.txt @@ -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 at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.UUID; +import org.apache.hugegraph.backend.id.EdgeId; +import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.backend.query.Aggregate.AggregateFunc; +import org.apache.hugegraph.backend.query.Condition; +import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.backend.serializer.BinaryBackendEntry.BinaryId; +import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.HugeKeys; + +public class LegacyCodecFixtureWriter { + private static final String LICENSE = + "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"; + private static final Map HEX = new LinkedHashMap<>(); + + public static void main(String[] args) throws Exception { + Path output = Path.of(args[0]); + Files.createDirectories(output); + System.out.println("Writer loaded from " + BytesBuffer.class.getProtectionDomain() + .getCodeSource().getLocation()); + long[] values = {Long.MIN_VALUE, -16385L, -16384L, -129L, -128L, -1L, + 0L, 1L, 127L, 128L, 16383L, 16384L, Long.MAX_VALUE}; + for (long value : values) { + put("id.long." + value, BytesBuffer.allocate(64).writeId(IdGenerator.of(value))); + put("vlong." + value, BytesBuffer.allocate(64).writeVLong(value)); + } + int[] ints = {Integer.MIN_VALUE, -129, -1, 0, 127, 128, 16383, 16384, + Integer.MAX_VALUE}; + for (int value : ints) { + put("vint." + value, BytesBuffer.allocate(64).writeVInt(value)); + } + put("id.text", BytesBuffer.allocate(64).writeId(IdGenerator.of("alice"))); + put("id.unicode", BytesBuffer.allocate(64).writeId(IdGenerator.of("顶点😀"))); + for (int length : new int[]{1, 64, 65, 256, 257, 16384}) { + put("id.text.length." + length, + BytesBuffer.allocate(64).writeId(IdGenerator.of("x".repeat(length)))); + } + put("id.uuid", BytesBuffer.allocate(64).writeId(IdGenerator.of( + UUID.fromString("01234567-89ab-cdef-0123-456789abcdef")))); + Id edge = new EdgeId(IdGenerator.of("alice"), Directions.OUT, IdGenerator.of(3L), + IdGenerator.of(4L), "2026!x", IdGenerator.of(99L)); + put("edge.out", BytesBuffer.allocate(128).writeEdgeId(edge)); + edge = new EdgeId(IdGenerator.of(99L), Directions.IN, IdGenerator.of(3L), + IdGenerator.of(4L), "2026!x", IdGenerator.of("alice")); + put("edge.in", BytesBuffer.allocate(128).writeEdgeId(edge)); + scalar("BOOLEAN", DataType.BOOLEAN, true); + scalar("BYTE", DataType.BYTE, (byte) -7); + scalar("INT", DataType.INT, -123456); + scalar("LONG", DataType.LONG, Long.MIN_VALUE); + scalar("FLOAT", DataType.FLOAT, 1.25f); + scalar("DOUBLE", DataType.DOUBLE, -123.125d); + scalar("DATE", DataType.DATE, new Date(1609459200123L)); + scalar("TEXT", DataType.TEXT, "属性😀\u0000"); + scalar("BLOB", DataType.BLOB, new byte[]{0, -1, 1, 127, -128}); + scalar("UUID", DataType.UUID, + UUID.fromString("01234567-89ab-cdef-0123-456789abcdef")); + scalar("OBJECT", DataType.OBJECT, new ArrayList<>(Arrays.asList("legacy", 7, true))); + scalar("UNKNOWN", DataType.UNKNOWN, "legacy-object"); + scalar("OBJECT_UNSUPPORTED_COLLECTION", DataType.OBJECT, Arrays.asList("legacy", 7, true)); + PropertyKey propertyKey = new PropertyKey(null, IdGenerator.of(101L), "legacy"); + propertyKey.dataType(DataType.INT); + schemaProperty("single", propertyKey, Cardinality.SINGLE, 7); + schemaProperty("list", propertyKey, Cardinality.LIST, Arrays.asList(7, -1, 7)); + schemaProperty("set", propertyKey, Cardinality.SET, + new LinkedHashSet<>(Arrays.asList(7, -1))); + byte[] stringIndex = "label!alice".getBytes(StandardCharsets.UTF_8); + put("index.string", BytesBuffer.allocate(64).writeIndexId( + new BinaryId(stringIndex, null), HugeType.SECONDARY_INDEX)); + byte[] intIndex = {(byte) 160, 0, 0, 0, 3, (byte) 128, 0, 0, 7}; + put("index.int", BytesBuffer.allocate(64).writeIndexId( + new BinaryId(intIndex, null), HugeType.RANGE_INT_INDEX)); + byte[] longIndex = {(byte) 162, 0, 0, 0, 3, (byte) 128, 0, 0, 0, 0, 0, 0, 7}; + put("index.long", BytesBuffer.allocate(64).writeIndexId( + new BinaryId(longIndex, null), HugeType.RANGE_LONG_INDEX)); + StringBuilder encoded = new StringBuilder("# " + LICENSE.replace("\n", "\n# ") + "\n"); + for (Map.Entry fixture : HEX.entrySet()) { + String line = fixture.getKey() + "=" + fixture.getValue(); + for (int start = 0; start < line.length(); start += 100) { + int end = Math.min(start + 100, line.length()); + encoded.append(line, start, end); + if (end < line.length()) { + encoded.append('\\'); + } + encoded.append('\n'); + } + } + Files.writeString(output.resolve("core-codecs.hex"), encoded.toString().replaceAll("(?m)[ \t]+$", ""), + StandardCharsets.UTF_8); + + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.eq(HugeKeys.LABEL, IdGenerator.of(3L)); + query.query(Condition.gte(IdGenerator.of(11L), 18)); + query.query(Condition.in(IdGenerator.of(12L), Arrays.asList(20L, 30L))); + query.query(Condition.eq(IdGenerator.of(13L), new Date(1609459200123L))); + query.query(Condition.or(Condition.eq(IdGenerator.of(14L), "alice"), + Condition.eq(IdGenerator.of(14L), "bob"))); + query.offset(7); + query.limit(19); + writeQuery(output, "core-query.json.txt", query); + ConditionQuery edgeQuery = new ConditionQuery(HugeType.EDGE); + edgeQuery.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of("alice")); + edgeQuery.eq(HugeKeys.DIRECTION, Directions.OUT); + edgeQuery.eq(HugeKeys.LABEL, IdGenerator.of(3L)); + edgeQuery.page("AQIDBA=="); + edgeQuery.limit(5); + writeQuery(output, "core-edge-query.json.txt", edgeQuery); + ConditionQuery countQuery = new ConditionQuery(HugeType.VERTEX); + countQuery.aggregate(AggregateFunc.COUNT, null); + writeQuery(output, "core-count-query.json.txt", countQuery); + ConditionQuery avgQuery = new ConditionQuery(HugeType.VERTEX); + avgQuery.eq(HugeKeys.LABEL, IdGenerator.of(3L)); + avgQuery.aggregate(AggregateFunc.AVG, "age"); + writeQuery(output, "core-avg-query.json.txt", avgQuery); + System.out.println("Generated " + HEX.size() + " byte fixtures and 4 query fixtures"); + } + + private static void writeQuery(Path output, String name, ConditionQuery query) + throws Exception { + String prolog = "/*\n" + LICENSE + "*/\n"; + String payload = new String(query.bytes(), StandardCharsets.UTF_8); + Files.writeString(output.resolve(name), prolog + payload + "\n", StandardCharsets.UTF_8); + } + + private static void schemaProperty(String name, PropertyKey key, + Cardinality cardinality, Object value) { + key.cardinality(cardinality); + put("schema." + name, BytesBuffer.allocate(64).writeProperty(key, value)); + } + + private static void scalar(String name, DataType type, Object value) { + BytesBuffer buffer = BytesBuffer.allocate(128); + buffer.writeProperty(type, value); + put("property." + name, buffer); + } + + private static void put(String name, BytesBuffer buffer) { + StringBuilder hex = new StringBuilder(); + for (byte value : buffer.bytes()) { + hex.append(String.format("%02x", value & 0xff)); + } + HEX.put(name, hex.toString()); + } +} diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyKryoFixtureWriter.java.txt b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyKryoFixtureWriter.java.txt new file mode 100644 index 0000000000..d2e75af556 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyKryoFixtureWriter.java.txt @@ -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. + */ +import java.lang.reflect.Array; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; +import org.apache.hugegraph.backend.id.EdgeId; +import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.backend.query.Aggregate; +import org.apache.hugegraph.backend.query.Condition; +import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.backend.serializer.BinaryBackendEntry.BinaryId; +import org.apache.hugegraph.backend.serializer.BytesBuffer; +import org.apache.hugegraph.backend.store.Shard; +import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.schema.IndexLabel; +import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.schema.Userdata; +import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.structure.GraphType; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.Directions; + +public class LegacyKryoFixtureWriter { + private static final Map OUTPUT = new LinkedHashMap<>(); + + public static void main(String[] args) throws Exception { + System.out.println("Kryo fixture writer loaded from " + BytesBuffer.class + .getProtectionDomain().getCodeSource().getLocation()); + Id longId = IdGenerator.of(7L); + Id stringId = IdGenerator.of("legacy-id"); + Id uuidId = IdGenerator.of(UUID.fromString("01234567-89ab-cdef-0123-456789abcdef")); + Id edgeId = new EdgeId(longId, Directions.OUT, IdGenerator.of(3L), + IdGenerator.of(4L), "sort", stringId); + Object[] instances = {longId, stringId, uuidId, edgeId, + new BinaryId(new byte[]{1, 2, 3}, longId), + new Shard("start", "end", 12L), + new PropertyKey(null, IdGenerator.of(11L), "age"), + new VertexLabel(null, IdGenerator.of(12L), "person"), + new EdgeLabel(null, IdGenerator.of(13L), "knows"), + new IndexLabel(null, IdGenerator.of(14L), "by-age")}; + String[] names = {"long_id", "string_id", "uuid_id", "edge_id", "binary_id", "shard", + "property_key", "vertex_label", "edge_label", "index_label"}; + for (int i = 0; i < names.length; i++) { + capture(names[i], instances[i]); + } + capture("aggregate_func", Aggregate.AggregateFunc.AVG); + capture("relation_type", Condition.RelationType.GTE); + capture("condition_type", Condition.ConditionType.OR); + capture("query_order", Query.Order.DESC); + capture("query_order_type", Query.OrderType.ORDER_STRICT); + capture("optimized_type", ConditionQuery.OptimizedType.INDEX_FILTER); + capture("id_type", Id.IdType.UUID); + capture("graph_type_class", GraphType.class); + capture("id_array", new Id[]{null}); + capture("id_matrix", new Id[][]{new Id[]{null}, new Id[0]}); + capture("shard_array", new Shard[]{null}); + capture("schema_array", new PropertyKey[]{null}); + Userdata userdata = new Userdata(); + userdata.put("tag", "legacy"); + userdata.put("count", 7); + userdata.put(Userdata.CREATE_TIME, new Date(1609459200123L)); + capture("userdata", userdata); + BackendColumn column = BackendColumn.of(new byte[]{1, 2}, new byte[]{3, 4}); + capture("backend_column", column); + capture("backend_column_array", new BackendColumn[]{column, null}); + capture("backend_column_class", BackendColumn.class); + ArrayList> classes = new ArrayList<>(); + for (Object instance : instances) { + classes.add(instance.getClass()); + } + capture("class_list", classes); + String[] contracts = {"org.apache.hugegraph.type.Nameable", + "org.apache.hugegraph.type.Typeable", + "org.apache.hugegraph.type.Indexable", + "org.apache.hugegraph.type.Propertiable", + "org.apache.hugegraph.backend.id.IdUtil", + "org.apache.hugegraph.backend.id.SplicingIdGenerator", + "org.apache.hugegraph.schema.builder.SchemaBuilder", + "org.apache.hugegraph.backend.query.serializer.AbstractSerializerAdapter", + "org.apache.hugegraph.backend.query.serializer.QueryAdapter", + "org.apache.hugegraph.backend.query.serializer.QueryIdAdapter"}; + ArrayList> contractClasses = new ArrayList<>(); + for (String contract : contracts) { + contractClasses.add(Class.forName(contract)); + } + capture("contract_classes", contractClasses); + String[] schemaNames = {"org.apache.hugegraph.schema.SchemaElement$TaskWithSchema", + "org.apache.hugegraph.schema.PropertyKey$P", + "org.apache.hugegraph.schema.VertexLabel$P", + "org.apache.hugegraph.schema.EdgeLabel$P", + "org.apache.hugegraph.schema.IndexLabel$P", + "org.apache.hugegraph.schema.SchemaElement$P", + "org.apache.hugegraph.schema.PropertyKey$Builder", + "org.apache.hugegraph.schema.VertexLabel$Builder", + "org.apache.hugegraph.schema.EdgeLabel$Builder", + "org.apache.hugegraph.schema.IndexLabel$Builder"}; + ArrayList> schemaClasses = new ArrayList<>(); + ArrayList schemaArrays = new ArrayList<>(); + for (String schemaName : schemaNames) { + Class schemaClass = Class.forName(schemaName); + schemaClasses.add(schemaClass); + schemaArrays.add(Array.newInstance(schemaClass, 1)); + } + capture("schema_nested_classes", schemaClasses); + capture("schema_nested_arrays", schemaArrays); + capture("index_classes", new ArrayList<>(Arrays.asList( + org.apache.hugegraph.structure.HugeIndex.class, + org.apache.hugegraph.structure.HugeIndex.IdWithExpiredTime.class))); + capture("index_array", new org.apache.hugegraph.structure.HugeIndex[]{null}); + capture("expiry_id_array", new org.apache.hugegraph.structure.HugeIndex.IdWithExpiredTime[]{null}); + capture("expiry_id", new org.apache.hugegraph.structure.HugeIndex.IdWithExpiredTime(longId, + 1609459260000L)); + Map nested = new LinkedHashMap<>(); + nested.put("classes", classes); + nested.put("metadata", userdata); + nested.put("ids", new Id[][]{new Id[]{null}, new Id[0]}); + nested.put("predicate", Condition.RelationType.GTE); + nested.put("column", column); + capture("nested", nested); + capture("unsupported_arrays_list", Arrays.asList("legacy", 7, true)); + String source = Files.readString(Path.of(args[1]), StandardCharsets.UTF_8); + String license = source.substring(0, source.indexOf("*/") + 2); + StringBuilder text = new StringBuilder(license.replace("/*", "#") + .replace("*/", "#").replace(" *", "#")).append('\n'); + OUTPUT.forEach((key, value) -> append(text, key + "=" + value)); + Files.writeString(Path.of(args[0]).resolve("core-kryo-objects.properties"), text, + StandardCharsets.UTF_8); + } + + private static void capture(String name, Object value) { + BytesBuffer buffer = BytesBuffer.allocate(128); + buffer.writeProperty(DataType.OBJECT, value); + OUTPUT.put(name + ".hex", hex(buffer.bytes())); + try { + Object decoded = BytesBuffer.wrap(buffer.bytes()).readProperty(DataType.OBJECT); + OUTPUT.put(name + ".baseline_readable", "true"); + OUTPUT.put(name + ".baseline_type", decoded.getClass().getName()); + System.out.println(name + ": baseline readable " + decoded.getClass().getName()); + } catch (RuntimeException error) { + OUTPUT.put(name + ".baseline_readable", "false"); + OUTPUT.put(name + ".baseline_error", error.getClass().getName() + ": " + + error.getMessage().replace('\n', ' ')); + System.out.println(name + ": baseline fails " + error.getMessage().split("\\n")[0]); + } + } + + private static void append(StringBuilder output, String line) { + for (int start = 0; start < line.length(); start += 100) { + int end = Math.min(start + 100, line.length()); + output.append(line, start, end); + if (end < line.length()) { + output.append('\\'); + } + output.append('\n'); + } + } + + private static String hex(byte[] bytes) { + StringBuilder text = new StringBuilder(); + for (byte value : bytes) { + text.append(String.format("%02x", value & 0xff)); + } + return text.toString(); + } +} diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/RoleElectionStateMachine.java b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyRelationEnum.java.txt similarity index 79% rename from hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/RoleElectionStateMachine.java rename to hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyRelationEnum.java.txt index 4d60300d32..366bb30d7e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/masterelection/RoleElectionStateMachine.java +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyRelationEnum.java.txt @@ -14,12 +14,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package org.apache.hugegraph.masterelection; - -public interface RoleElectionStateMachine { - - void shutdown(); - - void start(RoleListener callback); -} +package org.apache.hugegraph.backend.query; public class Condition { public enum RelationType {EQ,GT,GTE,LT,LTE,NEQ,IN,NOT_IN,PREFIX,TEXT_CONTAINS,TEXT_CONTAINS_ANY,CONTAINS,CONTAINS_VALUE,CONTAINS_KEY,SCAN} } diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyRowFixtureWriter.java.txt b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyRowFixtureWriter.java.txt new file mode 100644 index 0000000000..abf389945d --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyRowFixtureWriter.java.txt @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.backend.id.EdgeId; +import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.backend.serializer.BinarySerializer; +import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.hugegraph.backend.store.BackendEntry; +import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.schema.EdgeLabel; +import org.apache.hugegraph.schema.IndexLabel; +import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.schema.VertexLabel; +import org.apache.hugegraph.structure.HugeEdge; +import org.apache.hugegraph.structure.HugeElement; +import org.apache.hugegraph.structure.HugeIndex; +import org.apache.hugegraph.structure.HugeVertex; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.Frequency; +import org.apache.hugegraph.type.define.IndexType; +import org.apache.hugegraph.type.define.WriteType; + +public class LegacyRowFixtureWriter { + private static final long NOW = 1609459200000L; + private static final long EXPIRY = NOW + 60000L; + private static final Map PROPERTIES = new HashMap<>(); + private static final Map VERTICES = new HashMap<>(); + private static final Map EDGES = new HashMap<>(); + private static final Map INDEXES = new HashMap<>(); + private static HugeGraph fixtureGraph; + private static FixtureSerializer fixtureSerializer; + private static final Map ROWS = new LinkedHashMap<>(); + + public static void main(String[] args) throws Exception { + HugeGraph graph = (HugeGraph) Proxy.newProxyInstance( + HugeGraph.class.getClassLoader(), new Class[]{HugeGraph.class}, + (proxy, method, values) -> { + switch (method.getName()) { + case "now": return NOW; + case "name": return "legacy-fixture"; + case "hugegraph": return proxy; + case "propertyKey": return PROPERTIES.get(((Id) values[0]).asLong()); + case "vertexLabel": + case "vertexLabelOrNone": return VERTICES.get(((Id) values[0]).asLong()); + case "edgeLabel": + case "edgeLabelOrNone": return EDGES.get(((Id) values[0]).asLong()); + case "indexLabel": return INDEXES.get(((Id) values[0]).asLong()); + default: throw new AssertionError("Unexpected graph call: " + method); + } + }); + fixtureGraph = graph; + PropertyKey single = property(graph, 1, "age", Cardinality.SINGLE); + PropertyKey list = property(graph, 2, "scores", Cardinality.LIST); + PropertyKey set = property(graph, 3, "groups", Cardinality.SET); + VertexLabel vertexLabel = new VertexLabel(graph, IdGenerator.of(4L), "person"); + vertexLabel.ttl(60000L); + vertexLabel.properties(single.id(), list.id(), set.id()); + VERTICES.put(4L, vertexLabel); + EdgeLabel edgeLabel = new EdgeLabel(graph, IdGenerator.of(5L), "knows"); + edgeLabel.ttl(60000L); + edgeLabel.frequency(Frequency.MULTIPLE); + edgeLabel.links(Pair.of(vertexLabel.id(), vertexLabel.id())); + edgeLabel.properties(single.id(), list.id(), set.id()); + EDGES.put(5L, edgeLabel); + HugeVertex source = new HugeVertex(graph, IdGenerator.of(1L), vertexLabel); + HugeVertex target = new HugeVertex(graph, IdGenerator.of(2L), vertexLabel); + fill(source, single, list, set); + source.expiredTime(EXPIRY); + FixtureSerializer serializer = new FixtureSerializer(); + fixtureSerializer = serializer; + System.out.println("Row writer loaded from " + BinarySerializer.class.getProtectionDomain() + .getCodeSource().getLocation()); + row("vertex", serializer.writeVertex(source)); + HugeEdge edge = new HugeEdge(graph, + new EdgeId(source.id(), Directions.OUT, edgeLabel.id(), edgeLabel.id(), + "2026!x", target.id()), edgeLabel); + edge.vertices(true, source, target); + fill(edge, single, list, set); + edge.expiredTime(EXPIRY); + row("edge.out", serializer.writeEdge(edge)); + row("edge.in", serializer.writeEdge(edge.switchOwner())); + PropertyKey olapKey = property(graph, 9, "rank", Cardinality.SINGLE); + olapKey.writeType(WriteType.OLAP_COMMON); + HugeVertex olap = new HugeVertex(graph, source.id(), VertexLabel.OLAP_VL); + olap.addProperty(olapKey, 42); + row("olap", serializer.writeVertex(olap)); + property(graph, 16L, "weight", Cardinality.SINGLE).dataType(DataType.LONG); + property(graph, 17L, "name", Cardinality.SINGLE).dataType(DataType.TEXT); + index("index.secondary", graph, serializer, 6L, IndexType.SECONDARY, "alice"); + index("index.int", graph, serializer, 7L, IndexType.RANGE_INT, 7); + index("index.long", graph, serializer, 8L, IndexType.RANGE_LONG, 7L); + VertexLabel timeless = new VertexLabel(graph, IdGenerator.of(10L), "timeless"); + timeless.properties(single.id(), list.id(), set.id()); + VERTICES.put(10L, timeless); + HugeVertex timelessVertex = new HugeVertex(graph, IdGenerator.of(3L), timeless); + fill(timelessVertex, single, list, set); + row("vertex.untimed", serializer.writeVertex(timelessVertex)); + EdgeLabel timelessEdgeLabel = new EdgeLabel(graph, IdGenerator.of(11L), "untimed"); + timelessEdgeLabel.links(Pair.of(vertexLabel.id(), vertexLabel.id())); + timelessEdgeLabel.properties(single.id(), list.id(), set.id()); + EDGES.put(11L, timelessEdgeLabel); + HugeEdge timelessEdge = new HugeEdge(graph, + new EdgeId(source.id(), Directions.OUT, timelessEdgeLabel.id(), timelessEdgeLabel.id(), + "", target.id()), timelessEdgeLabel); + timelessEdge.vertices(true, source, target); + fill(timelessEdge, single, list, set); + row("edge.untimed", serializer.writeEdge(timelessEdge)); + index("index.secondary.untimed", graph, serializer, 12L, + IndexType.SECONDARY, "alice", 10L); + index("index.int.untimed", graph, serializer, 13L, IndexType.RANGE_INT, 7, 10L); + index("index.long.untimed", graph, serializer, 14L, IndexType.RANGE_LONG, 7L, 10L); + index("index.hashed", graph, serializer, 15L, IndexType.SECONDARY, + "long-field-".repeat(20)); + HugeIndex systemIndex = new HugeIndex(graph, IndexLabel.label(HugeType.VERTEX)); + systemIndex.fieldValues(4L); + systemIndex.elementIds(source.id(), EXPIRY); + row("index.system", serializer.writeIndex(systemIndex)); + HugeIndex edgeSystemIndex = new HugeIndex(graph, IndexLabel.label(HugeType.EDGE)); + edgeSystemIndex.fieldValues(5L); + edgeSystemIndex.elementIds(edge.id(), EXPIRY); + row("index.system.edge", serializer.writeIndex(edgeSystemIndex)); + String producer = Files.readString(Path.of(args[1]), StandardCharsets.UTF_8); + String license = producer.substring(0, producer.indexOf("*/") + 2); + StringBuilder encoded = new StringBuilder(license.replace("/*", "#") + .replace("*/", "#").replace(" *", "#")).append('\n'); + encoded.append("now=").append(NOW).append('\n'); + encoded.append("expiry=").append(EXPIRY).append('\n'); + ROWS.forEach((key, value) -> encoded.append(key).append('=').append(value).append('\n')); + Files.createDirectories(Path.of(args[0])); + Files.writeString(Path.of(args[0]).resolve("core-rows.properties"), encoded, + StandardCharsets.UTF_8); + System.out.println("Generated 15 real legacy BinarySerializer rows"); + } + + private static PropertyKey property(HugeGraph graph, long id, String name, + Cardinality cardinality) { + PropertyKey key = new PropertyKey(graph, IdGenerator.of(id), name); + key.dataType(DataType.INT); + key.cardinality(cardinality); + PROPERTIES.put(id, key); + return key; + } + + private static void fill(HugeElement element, PropertyKey single, + PropertyKey list, PropertyKey set) { + element.addProperty(single, 7); + element.addProperty(list, Arrays.asList(7, -1, 7)); + element.addProperty(set, Arrays.asList(7, -1)); + } + + private static void index(String name, HugeGraph graph, BinarySerializer serializer, + long id, IndexType type, Object value) { + index(name, graph, serializer, id, type, value, 4L); + } + + private static void index(String name, HugeGraph graph, BinarySerializer serializer, + long id, IndexType type, Object value, long baseId) { + IndexLabel label = new IndexLabel(graph, IdGenerator.of(id), name); + label.indexType(type); + label.indexFields(IdGenerator.of(type == IndexType.RANGE_LONG ? 16L : + type == IndexType.SECONDARY ? 17L : 1L)); + label.baseType(HugeType.VERTEX_LABEL); + label.baseValue(IdGenerator.of(baseId)); + INDEXES.put(id, label); + HugeIndex index = new HugeIndex(graph, label); + index.fieldValues(value); + index.elementIds(IdGenerator.of(1L), baseId == 4L ? EXPIRY : 0L); + row(name, serializer.writeIndex(index)); + } + + private static void row(String name, BackendEntry entry) { + if (entry.type().isVertex()) { + HugeVertex decoded = fixtureSerializer.readVertex(fixtureGraph, entry); + if (decoded.sizeOfProperties() != 3 || + !Integer.valueOf(7).equals(decoded.getPropertyValue(IdGenerator.of(1L)))) { + throw new AssertionError("Legacy vertex self-read failed: " + name); + } + } else if (entry.type().isEdge()) { + HugeEdge decoded = fixtureSerializer.readEdge(fixtureGraph, entry); + if (decoded.sizeOfProperties() != 3 || + !Integer.valueOf(7).equals(decoded.getPropertyValue(IdGenerator.of(1L)))) { + throw new AssertionError("Legacy edge self-read failed: " + name); + } + } else if (entry.type() == HugeType.OLAP) { + HugeVertex decoded = new HugeVertex(fixtureGraph, IdGenerator.of(1L), VERTICES.get(4L)); + fixtureSerializer.parseOlap(entry.columns().iterator().next().value, decoded); + if (!Integer.valueOf(42).equals(decoded.getPropertyValue(IdGenerator.of(9L)))) { + throw new AssertionError("Legacy OLAP self-read failed"); + } + } else if (entry.type().isIndex()) { + ConditionQuery query = new ConditionQuery(entry.type()); + if (name.equals("index.secondary") || name.equals("index.secondary.untimed")) { + query.eq(HugeKeys.FIELD_VALUES, "alice"); + } else if (name.equals("index.hashed")) { + query.eq(HugeKeys.FIELD_VALUES, "long-field-".repeat(20)); + } else if (name.equals("index.system")) { + query.eq(HugeKeys.FIELD_VALUES, 4L); + } else if (name.equals("index.system.edge")) { + query.eq(HugeKeys.FIELD_VALUES, 5L); + } + HugeIndex decoded = fixtureSerializer.readIndex(fixtureGraph, query, entry); + Id expectedElement = name.equals("index.system.edge") ? + new EdgeId(IdGenerator.of(1L), Directions.OUT, + IdGenerator.of(5L), IdGenerator.of(5L), + "2026!x", IdGenerator.of(2L)) : IdGenerator.of(1L); + if (!decoded.elementIds().contains(expectedElement)) { + throw new AssertionError("Legacy index self-read failed: " + name); + } + } + ROWS.put(name + ".type", entry.type().name()); + ROWS.put(name + ".id", hex(entry.id().asBytes())); + ROWS.put(name + ".ttl", String.valueOf(entry.ttl())); + ROWS.put(name + ".columns", String.valueOf(entry.columnsSize())); + int number = 0; + for (BackendColumn column : entry.columns()) { + ROWS.put(name + ".column." + number + ".name", hex(column.name)); + ROWS.put(name + ".column." + number + ".value", hex(column.value)); + number++; + } + } + + private static class FixtureSerializer extends BinarySerializer { + private void parseOlap(byte[] bytes, HugeVertex vertex) { + super.parseVertexOlap(bytes, vertex); + } + } + + private static String hex(byte[] bytes) { + StringBuilder text = new StringBuilder(); + for (byte value : bytes) { + text.append(String.format("%02x", value & 0xff)); + } + return text.toString(); + } +} diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyServerIndexQueryProbe.java.txt b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyServerIndexQueryProbe.java.txt new file mode 100644 index 0000000000..e51eca9f31 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyServerIndexQueryProbe.java.txt @@ -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. + */ +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Collections; +import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.backend.query.IdPrefixQuery; +import org.apache.hugegraph.backend.serializer.BinarySerializer; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.HugeKeys; + +public class LegacyServerIndexQueryProbe { + public static void main(String[] args) throws Exception { + String input = "long-field-".repeat(20); + String value = ConditionQuery.concatValues(Collections.singletonList(input)); + ConditionQuery query = new ConditionQuery(HugeType.SECONDARY_INDEX); + query.eq(HugeKeys.INDEX_LABEL_ID, IdGenerator.of(15L)); + query.eq(HugeKeys.FIELD_VALUES, value); + IdPrefixQuery encoded = (IdPrefixQuery) new BinarySerializer().writeQuery(query); + String text = "server.query.field.length=" + value.length() + "\n" + + "server.query.prefix=" + hex(encoded.prefix().asBytes()) + "\n"; + Files.writeString(Path.of(args[0]).resolve("index-builder-contract.properties"), text, + StandardCharsets.UTF_8, StandardOpenOption.APPEND); + System.out.println("Original Server query: field " + value.length() + + " -> prefix " + hex(encoded.prefix().asBytes())); + } + private static String hex(byte[] bytes) { + StringBuilder text = new StringBuilder(); + for (byte value : bytes) { text.append(String.format("%02x", value & 0xff)); } + return text.toString(); + } +} diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyStoreIndexBuilderProbe.java.txt b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyStoreIndexBuilderProbe.java.txt new file mode 100644 index 0000000000..c4a3fbdc2c --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyStoreIndexBuilderProbe.java.txt @@ -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. + */ +import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import org.apache.hugegraph.HugeGraphSupplier; +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.serializer.BinaryElementSerializer; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.BaseVertex; +import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.structure.builder.IndexBuilder; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.IndexType; + +public class LegacyStoreIndexBuilderProbe { + public static void main(String[] args) throws Exception { + HugeConfig config = new HugeConfig(Collections.emptyMap()); + PropertyKey pkey = new PropertyKey(null, IdGenerator.of(17L), "name"); + pkey.dataType(DataType.TEXT); + VertexLabel vlabel = new VertexLabel(null, IdGenerator.of(4L), "person"); + vlabel.properties(pkey.id()); + vlabel.ttl(60000L); + HugeGraphSupplier graph = (HugeGraphSupplier) Proxy.newProxyInstance( + HugeGraphSupplier.class.getClassLoader(), new Class[]{HugeGraphSupplier.class}, + (proxy, method, values) -> { + switch (method.getName()) { + case "configuration": return config; + case "propertyKey": return pkey; + case "vertexLabel": + case "vertexLabelOrNone": return vlabel; + case "now": return 1609459200000L; + case "name": return "legacy-fixture"; + default: throw new AssertionError("Unexpected supplier call: " + method); + } + }); + IndexLabel ilabel = new IndexLabel(graph, IdGenerator.of(15L), "by-name"); + ilabel.indexType(IndexType.SECONDARY); + ilabel.baseType(HugeType.VERTEX_LABEL); + ilabel.baseValue(vlabel.id()); + ilabel.indexFields(pkey.id()); + String value = "long-field-".repeat(20); + BaseVertex vertex = new BaseVertex(IdGenerator.of(1L), vlabel); + vertex.addProperty(pkey, value); + vertex.expiredTime(1609459260000L); + IndexBuilder builder = new IndexBuilder(graph); + Index index = builder.buildIndex(vertex, ilabel).get(0); + BackendColumn column = new BinaryElementSerializer().writeIndex(index); + String producer = Files.readString(Path.of(args[1]), StandardCharsets.UTF_8); + String license = producer.substring(0, producer.indexOf("*/") + 2); + String text = license.replace("/*", "#").replace("*/", "#").replace(" *", "#") + "\n" + + "input.length=" + value.length() + "\n" + + "store.builder.field.length=" + index.fieldValues().toString().length() + "\n" + + "store.builder.field=" + index.fieldValues() + "\n" + + "store.builder.key=" + hex(column.name) + "\n" + + "store.builder.value=" + hex(column.value) + "\n"; + Files.writeString(Path.of(args[0]).resolve("index-builder-contract.properties"), text, + StandardCharsets.UTF_8); + System.out.println("Original Store IndexBuilder: input " + value.length() + + " -> field " + index.fieldValues()); + } + + private static String hex(byte[] bytes) { + StringBuilder text = new StringBuilder(); + for (byte value : bytes) { text.append(String.format("%02x", value & 0xff)); } + return text.toString(); + } +} diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyStoreIndexFixtureWriter.java.txt b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyStoreIndexFixtureWriter.java.txt new file mode 100644 index 0000000000..9a9a468c4d --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyStoreIndexFixtureWriter.java.txt @@ -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. + */ +import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import org.apache.hugegraph.HugeGraphSupplier; +import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.serializer.BinaryElementSerializer; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.IndexType; + +public class LegacyStoreIndexFixtureWriter { + private static final long EXPIRY = 1609459260000L; + private static final Map INDEXES = new HashMap<>(); + private static final Map PROPERTIES = new HashMap<>(); + private static final Map OUTPUT = new LinkedHashMap<>(); + + public static void main(String[] args) throws Exception { + VertexLabel label = new VertexLabel(null, IdGenerator.of(4L), "person"); + label.ttl(60000L); + HugeGraphSupplier graph = (HugeGraphSupplier) Proxy.newProxyInstance( + HugeGraphSupplier.class.getClassLoader(), new Class[]{HugeGraphSupplier.class}, + (proxy, method, values) -> { + switch (method.getName()) { + case "vertexLabel": + case "vertexLabelOrNone": return label; + case "indexLabel": return INDEXES.get(((Id) values[0]).asLong()); + case "propertyKey": return PROPERTIES.get(((Id) values[0]).asLong()); + case "now": return 1609459200000L; + case "name": return "legacy-fixture"; + default: throw new AssertionError("Unexpected supplier call: " + method); + } + }); + PropertyKey intKey = new PropertyKey(graph, IdGenerator.of(1L), "age"); + intKey.dataType(DataType.INT); + PROPERTIES.put(1L, intKey); + PropertyKey longKey = new PropertyKey(graph, IdGenerator.of(16L), "weight"); + longKey.dataType(DataType.LONG); + PROPERTIES.put(16L, longKey); + BinaryElementSerializer serializer = new BinaryElementSerializer(); + System.out.println("Store index writer loaded from " + BinaryElementSerializer.class + .getProtectionDomain().getCodeSource().getLocation()); + user("secondary", graph, serializer, 6L, IndexType.SECONDARY, "alice"); + user("int", graph, serializer, 7L, IndexType.RANGE_INT, 7); + user("long", graph, serializer, 8L, IndexType.RANGE_LONG, 7L); + Index system = new Index(graph, IndexLabel.label(HugeType.VERTEX), true); + system.fieldValues(4L); + system.elementIds(IdGenerator.of(1L), EXPIRY); + row("system", serializer, graph, system); + Index edgeSystem = new Index(graph, IndexLabel.label(HugeType.EDGE), true); + edgeSystem.fieldValues(5L); + edgeSystem.elementIds(new EdgeId(IdGenerator.of(1L), Directions.OUT, + IdGenerator.of(5L), IdGenerator.of(5L), "2026!x", IdGenerator.of(2L)), EXPIRY); + row("system.edge", serializer, graph, edgeSystem); + String source = Files.readString(Path.of(args[1]), StandardCharsets.UTF_8); + String license = source.substring(0, source.indexOf("*/") + 2); + StringBuilder text = new StringBuilder(license.replace("/*", "#") + .replace("*/", "#").replace(" *", "#")).append('\n'); + OUTPUT.forEach((key, value) -> text.append(key).append('=').append(value).append('\n')); + Files.writeString(Path.of(args[0]).resolve("store-index-rows.properties"), text, + StandardCharsets.UTF_8); + System.out.println("Generated 5 original Store TTL index rows"); + } + + private static void user(String name, HugeGraphSupplier graph, + BinaryElementSerializer serializer, long id, + IndexType type, Object value) { + IndexLabel label = new IndexLabel(graph, IdGenerator.of(id), "index-" + id); + label.baseType(HugeType.VERTEX_LABEL); + label.baseValue(IdGenerator.of(4L)); + label.indexType(type); + label.indexFields(IdGenerator.of(type == IndexType.RANGE_LONG ? 16L : 1L)); + INDEXES.put(id, label); + Index index = new Index(graph, label, true); + index.fieldValues(value); + index.elementIds(IdGenerator.of(1L), EXPIRY); + row(name, serializer, graph, index); + } + + private static void row(String name, BinaryElementSerializer serializer, + HugeGraphSupplier graph, Index index) { + BackendColumn column = serializer.writeIndex(index); + Index decoded = serializer.parseIndex(graph, column, null); + if (decoded.expiredTime() != EXPIRY) { + throw new AssertionError("Legacy Store reader TTL mismatch: " + name); + } + OUTPUT.put(name + ".name", hex(column.name)); + OUTPUT.put(name + ".value", hex(column.value)); + OUTPUT.put(name + ".expiry", String.valueOf(EXPIRY)); + } + + private static String hex(byte[] bytes) { + StringBuilder text = new StringBuilder(); + for (byte value : bytes) { + text.append(String.format("%02x", value & 0xff)); + } + return text.toString(); + } +} diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyStructFixtureWriter.java.txt b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyStructFixtureWriter.java.txt new file mode 100644 index 0000000000..02d42fe869 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/LegacyStructFixtureWriter.java.txt @@ -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. + */ +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.UUID; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.type.define.Cardinality; +import org.apache.hugegraph.type.define.DataType; + +public class LegacyStructFixtureWriter { + private static final StringBuilder OUTPUT = new StringBuilder(); + + public static void main(String[] args) throws Exception { + System.out.println("Writer loaded from " + BytesBuffer.class.getProtectionDomain() + .getCodeSource().getLocation()); + Path source = Path.of(args[1]); + String producer = Files.readString(source, StandardCharsets.UTF_8); + String license = producer.substring(0, producer.indexOf("*/") + 2); + OUTPUT.append(license.replace("/*", "#").replace("*/", "#") + .replace(" *", "#")).append('\n'); + scalar(DataType.BOOLEAN, true); + scalar(DataType.BYTE, (byte) -7); + scalar(DataType.INT, -123456); + scalar(DataType.LONG, Long.MIN_VALUE); + scalar(DataType.FLOAT, 1.25f); + scalar(DataType.DOUBLE, -123.125d); + scalar(DataType.DATE, new Date(1609459200123L)); + scalar(DataType.TEXT, "属性😀\u0000"); + scalar(DataType.BLOB, new byte[]{0, -1, 1, 127, -128}); + scalar(DataType.UUID, UUID.fromString("01234567-89ab-cdef-0123-456789abcdef")); + PropertyKey key = new PropertyKey(null, IdGenerator.of(101L), "legacy"); + key.dataType(DataType.INT); + key.cardinality(Cardinality.LIST); + put("list", BytesBuffer.allocate(64).writeProperty(key, Arrays.asList(7, -1, 7))); + key.cardinality(Cardinality.SET); + put("set", BytesBuffer.allocate(64).writeProperty( + key, new LinkedHashSet<>(Arrays.asList(7, -1)))); + Files.writeString(Path.of(args[0]).resolve("struct-tagged-properties.hex"), + OUTPUT, StandardCharsets.UTF_8); + System.out.println("Generated 12 tagged property fixtures"); + } + + private static void scalar(DataType type, Object value) { + put(type.name(), BytesBuffer.allocate(128).writeProperty(Cardinality.SINGLE, type, value)); + } + + private static void put(String key, BytesBuffer buffer) { + OUTPUT.append(key).append('='); + for (byte value : buffer.bytes()) { + OUTPUT.append(String.format("%02x", value & 0xff)); + } + OUTPUT.append('\n'); + } +} diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/README.md b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/README.md new file mode 100644 index 0000000000..c425ee6878 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/README.md @@ -0,0 +1,128 @@ + + +# Historical codec fixtures + +These files preserve historical payloads and the producer evidence needed to reproduce them. The `2f827d6e8` directory identifies the pre-consolidation source revision: [apache/hugegraph `2f827d6e8c9c62ae858f2fc122b3a192d015e2f4`](https://github.com/apache/hugegraph/commit/2f827d6e8c9c62ae858f2fc122b3a192d015e2f4). Core and Store/struct fixtures use their respective original implementations at that revision. Tests load the historical payloads described below without regenerating them with the current writer. The index-builder contract file records producer output; `SharedIndexTest` uses its captured Server hash as an assertion rather than loading that file. + +## Files and purpose + +| Fixture | Original producer | Compatibility contract | +|---------|-------------------|------------------------| +| `core-codecs.hex`, four `core-*.json.txt` files | `LegacyCodecFixtureWriter.java.txt`, baseline core | Scalar/ID/index bytes and `ConditionQuery.bytes()` JSON | +| `struct-tagged-properties.hex` | `LegacyStructFixtureWriter.java.txt`, baseline struct | Tagged scalar/LIST/SET properties and metadata recovery | +| `core-rows.properties` | `LegacyRowFixtureWriter.java.txt`, baseline core | Complete `BinarySerializer` rows, including property framing, TTL, OLAP and indexes | +| `store-index-rows.properties` | `LegacyStoreIndexFixtureWriter.java.txt`, baseline struct | Original `BinaryElementSerializer.writeIndex` keys and expiry values | +| `core-kryo-objects.properties` | `LegacyKryoFixtureWriter.java.txt`, baseline core | Relocated class descriptors and the original reader's supported/unsupported cases | +| `index-builder-contract.properties` | `LegacyStoreIndexBuilderProbe.java.txt` and `LegacyServerIndexQueryProbe.java.txt` | Existing Store long-text index/Server query mismatch | +| `core-relation-ordinals.properties` | `RelationFixtureWriter.java.txt` and pinned enum declarations | Legacy/canonical enum ordinals; see [provenance](relation-ordinals-provenance.md) | + +The `.java.txt` producers preserve reproducibility while keeping legacy imports outside test compilation. Store ingress and Server serializer tests carry copies of the relevant payloads because each module loads its own test resources. Keep those copies byte-identical when intentionally updating a fixture. + +`core-codecs.hex` contains 65 named `key=hex` payloads, with properties-style line continuations for large IDs. The JSON files wrap exact UTF-8 query output in an ASF comment prolog and final newline; tests remove those container boundaries. The tagged-property file contains 12 payloads. Source/fixture license headers must be retained. + +## Reproduce with the historical writers + +Use Java 11, UTF-8 and UTC in an isolated checkout of the pinned revision: + +```sh +git clone https://github.com/apache/hugegraph.git legacy-core +cd legacy-core +git checkout --detach 2f827d6e8c9c62ae858f2fc122b3a192d015e2f4 +mvn install -pl hugegraph-server/hugegraph-core -am -DskipTests +mvn dependency:build-classpath -pl hugegraph-server/hugegraph-core \ + -Dmdep.outputFile="$PWD/core-classpath.txt" +mvn install -pl hugegraph-struct -am -DskipTests +mvn dependency:build-classpath -pl hugegraph-struct \ + -Dmdep.outputFile="$PWD/struct-classpath.txt" +``` + +Copy each required `.java.txt` producer to a separate working directory as a `.java` file. Compile with `javac -proc:none -encoding UTF-8` against the relevant baseline module jar and its Maven runtime classpath. Put that baseline jar first; exclude the current shared implementations and other HugeGraph versions. Use separate core and struct classpaths, with no core jar for struct producers. Keep the pinned IndexBuilder's analyzer dependencies even for secondary-index probes. + +Run with `java -Dfile.encoding=UTF-8 -Duser.timezone=UTC` and these arguments: + +| Producer class | Arguments | +|----------------|-----------| +| `LegacyCodecFixtureWriter` | `` | +| `LegacyStructFixtureWriter` | ` ` | +| `LegacyRowFixtureWriter` | ` ` | +| `LegacyStoreIndexFixtureWriter` | ` ` | +| `LegacyKryoFixtureWriter` | ` ` | +| `LegacyStoreIndexBuilderProbe` | ` ` | +| `LegacyServerIndexQueryProbe` | ``; run after the Store probe, in a separate core-classpath process, to append the Server prefix | + +Compare output payloads with the committed resources before accepting changes. The source revision, producer source and payload bytes are the provenance; rebuilt jar hashes can differ because of Maven archive timestamps. Do not replace historical inputs with output from a new writer. Enum-shell reproduction has its own instructions in [relation ordinal provenance](relation-ordinals-provenance.md). + +## Scalar and query coverage + +The scalar fixtures cover signed varint/varlong limits and width boundaries; long, Unicode string and UUID IDs, string-header width changes and the 16 KiB limit; incoming/outgoing edge IDs with parent/sublabel; every scalar property type, including the shaded Kryo fallback; schema-driven SINGLE/LIST/SET values; and string plus 4/8-byte range index IDs. The range-index input is an explicit type/label/value layout, not a full index-builder output. + +Query fixtures cover vertex labels, numeric ranges, typed IN lists, dates, OR, limit/offset, edge ownership/direction/page tokens and executable COUNT(*)/AVG(age) strategies with their JSON aggregate shape. Legacy JSON dates encode local wall time without a zone: `2021-01-01 00:00:00.123` represents epoch `1609459200123` in UTC and `1609430400123` in Asia/Singapore. Tests calculate expected local time without changing the JVM default zone after adapters initialize. + +Core schema-driven properties are untagged; Store/struct properties carry a cardinality/type tag. Both formats share scalar suffixes but remain separate wire entry points. Tests compare their suffixes against independently produced core bytes; they must not read untagged core values as tagged Store properties. + +## Complete row and TTL contracts + +`core-rows.properties` contains 15 complete rows produced by the real baseline `HugeVertex`, `HugeEdge`, `HugeIndex`, properties and `BinarySerializer`. A proxy supplies schema lookup and a fixed clock and rejects unexpected graph calls. The producer validates rows with the original reader; its OLAP reader subclass only exposes `parseVertexOlap`, without overriding encoding. Row IDs, names and values are hex; `type`, `ttl` and `columns` retain `BackendEntry` metadata. + +| Schema | Definition | +|--------|------------| +| Properties 1/2/3 | `age` INT SINGLE = 7; `scores` INT LIST = `[7, -1, 7]`; `groups` INT SET = `{-1, 7}` | +| Property 9 | `rank` INT SINGLE OLAP_COMMON = 42 | +| Properties 16/17 | `weight` LONG SINGLE (range-long); `name` TEXT SINGLE (secondary) | +| Vertex labels 4/10 | `person` TTL 60000 / `timeless` without TTL; both use properties 1/2/3 | +| Edge labels 5/11 | `knows` TTL 60000 / `untimed` without TTL; both link 4→4 and use properties 1/2/3 | +| Index labels 6/7/8 | Secondary/range-int/range-long on vertex label 4 | +| Index labels 12/13/14 | Secondary/range-int/range-long on vertex label 10 | +| Index label 15 | Secondary on vertex label 4, hashed long-string key | + +The clock is `1609459200000`; timed rows expire at `1609459260000`. Vertex IDs are 1 (timed) and 3 (untimed). Edges connect 1 and 2 with sort value `2026!x`, or an empty sort value for the untimed edge. Index values are `alice`, integer 7, long 7, or `"long-field-".repeat(20)`. Default serializer flags enable key/index ID prefixes and disable partition prefixes. + +The OLAP column name is the vertex ID; its value is the property ID plus an untagged scalar. Merged-row tests supply an existing vertex, matching the original reader's main-row-then-OLAP-columns convention. + +Core system vertex/edge label indexes receive positive expiry but retain stable keys and empty values without expiry suffixes. Their fields are vertex label 4 and edge label 5 (with the full edge ID). Store's original user secondary/range and system-label indexes use the same expiration above, with no key expiry suffix and a 13-byte value envelope: `0x00 + Base64(big-endian long)`. + +Shared schema readers must read those Store rows while ordinary schema writers retain the Server key/hash/TTL layout. Tagged writers retain their Store format. Label TTL regression checks cover recovered expiry, `expiredElementIds`, clone/reset isolation, elimination, append and re-read: the exact original key remains stable, core's primitive writer emits its original empty value and the storage writer retains its expiry envelope. + +`property.OBJECT_UNSUPPORTED_COLLECTION` is a skip-only fixture: skipping must retain the trailing marker and TTL without instantiating Kryo data. It does not assert that the collection is readable. These row fixtures do not cover every partition layout, storage transaction or RocksDB/HStore runtime path. + +## Relocated Kryo descriptors + +`core-kryo-objects.properties` contains 36 unmodified original OBJECT payloads. Its `baseline_readable` fields record the original default reader's behavior. Twenty-four were readable: query/ID enums, Userdata with string/int/date entries, Class literals, a `BackendColumn` and populated array, null-element moved-type arrays/matrices, and nested mutable maps/lists. These must decode to the canonical runtime types and values. New writes may use canonical names; semantic roundtrips do not require retaining deprecated Java API names. + +Twelve written payloads were unreadable because the original reader required missing no-arg constructors: LongId/StringId/UuidId/EdgeId/BinaryId/Shard, four schema instances, IdWithExpiredTime and Arrays$ArrayList. Preserve class resolution without a ClassNotFound regression; these fixtures do not change the general instantiation policy or promise newly supported model instances. The readable OBJECT collection is a mutable `ArrayList` rather than `Arrays.asList`. + +Schema/index descriptor fixtures include TaskWithSchema, five P classes, four Builder interfaces, HugeIndex/IdWithExpiredTime Class literals and typed arrays. Empty/null-element arrays and Class literals remain supported even for types whose populated instances are unreadable. No readable live-graph HugeIndex OBJECT instance is claimed. Resolver tests separately accept the JVM 255-dimension boundary, reject 256/5000 dimensions before allocation, and delegate unknown and primitive descriptors to the normal resolver. + +## Existing IndexBuilder/query mismatch + +For TEXT property 17, index label 15, vertex label 4 and `"long-field-".repeat(20)`, the original Store `buildIndex` truncates the 220-character value to `long-field-long-fiel` (20 characters). Its key starts with that literal. The original Server `BinarySerializer.writeQuery`, using `ConditionQuery.concatValues` on the full value, emits hashed prefix `53493a463a323234303534613200`. The Store key does not match that query. + +The fixture records both executed paths, rather than combining guessed helper outputs. Writer/query alignment tests use the original Server prefix while retaining old Store row decoding. Installations using old Store rebuilding may need to rebuild affected indexes; decoding alone cannot repair the mismatch. + +## Payload integrity + +These SHA-256 values identify committed fixture bytes, independent of rebuilt jar timestamps: + +| File | SHA-256 | +|------|---------| +| `core-rows.properties` | `d90cfe0b349edb832bf7425b6b8e2ddb06b3eff3f29e834cf8363b0d2fb84662` | +| `store-index-rows.properties` | `e9e9c967884d0d91b6cf6d5eabf2050a9bad0fe7898d1c47b58535ebdf1b9431` | +| `core-kryo-objects.properties` | `45c10201a1f88b38771fd5bd4e609f461729ac1d5e8fb94304fa176bfd5cf369` | +| `index-builder-contract.properties` | `3be36ba02c4afd58aab383549b041368fab4b9e8e32474122457b5608970161b` | + +Scalar/query and complete-row tests establish these codec contracts, not full storage transactions or query-runtime compatibility. Those require integration and storage tests. diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/RelationFixtureWriter.java.txt b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/RelationFixtureWriter.java.txt new file mode 100644 index 0000000000..703cb3a919 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/RelationFixtureWriter.java.txt @@ -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. + */ +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import org.apache.tinkerpop.shaded.kryo.Kryo; +import org.apache.tinkerpop.shaded.kryo.io.Output; +import org.apache.hugegraph.backend.query.Condition.RelationType; +public class RelationFixtureWriter { + public static void main(String[] args) { + System.out.println("legacy.names=" + String.join(",", Arrays.stream(RelationType.values()).map(Enum::name).toArray(String[]::new))); + for (RelationType value : RelationType.values()) write("enum." + value, value); + write("array", RelationType.values()); + write("matrix", new RelationType[][]{RelationType.values(), null, new RelationType[0]}); + Map nested = new LinkedHashMap<>(); + nested.put("values", new ArrayList<>(Arrays.asList(RelationType.values()))); + nested.put("array", RelationType.values()); nested.put("last", RelationType.SCAN); + write("nested", nested); + RelationType[] oldArray = {RelationType.SCAN, null, RelationType.TEXT_CONTAINS}; + org.apache.hugegraph.query.Condition.RelationType[] currentArray = { + org.apache.hugegraph.query.Condition.RelationType.TEXT_PREFIX, null, + org.apache.hugegraph.query.Condition.RelationType.TEXT_REGEX}; + write("interleaved", new ArrayList<>(Arrays.asList(RelationType.SCAN, + org.apache.hugegraph.query.Condition.RelationType.TEXT_PREFIX, + RelationType.TEXT_CONTAINS, org.apache.hugegraph.query.Condition.RelationType.TEXT_REGEX, + oldArray, currentArray, oldArray, currentArray))); + } + private static void write(String name, Object value) { + Output output = new Output(4096); new Kryo().writeClassAndObject(output, value); + StringBuilder hex = new StringBuilder(); + for (byte b : output.toBytes()) hex.append(String.format("%02x", b & 255)); + System.out.println(name + ".hex=" + hex); + } +} diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-avg-query.json.txt b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-avg-query.json.txt new file mode 100644 index 0000000000..78757f8fdc --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-avg-query.json.txt @@ -0,0 +1,17 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +{"conditions":[{"cls":"S","el":{"key":"LABEL","relation":"EQ","value":{"id":3,"cls":"org.apache.hugegraph.backend.id.IdGenerator$LongId"}}}],"optimizedType":"NONE","ids":[],"mustSortByInput":true,"resultType":"VERTEX","offset":0,"actualOffset":0,"actualStoreOffset":0,"limit":9223372036854775807,"skipDegree":9223372036854775807,"capacity":800000,"showHidden":false,"showDeleting":false,"showExpired":false,"olap":false,"withProperties":true,"orderType":"ORDER_STRICT","olapPks":[],"aggregate":{"func":"AVG","column":"age"}} diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-codecs.hex b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-codecs.hex new file mode 100644 index 0000000000..9665439e6e --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-codecs.hex @@ -0,0 +1,420 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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.long.-9223372036854775808=708000000000000000 +vlong.-9223372036854775808=81808080808080808000 +id.long.-16385=17bfff +vlong.-16385=81fffffffffffffeff7f +id.long.-16384=17c000 +vlong.-16384=81ffffffffffffff8000 +id.long.-129=077f +vlong.-129=81fffffffffffffffe7f +id.long.-128=0780 +vlong.-128=81ffffffffffffffff00 +id.long.-1=07ff +vlong.-1=81ffffffffffffffff7f +id.long.0=0800 +vlong.0=00 +id.long.1=0801 +vlong.1=01 +id.long.127=087f +vlong.127=7f +id.long.128=0880 +vlong.128=8100 +id.long.16383=183fff +vlong.16383=ff7f +id.long.16384=184000 +vlong.16384=818000 +id.long.9223372036854775807=787fffffffffffffff +vlong.9223372036854775807=ffffffffffffffff7f +vint.-2147483648=8880808000 +vint.-129=8ffffffe7f +vint.-1=8fffffff7f +vint.0=00 +vint.127=7f +vint.128=8100 +vint.16383=ff7f +vint.16384=818000 +vint.2147483647=87ffffff7f +id.text=84616c696365 +id.unicode=89e9a1b6e782b9f09f9880 +id.text.length.1=8078 +id.text.length.64=bf78787878787878787878787878787878787878787878787878787878787878787878787878787878\ +787878787878787878787878787878787878787878787878 +id.text.length.65=c040787878787878787878787878787878787878787878787878787878787878787878787878787878\ +7878787878787878787878787878787878787878787878787878 +id.text.length.256=c0ff78787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +87878787878787878787878787878787878 +id.text.length.257=c10078787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878 +id.text.length.16384=ffff787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +8787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787\ +878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878 +id.uuid=7f0123456789abcdef0123456789abcdef +edge.out=84616c6963658208030804323032362178000863 +edge.in=08638c080308043230323621780084616c696365 +property.BOOLEAN=01 +property.BYTE=8fffffff79 +property.INT=8ffff8bb40 +property.LONG=81808080808080808000 +property.FLOAT=3fa00000 +property.DOUBLE=c05ec80000000000 +property.DATE=aeebd9f9e07b +property.TEXT=0be5b19ee680a7f09f988000 +property.BLOB=0500ff017f80 +property.UUID=0123456789abcdef0123456789abcdef +property.OBJECT=2301006a6176612e7574696c2e41727261794c6973f4010303016c65676163f9020e0501 +property.UNKNOWN=0f03016c65676163792d6f626a6563f4 +property.OBJECT_UNSUPPORTED_COLLECTION=2a01006a6176612e7574696c2e4172726179732441727261794c6973f4010\ +303016c65676163f9020e0501 +schema.single=07 +schema.list=03078fffffff7f07 +schema.set=02078fffffff7f +index.string=6c6162656c21616c69636500 +index.int=a00000000380000007 +index.long=a2000000038000000000000007 diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-count-query.json.txt b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-count-query.json.txt new file mode 100644 index 0000000000..68cf5a6f85 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-count-query.json.txt @@ -0,0 +1,17 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +{"conditions":[],"optimizedType":"NONE","ids":[],"mustSortByInput":true,"resultType":"VERTEX","offset":0,"actualOffset":0,"actualStoreOffset":0,"limit":9223372036854775807,"skipDegree":9223372036854775807,"capacity":800000,"showHidden":false,"showDeleting":false,"showExpired":false,"olap":false,"withProperties":true,"orderType":"ORDER_STRICT","olapPks":[],"aggregate":{"func":"COUNT"}} diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-edge-query.json.txt b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-edge-query.json.txt new file mode 100644 index 0000000000..fed3f9cfa3 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-edge-query.json.txt @@ -0,0 +1,17 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +{"conditions":[{"cls":"S","el":{"key":"OWNER_VERTEX","relation":"EQ","value":{"id":"alice","cls":"org.apache.hugegraph.backend.id.IdGenerator$StringId"}}},{"cls":"S","el":{"key":"DIRECTION","relation":"EQ","value":"OUT"}},{"cls":"S","el":{"key":"LABEL","relation":"EQ","value":{"id":3,"cls":"org.apache.hugegraph.backend.id.IdGenerator$LongId"}}}],"optimizedType":"NONE","ids":[],"mustSortByInput":true,"resultType":"EDGE","offset":0,"actualOffset":0,"actualStoreOffset":0,"limit":5,"skipDegree":9223372036854775807,"page":"AQIDBA\u003d\u003d","capacity":800000,"showHidden":false,"showDeleting":false,"showExpired":false,"olap":false,"withProperties":true,"orderType":"ORDER_STRICT","olapPks":[]} diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-kryo-objects.properties b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-kryo-objects.properties new file mode 100644 index 0000000000..0f644d2219 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-kryo-objects.properties @@ -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. + # +long_id.hex=3701006f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496447656e65726174\ +6f72244c6f6e6749e401010e +long_id.baseline_readable=false +long_id.baseline_error=org.apache.tinkerpop.shaded.kryo.KryoException: Class cannot be created (miss\ +ing no-arg constructor): org.apache.hugegraph.backend.id.IdGenerator$LongId +string_id.hex=4201006f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496447656e657261\ +746f7224537472696e6749e40100016c65676163792d69e4 +string_id.baseline_readable=false +string_id.baseline_error=org.apache.tinkerpop.shaded.kryo.KryoException: Class cannot be created (mi\ +ssing no-arg constructor): org.apache.hugegraph.backend.id.IdGenerator$StringId +uuid_id.hex=4601006f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496447656e65726174\ +6f72245575696449e401010123456789abcdef0123456789abcdef +uuid_id.baseline_readable=false +uuid_id.baseline_error=org.apache.tinkerpop.shaded.kryo.KryoException: Class cannot be created (miss\ +ing no-arg constructor): org.apache.hugegraph.backend.id.IdGenerator$UuidId +edge_id.hex=813501006f72672e6170616368652e6875676567726170682e6261636b656e642e69642e4564676549e40100\ +00010201016f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496447656e657261746f72244c\ +6f6e6749e401010601026f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496447656e657261\ +746f7224537472696e6749e40100016c65676163792d69e4010101010e01736f72f40101010108 +edge_id.baseline_readable=false +edge_id.baseline_error=org.apache.tinkerpop.shaded.kryo.KryoException: Class cannot be created (miss\ +ing no-arg constructor): org.apache.hugegraph.backend.id.EdgeId +binary_id.hex=81040100c4016f72672e6170616368652e6875676567726170682e6261636b656e642e73657269616c697a\ +65722e42696e6172794261636b656e64456e7472792442696e617279496401010401020301016f72672e6170616368652e68\ +75676567726170682e6261636b656e642e69642e496447656e657261746f72244c6f6e6749e401010e +binary_id.baseline_readable=false +binary_id.baseline_error=org.apache.tinkerpop.shaded.kryo.KryoException: Class cannot be created (mi\ +ssing no-arg constructor): org.apache.hugegraph.backend.serializer.BinaryBackendEntry$BinaryId +shard.hex=3601006f72672e6170616368652e6875676567726170682e6261636b656e642e73746f72652e53686172e40101\ +656ee4180173746172f4 +shard.baseline_readable=false +shard.baseline_error=org.apache.tinkerpop.shaded.kryo.KryoException: Class cannot be created (missin\ +g no-arg constructor): org.apache.hugegraph.backend.store.Shard +property_key.hex=811801006f72672e6170616368652e6875676567726170682e736368656d612e50726f70657274794b6\ +5f9010101010101090001016f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496447656e657\ +261746f72244c6f6e6749e4010116016167e5010101026f72672e6170616368652e6875676567726170682e736368656d612\ +e55736572646174e101000101 +property_key.baseline_readable=false +property_key.baseline_error=org.apache.tinkerpop.shaded.kryo.KryoException: Class cannot be created \ +(missing no-arg constructor): org.apache.hugegraph.schema.PropertyKey +vertex_label.hex=815001006f72672e6170616368652e6875676567726170682e736368656d612e5665727465784c61626\ +5ec01010001016f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496447656e657261746f722\ +44c6f6e6749e4010118010101026a6176612e7574696c2e486173685365f4010001706572736fee0102010001036a6176612\ +e7574696c2e41727261794c6973f4010001020100010100010101010001046f72672e6170616368652e68756765677261706\ +82e736368656d612e55736572646174e10100 +vertex_label.baseline_readable=false +vertex_label.baseline_error=org.apache.tinkerpop.shaded.kryo.KryoException: Class cannot be created \ +(missing no-arg constructor): org.apache.hugegraph.schema.VertexLabel +edge_label.hex=815a01006f72672e6170616368652e6875676567726170682e736368656d612e456467654c616265ec010\ +101010001010001016f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496447656e657261746\ +f72244c6f6e6749e401011a01026a6176612e7574696c2e486173685365f4010001020100016b6e6f77f3010201000102010\ +001036a6176612e7574696c2e41727261794c6973f401000101010100010101010c0001010c01046f72672e6170616368652\ +e6875676567726170682e736368656d612e55736572646174e10100 +edge_label.baseline_readable=false +edge_label.baseline_error=org.apache.tinkerpop.shaded.kryo.KryoException: Class cannot be created (m\ +issing no-arg constructor): org.apache.hugegraph.schema.EdgeLabel +index_label.hex=813201006f72672e6170616368652e6875676567726170682e736368656d612e496e6465784c616265ec\ +01012001016f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496447656e657261746f72244c\ +6f6e6749e401010000010101011c01026a6176612e7574696c2e41727261794c6973f4010001010162792d6167e501010103\ +6f72672e6170616368652e6875676567726170682e736368656d612e55736572646174e10100 +index_label.baseline_readable=false +index_label.baseline_error=org.apache.tinkerpop.shaded.kryo.KryoException: Class cannot be created (\ +missing no-arg constructor): org.apache.hugegraph.schema.IndexLabel +aggregate_func.hex=3e01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e41676\ +77265676174652441676772656761746546756ee30104 +aggregate_func.baseline_readable=true +aggregate_func.baseline_type=org.apache.hugegraph.backend.query.Aggregate$AggregateFunc +relation_type.hex=3d01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e\ +646974696f6e2452656c6174696f6e547970e50103 +relation_type.baseline_readable=true +relation_type.baseline_type=org.apache.hugegraph.backend.query.Condition$RelationType +condition_type.hex=3e01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6\ +e646974696f6e24436f6e646974696f6e547970e50104 +condition_type.baseline_readable=true +condition_type.baseline_type=org.apache.hugegraph.backend.query.Condition$ConditionType +query_order.hex=3201006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e51756572\ +79244f726465f20102 +query_order.baseline_readable=true +query_order.baseline_type=org.apache.hugegraph.backend.query.Query$Order +query_order_type.hex=3601006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e517\ +5657279244f72646572547970e50103 +query_order_type.baseline_readable=true +query_order_type.baseline_type=org.apache.hugegraph.backend.query.Query$OrderType +optimized_type.hex=4301006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6\ +e646974696f6e5175657279244f7074696d697a6564547970e50105 +optimized_type.baseline_readable=true +optimized_type.baseline_type=org.apache.hugegraph.backend.query.ConditionQuery$OptimizedType +id_type.hex=2d01006f72672e6170616368652e6875676567726170682e6261636b656e642e69642e4964244964547970e5\ +0103 +id_type.baseline_readable=true +id_type.baseline_type=org.apache.hugegraph.backend.id.Id$IdType +graph_type_class.hex=3d01006a6176612e6c616e672e436c6173f30101016f72672e6170616368652e687567656772617\ +0682e7374727563747572652e4772617068547970e500 +graph_type_class.baseline_readable=true +graph_type_class.baseline_type=java.lang.Class +id_array.hex=2a01005b4c6f72672e6170616368652e6875676567726170682e6261636b656e642e69642e4964bb010200 +id_array.baseline_readable=true +id_array.baseline_type=[Lorg.apache.hugegraph.backend.id.Id; +id_matrix.hex=2f01005b5b4c6f72672e6170616368652e6875676567726170682e6261636b656e642e69642e4964bb0103\ +0102000101 +id_matrix.baseline_readable=true +id_matrix.baseline_type=[[Lorg.apache.hugegraph.backend.id.Id; +shard_array.hex=3001005b4c6f72672e6170616368652e6875676567726170682e6261636b656e642e73746f72652e5368\ +617264bb010200 +shard_array.baseline_readable=true +shard_array.baseline_type=[Lorg.apache.hugegraph.backend.store.Shard; +schema_array.hex=2f01005b4c6f72672e6170616368652e6875676567726170682e736368656d612e50726f70657274794\ +b6579bb010200 +schema_array.baseline_readable=true +schema_array.baseline_type=[Lorg.apache.hugegraph.schema.PropertyKey; +userdata.hex=6301006f72672e6170616368652e6875676567726170682e736368656d612e55736572646174e1010303017\ +e6372656174655f74696de501016a6176612e7574696c2e446174e501fbe0f9d9eb2e0301636f756ef4020e03017461e7030\ +16c65676163f9 +userdata.baseline_readable=true +userdata.baseline_type=org.apache.hugegraph.schema.Userdata +backend_column.hex=4801006f72672e6170616368652e6875676567726170682e6261636b656e642e73746f72652e42616\ +36b656e64456e747279244261636b656e64436f6c756dee010103010201030304 +backend_column.baseline_readable=true +backend_column.baseline_type=org.apache.hugegraph.backend.store.BackendEntry$BackendColumn +backend_column_array.hex=810f0100c1015b4c6f72672e6170616368652e6875676567726170682e6261636b656e642e7\ +3746f72652e4261636b656e64456e747279244261636b656e64436f6c756d6e3b010301016f72672e6170616368652e68756\ +76567726170682e6261636b656e642e73746f72652e4261636b656e64456e747279244261636b656e64436f6c756dee01010\ +301020103030400 +backend_column_array.baseline_readable=true +backend_column_array.baseline_type=[Lorg.apache.hugegraph.backend.store.BackendEntry$BackendColumn; +backend_column_class.hex=5201006a6176612e6c616e672e436c6173f30101016f72672e6170616368652e68756765677\ +26170682e6261636b656e642e73746f72652e4261636b656e64456e747279244261636b656e64436f6c756dee00 +backend_column_class.baseline_readable=true +backend_column_class.baseline_type=java.lang.Class +class_list.hex=842601006a6176612e7574696c2e41727261794c6973f4010a01016a6176612e6c616e672e436c6173f30\ +101026f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496447656e657261746f72244c6f6e6\ +749e40001010101036f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496447656e657261746\ +f7224537472696e6749e40001010101046f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496\ +447656e657261746f72245575696449e40001010101056f72672e6170616368652e6875676567726170682e6261636b656e6\ +42e69642e4564676549e4000101010106c4016f72672e6170616368652e6875676567726170682e6261636b656e642e73657\ +269616c697a65722e42696e6172794261636b656e64456e7472792442696e61727949640001010101076f72672e617061636\ +8652e6875676567726170682e6261636b656e642e73746f72652e53686172e40001010101086f72672e6170616368652e687\ +5676567726170682e736368656d612e50726f70657274794b65f90001010101096f72672e6170616368652e6875676567726\ +170682e736368656d612e5665727465784c616265ec00010101010a6f72672e6170616368652e6875676567726170682e736\ +368656d612e456467654c616265ec00010101010b6f72672e6170616368652e6875676567726170682e736368656d612e496\ +e6465784c616265ec00 +class_list.baseline_readable=true +class_list.baseline_type=java.util.ArrayList +contract_classes.hex=843801006a6176612e7574696c2e41727261794c6973f4010a01016a6176612e6c616e672e436c6\ +173f30101026f72672e6170616368652e6875676567726170682e747970652e4e616d6561626ce50001010101036f72672e6\ +170616368652e6875676567726170682e747970652e5479706561626ce50001010101046f72672e6170616368652e6875676\ +567726170682e747970652e496e64657861626ce50001010101056f72672e6170616368652e6875676567726170682e74797\ +0652e50726f706572746961626ce50001010101066f72672e6170616368652e6875676567726170682e6261636b656e642e6\ +9642e4964557469ec0001010101076f72672e6170616368652e6875676567726170682e6261636b656e642e69642e53706c6\ +963696e67496447656e657261746ff20001010101086f72672e6170616368652e6875676567726170682e736368656d612e6\ +275696c6465722e536368656d614275696c6465f2000101010109c8016f72672e6170616368652e6875676567726170682e6\ +261636b656e642e71756572792e73657269616c697a65722e416273747261637453657269616c697a6572416461707465720\ +0010101010a6f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e73657269616c697a657\ +22e5175657279416461707465f200010101010b6f72672e6170616368652e6875676567726170682e6261636b656e642e717\ +56572792e73657269616c697a65722e51756572794964416461707465f200 +contract_classes.baseline_readable=true +contract_classes.baseline_type=java.util.ArrayList +schema_nested_classes.hex=841f01006a6176612e7574696c2e41727261794c6973f4010a01016a6176612e6c616e672e\ +436c6173f30101026f72672e6170616368652e6875676567726170682e736368656d612e536368656d61456c656d656e7424\ +5461736b57697468536368656de10001010101036f72672e6170616368652e6875676567726170682e736368656d612e5072\ +6f70657274794b657924d00001010101046f72672e6170616368652e6875676567726170682e736368656d612e5665727465\ +784c6162656c24d00001010101056f72672e6170616368652e6875676567726170682e736368656d612e456467654c616265\ +6c24d00001010101066f72672e6170616368652e6875676567726170682e736368656d612e496e6465784c6162656c24d000\ +01010101076f72672e6170616368652e6875676567726170682e736368656d612e536368656d61456c656d656e7424d00001\ +010101086f72672e6170616368652e6875676567726170682e736368656d612e50726f70657274794b6579244275696c6465\ +f20001010101096f72672e6170616368652e6875676567726170682e736368656d612e5665727465784c6162656c24427569\ +6c6465f200010101010a6f72672e6170616368652e6875676567726170682e736368656d612e456467654c6162656c244275\ +696c6465f200010101010b6f72672e6170616368652e6875676567726170682e736368656d612e496e6465784c6162656c24\ +4275696c6465f200 +schema_nested_classes.baseline_readable=true +schema_nested_classes.baseline_type=java.util.ArrayList +schema_nested_arrays.hex=842401006a6176612e7574696c2e41727261794c6973f4010a01015b4c6f72672e617061636\ +8652e6875676567726170682e736368656d612e536368656d61456c656d656e74245461736b57697468536368656d61bb010\ +20001025b4c6f72672e6170616368652e6875676567726170682e736368656d612e50726f70657274794b65792450bb01020\ +001035b4c6f72672e6170616368652e6875676567726170682e736368656d612e5665727465784c6162656c2450bb0102000\ +1045b4c6f72672e6170616368652e6875676567726170682e736368656d612e456467654c6162656c2450bb01020001055b4\ +c6f72672e6170616368652e6875676567726170682e736368656d612e496e6465784c6162656c2450bb01020001065b4c6f7\ +2672e6170616368652e6875676567726170682e736368656d612e536368656d61456c656d656e742450bb01020001075b4c6\ +f72672e6170616368652e6875676567726170682e736368656d612e50726f70657274794b6579244275696c646572bb01020\ +001085b4c6f72672e6170616368652e6875676567726170682e736368656d612e5665727465784c6162656c244275696c646\ +572bb01020001095b4c6f72672e6170616368652e6875676567726170682e736368656d612e456467654c6162656c2442756\ +96c646572bb010200010a5b4c6f72672e6170616368652e6875676567726170682e736368656d612e496e6465784c6162656\ +c244275696c646572bb010200 +schema_nested_arrays.baseline_readable=true +schema_nested_arrays.baseline_type=java.util.ArrayList +index_classes.hex=811401006a6176612e7574696c2e41727261794c6973f4010201016a6176612e6c616e672e436c6173\ +f30101026f72672e6170616368652e6875676567726170682e7374727563747572652e48756765496e6465f8000101010103\ +6f72672e6170616368652e6875676567726170682e7374727563747572652e48756765496e64657824496457697468457870\ +6972656454696de500 +index_classes.baseline_readable=true +index_classes.baseline_type=java.util.ArrayList +index_array.hex=3001005b4c6f72672e6170616368652e6875676567726170682e7374727563747572652e48756765496e\ +646578bb010200 +index_array.baseline_readable=true +index_array.baseline_type=[Lorg.apache.hugegraph.structure.HugeIndex; +expiry_id_array.hex=4201005b4c6f72672e6170616368652e6875676567726170682e7374727563747572652e48756765\ +496e646578244964576974684578706972656454696d65bb010200 +expiry_id_array.baseline_readable=true +expiry_id_array.baseline_type=[Lorg.apache.hugegraph.structure.HugeIndex$IdWithExpiredTime; +expiry_id.hex=7a01006f72672e6170616368652e6875676567726170682e7374727563747572652e48756765496e646578\ +244964576974684578706972656454696de501c0e9fab3d75d01016f72672e6170616368652e6875676567726170682e6261\ +636b656e642e69642e496447656e657261746f72244c6f6e6749e401010e +expiry_id.baseline_readable=false +expiry_id.baseline_error=org.apache.tinkerpop.shaded.kryo.KryoException: Class cannot be created (mi\ +ssing no-arg constructor): org.apache.hugegraph.structure.HugeIndex$IdWithExpiredTime +nested.hex=870301006a6176612e7574696c2e4c696e6b6564486173684d61f001050301636c61737365f301016a6176612\ +e7574696c2e41727261794c6973f4010a01026a6176612e6c616e672e436c6173f30101036f72672e6170616368652e68756\ +76567726170682e6261636b656e642e69642e496447656e657261746f72244c6f6e6749e40001020101046f72672e6170616\ +368652e6875676567726170682e6261636b656e642e69642e496447656e657261746f7224537472696e6749e400010201010\ +56f72672e6170616368652e6875676567726170682e6261636b656e642e69642e496447656e657261746f72245575696449e\ +40001020101066f72672e6170616368652e6875676567726170682e6261636b656e642e69642e4564676549e400010201010\ +7c4016f72672e6170616368652e6875676567726170682e6261636b656e642e73657269616c697a65722e42696e617279426\ +1636b656e64456e7472792442696e61727949640001020101086f72672e6170616368652e6875676567726170682e6261636\ +b656e642e73746f72652e53686172e40001020101096f72672e6170616368652e6875676567726170682e736368656d612e5\ +0726f70657274794b65f900010201010a6f72672e6170616368652e6875676567726170682e736368656d612e56657274657\ +84c616265ec00010201010b6f72672e6170616368652e6875676567726170682e736368656d612e456467654c616265ec000\ +10201010c6f72672e6170616368652e6875676567726170682e736368656d612e496e6465784c616265ec0003016d6574616\ +46174e1010d6f72672e6170616368652e6875676567726170682e736368656d612e55736572646174e1010303017e6372656\ +174655f74696de5010e6a6176612e7574696c2e446174e501fbe0f9d9eb2e0301636f756ef4020e03017461e703016c65676\ +163f903016964f3010f5b5b4c6f72672e6170616368652e6875676567726170682e6261636b656e642e69642e4964bb01030\ +10200010103017072656469636174e501106f72672e6170616368652e6875676567726170682e6261636b656e642e7175657\ +2792e436f6e646974696f6e2452656c6174696f6e547970e501030301636f6c756dee01116f72672e6170616368652e68756\ +76567726170682e6261636b656e642e73746f72652e4261636b656e64456e747279244261636b656e64436f6c756dee01010\ +3010201030304 +nested.baseline_readable=true +nested.baseline_type=java.util.LinkedHashMap +unsupported_arrays_list.hex=2a01006a6176612e7574696c2e4172726179732441727261794c6973f4010303016c6567\ +6163f9020e0501 +unsupported_arrays_list.baseline_readable=false +unsupported_arrays_list.baseline_error=org.apache.tinkerpop.shaded.kryo.KryoException: Class cannot \ +be created (missing no-arg constructor): java.util.Arrays$ArrayList diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-query.json.txt b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-query.json.txt new file mode 100644 index 0000000000..8d86059b09 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-query.json.txt @@ -0,0 +1,17 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +{"conditions":[{"cls":"S","el":{"key":"LABEL","relation":"EQ","value":{"id":3,"cls":"org.apache.hugegraph.backend.id.IdGenerator$LongId"}}},{"cls":"U","el":{"key":{"cls":"L","el":{"id":11}},"relation":"GTE","value":18,"valuecls":"java.lang.Integer"}},{"cls":"U","el":{"key":{"cls":"L","el":{"id":12}},"relation":"IN","value":[20,30],"valuecls":"java.lang.Long"}},{"cls":"U","el":{"key":{"cls":"L","el":{"id":13}},"relation":"EQ","value":"2021-01-01 00:00:00.123","valuecls":"java.util.Date"}},{"cls":"O","el":{"left":{"cls":"U","el":{"key":{"cls":"L","el":{"id":14}},"relation":"EQ","value":"alice"}},"right":{"cls":"U","el":{"key":{"cls":"L","el":{"id":14}},"relation":"EQ","value":"bob"}}}}],"optimizedType":"NONE","ids":[],"mustSortByInput":true,"resultType":"VERTEX","offset":7,"actualOffset":0,"actualStoreOffset":0,"limit":19,"skipDegree":9223372036854775807,"capacity":800000,"showHidden":false,"showDeleting":false,"showExpired":false,"olap":false,"withProperties":true,"orderType":"ORDER_STRICT","olapPks":[]} diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-relation-ordinals.properties b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-relation-ordinals.properties new file mode 100644 index 0000000000..e810113beb --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-relation-ordinals.properties @@ -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. + # +# Independently generated with shaded Kryo 3.5.1; see relation-ordinals-provenance.md. +legacy.names=EQ,GT,GTE,LT,LTE,NEQ,IN,NOT_IN,PREFIX,TEXT_CONTAINS,TEXT_CONTAINS_ANY,CONTAINS,CONTAINS_VALUE,CONTAINS_KEY,SCAN +enum.EQ.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e50101 +enum.GT.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e50102 +enum.GTE.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e50103 +enum.LT.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e50104 +enum.LTE.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e50105 +enum.NEQ.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e50106 +enum.IN.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e50107 +enum.NOT_IN.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e50108 +enum.PREFIX.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e50109 +enum.TEXT_CONTAINS.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e5010a +enum.TEXT_CONTAINS_ANY.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e5010b +enum.CONTAINS.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e5010c +enum.CONTAINS_VALUE.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e5010d +enum.CONTAINS_KEY.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e5010e +enum.SCAN.hex=01006f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e5010f +array.hex=01005b4c6f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e54797065bb0110010101020103010401050106010701080109010a010b010c010d010e010f +matrix.hex=01005b5b4c6f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e54797065bb01040110010101020103010401050106010701080109010a010b010c010d010e010f000101 +nested.hex=01006a6176612e7574696c2e4c696e6b6564486173684d61f00103030176616c7565f301016a6176612e7574696c2e41727261794c6973f4010f01026f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e5010101020102010201030102010401020105010201060102010701020108010201090102010a0102010b0102010c0102010d0102010e0102010f030161727261f901035b4c6f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e54797065bb011005060708090a0b0c0d0e0f1011121303016c6173f4010213 +interleaved.hex=01006a6176612e7574696c2e41727261794c6973f4010801016f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e547970e5010f01026f72672e6170616368652e6875676567726170682e71756572792e436f6e646974696f6e2452656c6174696f6e547970e5010f0101010a0102011a01035b4c6f72672e6170616368652e6875676567726170682e6261636b656e642e71756572792e436f6e646974696f6e2452656c6174696f6e54797065bb010403000501045b4c6f72672e6170616368652e6875676567726170682e71756572792e436f6e646974696f6e2452656c6174696f6e54797065bb0104040006010307010408 diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-rows.properties b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-rows.properties new file mode 100644 index 0000000000..fd489e58c1 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/core-rows.properties @@ -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. + # +now=1609459200000 +expiry=1609459260000 +vertex.type=VERTEX +vertex.id=0801 +vertex.ttl=60000 +vertex.columns=1 +vertex.column.0.name=0801 +vertex.column.0.value=08040301070203078fffffff7f0703028fffffff7f07aeebd9fdb460 +edge.out.type=EDGE_OUT +edge.out.id=08018208050805323032362178000802 +edge.out.ttl=60000 +edge.out.columns=1 +edge.out.column.0.name=08018208050805323032362178000802 +edge.out.column.0.value=0301070203078fffffff7f0703028fffffff7f07aeebd9fdb460 +edge.in.type=EDGE_IN +edge.in.id=08028c08050805323032362178000801 +edge.in.ttl=60000 +edge.in.columns=1 +edge.in.column.0.name=08028c08050805323032362178000801 +edge.in.column.0.value=0301070203078fffffff7f0703028fffffff7f07aeebd9fdb460 +olap.type=OLAP +olap.id=0801 +olap.ttl=0 +olap.columns=1 +olap.column.0.name=0801 +olap.column.0.value=092a +index.secondary.type=SECONDARY_INDEX +index.secondary.id=53493a363a616c69636500 +index.secondary.ttl=60000 +index.secondary.columns=1 +index.secondary.column.0.name=53493a363a616c696365000801aeebd9fdb460 +index.secondary.column.0.value= +index.int.type=RANGE_INT_INDEX +index.int.id=a00000000780000007 +index.int.ttl=60000 +index.int.columns=1 +index.int.column.0.name=a000000007800000070801aeebd9fdb460 +index.int.column.0.value= +index.long.type=RANGE_LONG_INDEX +index.long.id=a2000000088000000000000007 +index.long.ttl=60000 +index.long.columns=1 +index.long.column.0.name=a20000000880000000000000070801aeebd9fdb460 +index.long.column.0.value= +vertex.untimed.type=VERTEX +vertex.untimed.id=0803 +vertex.untimed.ttl=0 +vertex.untimed.columns=1 +vertex.untimed.column.0.name=0803 +vertex.untimed.column.0.value=080a0301070203078fffffff7f0703028fffffff7f07 +edge.untimed.type=EDGE_OUT +edge.untimed.id=080182080b080b000802 +edge.untimed.ttl=0 +edge.untimed.columns=1 +edge.untimed.column.0.name=080182080b080b000802 +edge.untimed.column.0.value=0301070203078fffffff7f0703028fffffff7f07 +index.secondary.untimed.type=SECONDARY_INDEX +index.secondary.untimed.id=53493a433a616c69636500 +index.secondary.untimed.ttl=0 +index.secondary.untimed.columns=1 +index.secondary.untimed.column.0.name=53493a433a616c696365000801 +index.secondary.untimed.column.0.value= +index.int.untimed.type=RANGE_INT_INDEX +index.int.untimed.id=a00000000d80000007 +index.int.untimed.ttl=0 +index.int.untimed.columns=1 +index.int.untimed.column.0.name=a00000000d800000070801 +index.int.untimed.column.0.value= +index.long.untimed.type=RANGE_LONG_INDEX +index.long.untimed.id=a20000000e8000000000000007 +index.long.untimed.ttl=0 +index.long.untimed.columns=1 +index.long.untimed.column.0.name=a20000000e80000000000000070801 +index.long.untimed.column.0.value= +index.hashed.type=SECONDARY_INDEX +index.hashed.id=53493a463a323234303534613200 +index.hashed.ttl=60000 +index.hashed.columns=1 +index.hashed.column.0.name=53493a463a3232343035346132000801aeebd9fdb460 +index.hashed.column.0.value=6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d6c6f6e672d6669656c642d +index.system.type=VERTEX_LABEL_INDEX +index.system.id=56493a2d313a3400 +index.system.ttl=0 +index.system.columns=1 +index.system.column.0.name=56493a2d313a34000801 +index.system.column.0.value= +index.system.edge.type=EDGE_LABEL_INDEX +index.system.edge.id=45493a2d323a3500 +index.system.edge.ttl=0 +index.system.edge.columns=1 +index.system.edge.column.0.name=45493a2d323a35007e08018208050805323032362178000802 +index.system.edge.column.0.value= diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/index-builder-contract.properties b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/index-builder-contract.properties new file mode 100644 index 0000000000..a6139eef41 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/index-builder-contract.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. + # +input.length=220 +store.builder.field.length=20 +store.builder.field=long-field-long-fiel +store.builder.key=53493a463a6c6f6e672d6669656c642d6c6f6e672d6669656c000801 +store.builder.value=00414141426472732f576d413d +server.query.field.length=220 +server.query.prefix=53493a463a323234303534613200 diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/relation-ordinals-provenance.md b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/relation-ordinals-provenance.md new file mode 100644 index 0000000000..2d29d69331 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/relation-ordinals-provenance.md @@ -0,0 +1,42 @@ + + +# Relation ordinal fixture provenance + +`core-relation-ordinals.properties` contains raw `Kryo.writeClassAndObject` payloads without a BytesBuffer property-length prefix. The Kryo enum serializer bundled in `gremlin-shaded` 3.5.1 encodes minimal enum shells with these exact class names and declaration orders: + +| Enum declaration | Source revision | Values | +|------------------|-----------------|--------| +| Core `org.apache.hugegraph.backend.query.Condition.RelationType` | [2f827d6e8c9c62ae858f2fc122b3a192d015e2f4](https://github.com/apache/hugegraph/blob/2f827d6e8c9c62ae858f2fc122b3a192d015e2f4/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/Condition.java) | 15 | +| Struct `org.apache.hugegraph.query.Condition.RelationType` | [9c52f40f9a8e5a6c25feb78669d57e45ba0f4ec8](https://github.com/apache/hugegraph/blob/9c52f40f9a8e5a6c25feb78669d57e45ba0f4ec8/hugegraph-struct/src/main/java/org/apache/hugegraph/query/Condition.java) | 27 | + +Enum methods and predicate lambdas do not contribute to Kryo EnumSerializer bytes. These payloads were independently encoded from the pinned declarations, not produced by a full baseline core build. The legacy GTE bytes equal `relation_type.hex` in `core-kryo-objects.properties` after removing its `3d` property-length prefix, providing a cross-check against the original core writer. + +## Regeneration + +Work outside production sources, using the preserved `.java.txt` files: + +1. Copy `LegacyRelationEnum.java.txt` to `org/apache/hugegraph/backend/query/Condition.java` and `CanonicalRelationEnum.java.txt` to `org/apache/hugegraph/query/Condition.java`. +2. Copy `RelationFixtureWriter.java.txt` to `RelationFixtureWriter.java`. +3. Compile all three against `gremlin-shaded-3.5.1.jar`, then run `RelationFixtureWriter` with that jar and the output classes on the classpath. +4. Preserve the ASF header when saving the printed properties, and compare the resulting payloads with the committed fixture before accepting a change. + +## Invariants + +Populated typed arrays, matrices and nested values exercise repeated class descriptors and nullable components. The interleaved sample combines legacy and canonical scalar/array descriptors and repeats arrays to check object identity. New writers retain canonical default ordinal encoding. This fixture tests enum encoding compatibility; it does not establish deployed use of enum OBJECT properties. + +Fixture SHA-256: `93ee6acba21f94737cae3f929931fc09c879b930f47f0d43697a2b7f2654d1b4`. diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/store-index-rows.properties b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/store-index-rows.properties new file mode 100644 index 0000000000..7f5a599ed4 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/store-index-rows.properties @@ -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. + # +secondary.name=53493a363a616c696365000801 +secondary.value=00414141426472732f576d413d +secondary.expiry=1609459260000 +int.name=a000000007800000070801 +int.value=00414141426472732f576d413d +int.expiry=1609459260000 +long.name=a20000000880000000000000070801 +long.value=00414141426472732f576d413d +long.expiry=1609459260000 +system.name=56493a2d313a34000801 +system.value=00414141426472732f576d413d +system.expiry=1609459260000 +system.edge.name=45493a2d323a35007e08018208050805323032362178000802 +system.edge.value=00414141426472732f576d413d +system.edge.expiry=1609459260000 diff --git a/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/struct-tagged-properties.hex b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/struct-tagged-properties.hex new file mode 100644 index 0000000000..0602e40ed4 --- /dev/null +++ b/hugegraph-struct/src/test/resources/compatibility/2f827d6e8/struct-tagged-properties.hex @@ -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. + # +BOOLEAN=4201 +BYTE=438fffffff79 +INT=448ffff8bb40 +LONG=4581808080808080808000 +FLOAT=463fa00000 +DOUBLE=47c05ec80000000000 +DATE=4aaeebd9f9e07b +TEXT=480be5b19ee680a7f09f988000 +BLOB=490500ff017f80 +UUID=4b0123456789abcdef0123456789abcdef +list=8403078fffffff7f07 +set=c402078fffffff7f diff --git a/install-dist/release-docs/LICENSE b/install-dist/release-docs/LICENSE index b721884ff0..6e27cb1e2e 100644 --- a/install-dist/release-docs/LICENSE +++ b/install-dist/release-docs/LICENSE @@ -217,9 +217,9 @@ Apache 2.0 licenses The following components are provided under the Apache License. See project link for details. The text of each license is the standard Apache 2.0 license. -hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/Nameable.java from https://github.com/JanusGraph/janusgraph -hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Cardinality.java from https://github.com/JanusGraph/janusgraph -hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/StringEncoding.java from https://github.com/JanusGraph/janusgraph +hugegraph-struct/src/main/java/org/apache/hugegraph/type/Namifiable.java from https://github.com/JanusGraph/janusgraph +hugegraph-struct/src/main/java/org/apache/hugegraph/type/define/Cardinality.java from https://github.com/JanusGraph/janusgraph +hugegraph-struct/src/main/java/org/apache/hugegraph/util/StringEncoding.java from https://github.com/JanusGraph/janusgraph hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeScriptTraversal.java from https://github.com/apache/tinkerpop hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/ProcessBasicSuite.java from https://github.com/apache/tinkerpop hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/StructureBasicSuite.java from https://github.com/apache/tinkerpop @@ -258,16 +258,27 @@ Third party Apache 2.0 licenses The following components are provided under the Apache 2.0 License. See project link for details. Per-component LICENSE-[project].txt files are included only when additional non-Apache-2.0 license text is required for a bundled component. - - https://central.sonatype.com/artifact/com.alibaba/fastjson/1.2.83 -> Apache 2.0 +The Netty modules share licenses/LICENSE-netty-4.1.137.Final.txt; tcnative +modules use licenses/LICENSE-netty-tcnative-2.0.81.Final.txt. +The shaded gRPC transport uses licenses/LICENSE-grpc-netty-shaded-1.55.3.txt. +Arthas 4.1.5 includes shaded Jackson, FastDoubleParser, ASM 9.9, Logback 1.2.13 +and RSyntaxTextArea 3.3.4. +Their additional MIT, BSD, Boost and EPL 1.0 license texts and source locations +are in licenses/LICENSE-arthas-packaging-4.1.5.txt. +The same bundle supplies the agent, boot, client, core and spy JARs unpacked +under lib/arthas for offline local diagnosis. +Paths referenced by upstream licenses are reproduced as labeled sections in +the corresponding per-component license file. + + https://central.sonatype.com/artifact/at.yawk.lz4/lz4-java/1.8.1 -> Apache 2.0 + https://central.sonatype.com/artifact/com.alibaba/fastjson/1.2.84 -> Apache 2.0 https://central.sonatype.com/artifact/com.alipay.sofa.common/sofa-common-tools/1.0.12 -> Apache 2.0 https://central.sonatype.com/artifact/com.alipay.sofa.lookout/lookout-api/1.4.1 -> Apache 2.0 https://central.sonatype.com/artifact/com.alipay.sofa/bolt/1.6.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.alipay.sofa/bolt/1.6.4 -> Apache 2.0 https://central.sonatype.com/artifact/com.alipay.sofa/hessian/3.3.6 -> Apache 2.0 https://central.sonatype.com/artifact/com.alipay.sofa/hessian/3.3.7 -> Apache 2.0 - https://central.sonatype.com/artifact/com.alipay.sofa/jraft-core/1.3.11 -> Apache 2.0 - https://central.sonatype.com/artifact/com.alipay.sofa/jraft-core/1.3.13 -> Apache 2.0 + https://central.sonatype.com/artifact/com.alipay.sofa/jraft-core/1.3.14 -> Apache 2.0 https://central.sonatype.com/artifact/com.alipay.sofa/sofa-rpc-all/5.7.6 -> Apache 2.0 https://central.sonatype.com/artifact/com.alipay.sofa/tracer-core/3.0.8 -> Apache 2.0 https://central.sonatype.com/artifact/com.beust/jcommander/1.30 -> Apache 2.0 @@ -281,16 +292,17 @@ non-Apache-2.0 license text is required for a bundled component. https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-core/2.13.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-core/2.14.0-rc1 -> Apache 2.0 https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-core/2.15.2 -> Apache 2.0 - https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-databind/2.12.6.1 -> Apache 2.0 + https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-databind/2.12.7.1 -> Apache 2.0 https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-databind/2.13.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-databind/2.13.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-databind/2.14.0-rc1 -> Apache 2.0 https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-databind/2.15.2 -> Apache 2.0 - https://central.sonatype.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.11.2 -> Apache 2.0 - https://central.sonatype.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.9.3 -> Apache 2.0 - https://central.sonatype.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.12.6 -> Apache 2.0 + https://central.sonatype.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.15.2 -> Apache 2.0 + https://central.sonatype.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.12.7 -> Apache 2.0 + https://central.sonatype.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.15.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.11.2 -> Apache 2.0 - https://central.sonatype.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.12.6 -> Apache 2.0 + https://central.sonatype.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.12.7 -> Apache 2.0 + https://central.sonatype.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-base/2.15.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-json-provider/2.15.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/2.14.0-rc1 -> Apache 2.0 @@ -300,8 +312,10 @@ non-Apache-2.0 license text is required for a bundled component. https://central.sonatype.com/artifact/com.fasterxml.jackson.module/jackson-module-jakarta-xmlbind-annotations/2.15.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.fasterxml.jackson.module/jackson-module-jaxb-annotations/2.14.0-rc1 -> Apache 2.0 https://central.sonatype.com/artifact/com.fasterxml.jackson.module/jackson-module-jaxb-annotations/2.15.2 -> Apache 2.0 - https://central.sonatype.com/artifact/com.fasterxml.jackson.module/jackson-module-parameter-names/2.12.6 -> Apache 2.0 + https://central.sonatype.com/artifact/com.fasterxml.jackson.module/jackson-module-parameter-names/2.12.7 -> Apache 2.0 + https://central.sonatype.com/artifact/com.fasterxml.jackson.module/jackson-module-parameter-names/2.15.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.github.ben-manes.caffeine/caffeine/2.3.1 -> Apache 2.0 + https://central.sonatype.com/artifact/com.github.javaparser/javaparser-core/3.26.3 -> Apache 2.0 https://central.sonatype.com/artifact/com.github.jbellis/jamm/0.3.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.github.mifmif/generex/1.0.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.github.stephenc.findbugs/findbugs-annotations/1.3.9-1 -> Apache 2.0 @@ -309,50 +323,56 @@ non-Apache-2.0 license text is required for a bundled component. https://central.sonatype.com/artifact/com.google.android/annotations/4.1.1.4 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.api.grpc/proto-google-common-protos/1.17.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.api.grpc/proto-google-common-protos/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/com.google.api.grpc/proto-google-common-protos/2.9.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.auto.service/auto-service-annotations/1.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.code.findbugs/jsr305/3.0.1 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.code.findbugs/jsr305/3.0.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.code.gson/gson/2.8.6 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.code.gson/gson/2.8.9 -> Apache 2.0 + https://central.sonatype.com/artifact/com.google.code.gson/gson/2.9.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.errorprone/error_prone_annotations/2.1.3 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.errorprone/error_prone_annotations/2.10.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.errorprone/error_prone_annotations/2.18.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.errorprone/error_prone_annotations/2.3.4 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.errorprone/error_prone_annotations/2.4.0 -> Apache 2.0 + https://central.sonatype.com/artifact/com.google.errorprone/error_prone_annotations/2.11.0 -> Apache 2.0 + https://central.sonatype.com/artifact/com.google.errorprone/error_prone_annotations/2.48.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.guava/failureaccess/1.0.1 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.guava/guava/25.1-jre -> Apache 2.0 + https://central.sonatype.com/artifact/com.google.guava/guava/27.1-jre -> Apache 2.0 https://central.sonatype.com/artifact/com.google.guava/guava/30.0-jre -> Apache 2.0 https://central.sonatype.com/artifact/com.google.guava/guava/30.1-android -> Apache 2.0 https://central.sonatype.com/artifact/com.google.guava/guava/31.0.1-android -> Apache 2.0 https://central.sonatype.com/artifact/com.google.guava/guava/32.0.1-android -> Apache 2.0 + https://central.sonatype.com/artifact/com.google.guava/guava/31.1-android -> Apache 2.0 + https://central.sonatype.com/artifact/com.google.guava/guava/31.1-jre -> Apache 2.0 https://central.sonatype.com/artifact/com.google.guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava -> Apache 2.0 https://central.sonatype.com/artifact/com.google.j2objc/j2objc-annotations/1.1 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.j2objc/j2objc-annotations/1.3 -> Apache 2.0 https://central.sonatype.com/artifact/com.google.j2objc/j2objc-annotations/2.8 -> Apache 2.0 + https://central.sonatype.com/artifact/com.hankcs/hanlp/portable-1.5.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.hankcs/hanlp/portable-1.8.3 -> Apache 2.0 https://central.sonatype.com/artifact/com.huaban/jieba-analysis/1.0.2 -> Apache 2.0 https://central.sonatype.com/artifact/com.janeluo/ikanalyzer/2012_u6 -> Apache 2.0 https://central.sonatype.com/artifact/com.jayway.jsonpath/json-path/2.5.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.lmax/disruptor/3.3.7 -> Apache 2.0 https://central.sonatype.com/artifact/com.lmax/disruptor/3.4.1 -> Apache 2.0 - https://central.sonatype.com/artifact/com.nimbusds/nimbus-jose-jwt/4.41.2 -> Apache 2.0 + https://central.sonatype.com/artifact/com.nimbusds/nimbus-jose-jwt/8.2.1 -> Apache 2.0 https://central.sonatype.com/artifact/com.squareup.okhttp3/logging-interceptor/3.12.12 -> Apache 2.0 https://central.sonatype.com/artifact/com.squareup.okhttp3/logging-interceptor/4.10.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.squareup.okhttp3/okhttp/3.12.12 -> Apache 2.0 https://central.sonatype.com/artifact/com.squareup.okhttp3/okhttp/4.10.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.squareup.okio/okio-jvm/3.0.0 -> Apache 2.0 https://central.sonatype.com/artifact/com.squareup.okio/okio/1.15.0 -> Apache 2.0 - https://central.sonatype.com/artifact/com.squareup/javapoet/1.8.0 -> Apache 2.0 - https://central.sonatype.com/artifact/com.taobao.arthas/arthas-agent-attach/3.6.4 -> Apache 2.0 - https://central.sonatype.com/artifact/com.taobao.arthas/arthas-agent-attach/3.7.1 -> Apache 2.0 - https://central.sonatype.com/artifact/com.taobao.arthas/arthas-packaging/3.6.4 -> Apache 2.0 - https://central.sonatype.com/artifact/com.taobao.arthas/arthas-packaging/3.7.1 -> Apache 2.0 + https://central.sonatype.com/artifact/com.squareup/javapoet/1.13.0 -> Apache 2.0 + https://central.sonatype.com/artifact/com.taobao.arthas/arthas-agent-attach/4.1.5 -> Apache 2.0 + https://central.sonatype.com/artifact/com.taobao.arthas/arthas-packaging/4.1.5 -> Apache 2.0 https://central.sonatype.com/artifact/com.vaadin.external.google/android-json/0.0.20131108.vaadin1 -> Apache 2.0 https://central.sonatype.com/artifact/commons-beanutils/commons-beanutils/1.9.4 -> Apache 2.0 + https://central.sonatype.com/artifact/commons-cli/commons-cli/1.5.0 -> Apache 2.0 https://central.sonatype.com/artifact/commons-codec/commons-codec/1.13 -> Apache 2.0 https://central.sonatype.com/artifact/commons-codec/commons-codec/1.15 -> Apache 2.0 https://central.sonatype.com/artifact/commons-collections/commons-collections/3.2.2 -> Apache 2.0 - https://central.sonatype.com/artifact/commons-cli/commons-cli/1.5.0 -> Apache 2.0 https://central.sonatype.com/artifact/commons-configuration/commons-configuration/1.10 -> Apache 2.0 https://central.sonatype.com/artifact/commons-io/commons-io/2.12.0 -> Apache 2.0 https://central.sonatype.com/artifact/commons-io/commons-io/2.7 -> Apache 2.0 @@ -360,7 +380,7 @@ non-Apache-2.0 license text is required for a bundled component. https://central.sonatype.com/artifact/commons-lang/commons-lang/2.6 -> Apache 2.0 https://central.sonatype.com/artifact/commons-logging/commons-logging/1.1.1 -> Apache 2.0 https://central.sonatype.com/artifact/commons-logging/commons-logging/1.2 -> Apache 2.0 - https://central.sonatype.com/artifact/info.picocli/picocli/4.3.2 -> Apache 2.0 + https://central.sonatype.com/artifact/info.picocli/picocli/4.7.6 -> Apache 2.0 https://central.sonatype.com/artifact/io.dropwizard.metrics/metrics-annotation/4.2.4 -> Apache 2.0 https://central.sonatype.com/artifact/io.dropwizard.metrics/metrics-core/4.0.2 -> Apache 2.0 https://central.sonatype.com/artifact/io.dropwizard.metrics/metrics-core/4.2.4 -> Apache 2.0 @@ -389,62 +409,63 @@ non-Apache-2.0 license text is required for a bundled component. https://central.sonatype.com/artifact/io.fabric8/kubernetes-model-scheduling/5.6.0 -> Apache 2.0 https://central.sonatype.com/artifact/io.fabric8/kubernetes-model-storageclass/5.6.0 -> Apache 2.0 https://central.sonatype.com/artifact/io.fabric8/zjsonpatch/0.3.0 -> Apache 2.0 + https://central.sonatype.com/artifact/io.github.hakky54/ayza-for-netty/10.0.0 -> Apache 2.0 + https://central.sonatype.com/artifact/io.github.hakky54/ayza/10.0.0 -> Apache 2.0 https://central.sonatype.com/artifact/io.github.lognet/grpc-spring-boot-starter/4.5.5 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-api/1.28.1 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-api/1.39.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-api/1.47.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-context/1.28.1 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-context/1.39.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-context/1.47.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-core/1.28.1 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-core/1.39.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-core/1.47.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-grpclb/1.39.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-netty-shaded/1.28.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-netty-shaded/1.39.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-netty-shaded/1.47.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-netty/1.39.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-netty/1.47.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-protobuf-lite/1.28.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-protobuf-lite/1.39.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-protobuf/1.28.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-protobuf/1.39.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-services/1.39.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-stub/1.28.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-stub/1.39.0 -> Apache 2.0 - https://central.sonatype.com/artifact/io.grpc/grpc-stub/1.47.0 -> Apache 2.0 + https://central.sonatype.com/artifact/io.grpc/grpc-api/1.55.3 -> Apache 2.0 + https://central.sonatype.com/artifact/io.grpc/grpc-context/1.55.3 -> Apache 2.0 + https://central.sonatype.com/artifact/io.grpc/grpc-core/1.55.3 -> Apache 2.0 + https://central.sonatype.com/artifact/io.grpc/grpc-grpclb/1.55.3 -> Apache 2.0 + https://central.sonatype.com/artifact/io.grpc/grpc-netty/1.55.3 -> Apache 2.0 + https://central.sonatype.com/artifact/io.grpc/grpc-netty-shaded/1.55.3 -> Apache 2.0 + https://central.sonatype.com/artifact/io.grpc/grpc-protobuf/1.55.3 -> Apache 2.0 + https://central.sonatype.com/artifact/io.grpc/grpc-protobuf-lite/1.55.3 -> Apache 2.0 + https://central.sonatype.com/artifact/io.grpc/grpc-services/1.55.3 -> Apache 2.0 + https://central.sonatype.com/artifact/io.grpc/grpc-stub/1.55.3 -> Apache 2.0 https://central.sonatype.com/artifact/io.jsonwebtoken/jjwt-api/0.11.2 -> Apache 2.0 https://central.sonatype.com/artifact/io.jsonwebtoken/jjwt-api/0.11.5 -> Apache 2.0 https://central.sonatype.com/artifact/io.jsonwebtoken/jjwt-impl/0.11.2 -> Apache 2.0 https://central.sonatype.com/artifact/io.jsonwebtoken/jjwt-impl/0.11.5 -> Apache 2.0 + https://central.sonatype.com/artifact/io.jsonwebtoken/jjwt-jackson/0.11.2 -> Apache 2.0 https://central.sonatype.com/artifact/io.jsonwebtoken/jjwt-jackson/0.11.5 -> Apache 2.0 https://central.sonatype.com/artifact/io.micrometer/micrometer-core/1.7.12 -> Apache 2.0 https://central.sonatype.com/artifact/io.micrometer/micrometer-registry-prometheus/1.7.12 -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-all/4.1.42.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-all/4.1.61.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-buffer/4.1.52.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-buffer/4.1.72.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-codec-http/4.1.52.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-codec-http/4.1.72.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-codec-http2/4.1.52.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-codec-http2/4.1.72.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-codec-socks/4.1.52.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-codec-socks/4.1.72.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-codec/4.1.52.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-codec/4.1.72.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-common/4.1.52.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-common/4.1.72.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-handler-proxy/4.1.52.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-handler-proxy/4.1.72.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-handler/4.1.52.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-handler/4.1.72.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-resolver/4.1.52.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-resolver/4.1.72.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-tcnative-boringssl-static/2.0.25.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-tcnative-classes/2.0.46.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-transport-native-unix-common/4.1.72.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-transport/4.1.52.Final -> Apache 2.0 - https://central.sonatype.com/artifact/io.netty/netty-transport/4.1.72.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-all/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-buffer/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-codec-dns/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-codec-haproxy/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-codec-http/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-codec-http2/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-codec-memcache/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-codec-mqtt/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-codec-redis/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-codec-smtp/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-codec-socks/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-codec-stomp/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-codec-xml/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-codec/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-common/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-handler-proxy/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-handler-ssl-ocsp/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-handler/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-resolver-dns-classes-macos/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-resolver-dns-native-macos/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-resolver-dns/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-resolver/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-tcnative-boringssl-static/2.0.81.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-tcnative-classes/2.0.81.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-transport-classes-epoll/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-transport-classes-kqueue/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-transport-native-epoll/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-transport-native-kqueue/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-transport-native-unix-common/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-transport-rxtx/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-transport-sctp/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-transport-udt/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.netty/netty-transport/4.1.137.Final -> Apache 2.0 + https://central.sonatype.com/artifact/io.opentelemetry.semconv/opentelemetry-semconv/1.29.0-alpha -> Apache 2.0 + https://central.sonatype.com/artifact/io.opentelemetry/opentelemetry-api/1.49.0 -> Apache 2.0 + https://central.sonatype.com/artifact/io.opentelemetry/opentelemetry-context/1.49.0 -> Apache 2.0 https://central.sonatype.com/artifact/io.opentracing/opentracing-api/0.22.0 -> Apache 2.0 https://central.sonatype.com/artifact/io.opentracing/opentracing-mock/0.22.0 -> Apache 2.0 https://central.sonatype.com/artifact/io.opentracing/opentracing-noop/0.22.0 -> Apache 2.0 @@ -473,10 +494,10 @@ non-Apache-2.0 license text is required for a bundled component. https://central.sonatype.com/artifact/javax.validation/validation-api/1.1.0.Final -> Apache 2.0 https://central.sonatype.com/artifact/joda-time/joda-time/2.10.8 -> Apache 2.0 https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.10.20 -> Apache 2.0 - https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.10.5 -> Apache 2.0 - https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.11.6 -> Apache 2.0 + https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.18.3 -> Apache 2.0 + https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.17.6 -> Apache 2.0 https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy/1.10.20 -> Apache 2.0 - https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy/1.10.5 -> Apache 2.0 + https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy/1.17.6 -> Apache 2.0 https://central.sonatype.com/artifact/net.java.dev.jna/jna/5.12.1 -> Apache 2.0 https://central.sonatype.com/artifact/net.java.dev.jna/jna/5.5.0 -> Apache 2.0 https://central.sonatype.com/artifact/net.java.dev.jna/jna/5.7.0 -> Apache 2.0 @@ -487,124 +508,123 @@ non-Apache-2.0 license text is required for a bundled component. https://central.sonatype.com/artifact/org.ansj/ansj_seg/5.1.6 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.commons/commons-collections4/4.4 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.commons/commons-compress/1.21 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.commons/commons-configuration2/2.8.0 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.commons/commons-configuration2/2.10.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.commons/commons-configuration2/2.9.0 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.commons/commons-crypto/1.1.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.commons/commons-lang3/3.12.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.commons/commons-lang3/3.13.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.commons/commons-lang3/3.18.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.commons/commons-pool2/2.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.commons/commons-text/1.10.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.commons/commons-text/1.9 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.commons/commons-text/1.11.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.fury/fury-core/0.9.0 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.groovy/groovy-cli-picocli/4.0.25 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.groovy/groovy-console/4.0.25 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.groovy/groovy-groovysh/4.0.25 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.groovy/groovy-json/4.0.25 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.groovy/groovy-jsr223/4.0.25 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.groovy/groovy-swing/4.0.25 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.groovy/groovy-templates/4.0.25 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.groovy/groovy-xml/4.0.25 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.groovy/groovy/4.0.25 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.hbase.thirdparty/hbase-shaded-gson/4.1.13 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.hbase.thirdparty/hbase-shaded-miscellaneous/4.1.13 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.hbase.thirdparty/hbase-shaded-netty/4.1.13 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.hbase.thirdparty/hbase-shaded-protobuf/4.1.13 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.hbase.thirdparty/hbase-unsafe/4.1.13 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.hbase/hbase-client/2.6.5 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.hbase/hbase-common/2.6.5 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.hbase/hbase-endpoint/2.6.5 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.hbase/hbase-hadoop-compat/2.6.5 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.hbase/hbase-hadoop2-compat/2.6.5 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.hbase/hbase-logging/2.6.5 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.hbase/hbase-metrics/2.6.5 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.hbase/hbase-metrics-api/2.6.5 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.hbase/hbase-protocol/2.6.5 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.hbase/hbase-metrics/2.6.5 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.hbase/hbase-protocol-shaded/2.6.5 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.hbase/hbase-protocol/2.6.5 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.hbase/hbase-shaded-client/2.6.5 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.hbase/hbase-zookeeper/2.6.5 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.hbase.thirdparty/hbase-shaded-gson/4.1.13 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.hbase.thirdparty/hbase-shaded-miscellaneous/4.1.13 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.hbase.thirdparty/hbase-shaded-netty/4.1.13 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.hbase.thirdparty/hbase-shaded-protobuf/4.1.13 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.hbase.thirdparty/hbase-unsafe/4.1.13 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.htrace/htrace-core4/4.1.0-incubating -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.htrace/htrace-core4/4.2.0-incubating -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.httpcomponents/httpclient/4.5.13 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.httpcomponents/httpcore/4.4.13 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.ivy/ivy/2.4.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerb-admin/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerb-client/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerb-common/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerb-core/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerb-crypto/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerb-identity/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerb-server/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerb-simplekdc/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerb-util/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerby-asn1/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerby-config/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerby-pkix/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerby-util/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/kerby-xdr/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.kerby/token-provider/2.0.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api/2.15.0 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.ivy/ivy/2.6.0 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerb-admin/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerb-client/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerb-common/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerb-core/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerb-crypto/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerb-identity/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerb-server/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerb-simplekdc/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerb-util/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerby-asn1/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerby-config/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerby-pkix/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerby-util/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/kerby-xdr/2.0.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.kerby/token-provider/2.0.1 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api/2.17.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api/2.17.1 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api/2.17.2 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api/2.18.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-core/2.15.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-core/2.17.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-core/2.17.1 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-core/2.17.2 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-core/2.18.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-jul/2.17.2 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl/2.15.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl/2.17.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl/2.17.1 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl/2.18.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.lucene/lucene-analyzers-common/5.2.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.lucene/lucene-analyzers-common/7.4.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.lucene/lucene-analyzers-common/8.11.2 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.lucene/lucene-analyzers-smartcn/7.4.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.lucene/lucene-analyzers-smartcn/8.11.2 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.lucene/lucene-backward-codecs/5.2.1 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.lucene/lucene-core/7.4.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.lucene/lucene-core/8.11.2 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.lucene/lucene-misc/5.2.1 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.lucene/lucene-queries/4.7.2 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.lucene/lucene-queries/5.2.1 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.lucene/lucene-queryparser/4.7.2 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.lucene/lucene-queryparser/5.2.1 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.lucene/lucene-sandbox/4.7.2 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.lucene/lucene-sandbox/5.2.1 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.lucene/lucene-suggest/5.2.1 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-console/3.5.1 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-core/3.5.1 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-driver/3.5.1 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-groovy-test/3.2.11 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-groovy/3.5.1 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-server/3.5.1 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-shaded/3.5.1 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-test/3.5.1 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.tinkerpop/tinkergraph-gremlin/3.5.1 -> Apache 2.0 - https://central.sonatype.com/artifact/org.apache.tomcat.embed/tomcat-embed-el/9.0.63 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-console/3.8.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-core/3.8.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-driver/3.8.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-groovy/3.8.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-language/3.8.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-server/3.8.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-shaded/3.8.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-test/3.8.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.tinkerpop/gremlin-util/3.8.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.tinkerpop/tinkergraph-gremlin/3.8.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.tomcat.embed/tomcat-embed-el/9.0.75 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.yetus/audience-annotations/0.13.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.apache.yetus/audience-annotations/0.5.0 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.zookeeper/zookeeper-jute/3.8.6 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.zookeeper/zookeeper/3.8.6 -> Apache 2.0 https://central.sonatype.com/artifact/org.apiguardian/apiguardian-api/1.1.0 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apiguardian/apiguardian-api/1.1.2 -> Apache 2.0 https://central.sonatype.com/artifact/org.assertj/assertj-core/3.19.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.caffinitas.ohc/ohc-core/0.7.4 -> Apache 2.0 - https://central.sonatype.com/artifact/org.codehaus.groovy/groovy-cli-picocli/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.codehaus.groovy/groovy-console/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.codehaus.groovy/groovy-groovysh/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.codehaus.groovy/groovy-json/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.codehaus.groovy/groovy-jsr223/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.codehaus.groovy/groovy-swing/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.codehaus.groovy/groovy-templates/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.codehaus.groovy/groovy-xml/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.codehaus.groovy/groovy/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/javax-websocket-client-impl/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/javax-websocket-server-impl/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/websocket-api/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/websocket-client/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/websocket-common/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/websocket-server/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/websocket-servlet/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-annotations/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-client/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-continuation/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-http/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-io/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-plus/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-security/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-server/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-servlet/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-servlets/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-util-ajax/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-util/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-webapp/9.4.46.v20220331 -> Apache 2.0 - https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-xml/9.4.46.v20220331 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/javax-websocket-client-impl/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/javax-websocket-server-impl/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/websocket-api/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/websocket-client/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/websocket-common/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/websocket-server/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty.websocket/websocket-servlet/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-annotations/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-client/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-continuation/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-http/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-io/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-plus/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-security/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-server/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-servlet/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-servlets/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-util-ajax/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-util/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-webapp/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.eclipse.jetty/jetty-xml/9.4.51.v20230217 -> Apache 2.0 + https://central.sonatype.com/artifact/org.fusesource.jansi/jansi/2.4.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.gridkit.jvmtool/sjk-agent/0.22 -> Apache 2.0 https://central.sonatype.com/artifact/org.gridkit.jvmtool/sjk-cli/0.22 -> Apache 2.0 https://central.sonatype.com/artifact/org.gridkit.jvmtool/sjk-core/0.22 -> Apache 2.0 @@ -613,7 +633,6 @@ non-Apache-2.0 license text is required for a bundled component. https://central.sonatype.com/artifact/org.gridkit.jvmtool/sjk-stacktrace/0.22 -> Apache 2.0 https://central.sonatype.com/artifact/org.gridkit.lab/jvm-attach-api/1.5 -> Apache 2.0 https://central.sonatype.com/artifact/org.javassist/javassist/3.21.0-GA -> Apache 2.0 - https://central.sonatype.com/artifact/org.javassist/javassist/3.24.0-GA -> Apache 2.0 https://central.sonatype.com/artifact/org.javassist/javassist/3.28.0-GA -> Apache 2.0 https://central.sonatype.com/artifact/org.javatuples/javatuples/1.2 -> Apache 2.0 https://central.sonatype.com/artifact/org.jctools/jctools-core/2.1.1 -> Apache 2.0 @@ -628,8 +647,8 @@ non-Apache-2.0 license text is required for a bundled component. https://central.sonatype.com/artifact/org.lz4/lz4-java/1.4.0 -> Apache 2.0 https://central.sonatype.com/artifact/org.lz4/lz4-java/1.8.1 -> Apache 2.0 https://central.sonatype.com/artifact/org.nlpcn/nlp-lang/1.7.7 -> Apache 2.0 - https://central.sonatype.com/artifact/org.objenesis/objenesis/2.6 -> Apache 2.0 https://central.sonatype.com/artifact/org.objenesis/objenesis/3.2 -> Apache 2.0 + https://central.sonatype.com/artifact/org.objenesis/objenesis/3.3 -> Apache 2.0 https://central.sonatype.com/artifact/org.opencypher.gremlin/cypher-gremlin-extensions/1.0.4 -> Apache 2.0 https://central.sonatype.com/artifact/org.opencypher.gremlin/translation/1.0.4 -> Apache 2.0 https://central.sonatype.com/artifact/org.opencypher/ast-9.0/9.0.20190305 -> Apache 2.0 @@ -645,60 +664,46 @@ non-Apache-2.0 license text is required for a bundled component. https://central.sonatype.com/artifact/org.perfkit.sjk.parsers/sjk-jfr5/0.5 -> Apache 2.0 https://central.sonatype.com/artifact/org.perfkit.sjk.parsers/sjk-jfr6/0.7 -> Apache 2.0 https://central.sonatype.com/artifact/org.perfkit.sjk.parsers/sjk-nps/0.9 -> Apache 2.0 - https://central.sonatype.com/artifact/org.powermock/powermock-api-mockito2/2.0.0-RC.3 -> Apache 2.0 - https://central.sonatype.com/artifact/org.powermock/powermock-api-support/2.0.0-RC.3 -> Apache 2.0 - https://central.sonatype.com/artifact/org.powermock/powermock-classloading-base/2.0.0-RC.3 -> Apache 2.0 - https://central.sonatype.com/artifact/org.powermock/powermock-classloading-xstream/2.0.0-RC.3 -> Apache 2.0 - https://central.sonatype.com/artifact/org.powermock/powermock-core/2.0.0-RC.3 -> Apache 2.0 - https://central.sonatype.com/artifact/org.powermock/powermock-module-junit4-common/2.0.0-RC.3 -> Apache 2.0 - https://central.sonatype.com/artifact/org.powermock/powermock-module-junit4-rule/2.0.0-RC.3 -> Apache 2.0 - https://central.sonatype.com/artifact/org.powermock/powermock-module-junit4/2.0.0-RC.3 -> Apache 2.0 - https://central.sonatype.com/artifact/org.powermock/powermock-reflect/2.0.0-RC.3 -> Apache 2.0 https://central.sonatype.com/artifact/org.roaringbitmap/RoaringBitmap/0.9.38 -> Apache 2.0 https://central.sonatype.com/artifact/org.roaringbitmap/shims/0.9.38 -> Apache 2.0 - https://central.sonatype.com/artifact/org.rocksdb/rocksdbjni/6.29.5 -> Apache 2.0 - https://central.sonatype.com/artifact/org.rocksdb/rocksdbjni/7.7.3 -> Apache 2.0 https://central.sonatype.com/artifact/org.rocksdb/rocksdbjni/8.10.2 -> Apache 2.0 - https://central.sonatype.com/artifact/org.skyscreamer/jsonassert/1.5.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-actuator-autoconfigure/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-actuator/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-autoconfigure/2.5.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-autoconfigure/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-actuator/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-jetty/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-json/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-log4j2/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-test/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-web/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter/2.5.0 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-test-autoconfigure/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-test/2.5.14 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework.boot/spring-boot/2.5.14 -> Apache 2.0 + https://central.sonatype.com/artifact/org.skyscreamer/jsonassert/1.5.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-actuator-autoconfigure/2.5.15 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-actuator/2.5.15 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-autoconfigure/2.5.15 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-jarmode-layertools/2.7.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-actuator/2.5.15 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-jetty/2.5.15 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-json/2.5.15 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-log4j2/2.5.15 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-test/2.5.15 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-web/2.5.15 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter/2.5.15 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-test-autoconfigure/2.5.15 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-test/2.5.15 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework.boot/spring-boot/2.5.15 -> Apache 2.0 https://central.sonatype.com/artifact/org.springframework.security/spring-security-core/5.8.3 -> Apache 2.0 https://central.sonatype.com/artifact/org.springframework.security/spring-security-crypto/5.8.3 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework/spring-aop/5.3.20 -> Apache 2.0 https://central.sonatype.com/artifact/org.springframework/spring-aop/5.3.27 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework/spring-beans/5.3.20 -> Apache 2.0 https://central.sonatype.com/artifact/org.springframework/spring-beans/5.3.27 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework/spring-context-support/5.3.20 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework/spring-context/5.3.20 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework/spring-context-support/5.3.27 -> Apache 2.0 https://central.sonatype.com/artifact/org.springframework/spring-context/5.3.27 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework/spring-core/5.3.20 -> Apache 2.0 https://central.sonatype.com/artifact/org.springframework/spring-core/5.3.27 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework/spring-expression/5.3.20 -> Apache 2.0 https://central.sonatype.com/artifact/org.springframework/spring-expression/5.3.27 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework/spring-jcl/5.3.20 -> Apache 2.0 https://central.sonatype.com/artifact/org.springframework/spring-jcl/5.3.27 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework/spring-web/5.3.20 -> Apache 2.0 - https://central.sonatype.com/artifact/org.springframework/spring-webmvc/5.3.20 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework/spring-web/5.3.27 -> Apache 2.0 + https://central.sonatype.com/artifact/org.springframework/spring-webmvc/5.3.27 -> Apache 2.0 https://central.sonatype.com/artifact/org.xmlunit/xmlunit-core/2.8.4 -> Apache 2.0 - https://central.sonatype.com/artifact/org.yaml/snakeyaml/1.18 -> Apache 2.0 - https://central.sonatype.com/artifact/org.yaml/snakeyaml/1.26 -> Apache 2.0 - https://central.sonatype.com/artifact/org.yaml/snakeyaml/1.27 -> Apache 2.0 - https://central.sonatype.com/artifact/org.yaml/snakeyaml/1.28 -> Apache 2.0 https://central.sonatype.com/artifact/org.yaml/snakeyaml/2.2 -> Apache 2.0 - https://central.sonatype.com/artifact/org.zeroturnaround/zt-zip/1.14 -> Apache 2.0 + https://central.sonatype.com/artifact/org.zeroturnaround/zt-zip/1.16 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.commons/commons-pool2/2.0 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.lucene/lucene-analyzers-common/5.2.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.lucene/lucene-backward-codecs/5.2.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.lucene/lucene-misc/5.2.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.lucene/lucene-queries/5.2.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.lucene/lucene-queryparser/5.2.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.lucene/lucene-sandbox/5.2.1 -> Apache 2.0 + https://central.sonatype.com/artifact/org.apache.lucene/lucene-suggest/5.2.1 -> Apache 2.0 ======================================================================== Third party BSD licenses @@ -706,15 +711,11 @@ Third party BSD licenses The following components are provided under the BSD License. See project link for details. The text of each license is also included in licenses/LICENSE-[project].txt. - https://central.sonatype.com/artifact/com.belerweb/pinyin4j/2.5.0 -> BSD - https://central.sonatype.com/artifact/com.jcabi/jcabi-log/0.14 -> BSD - https://central.sonatype.com/artifact/com.jcabi/jcabi-manifests/1.1 -> BSD - https://central.sonatype.com/artifact/com.thoughtworks.xstream/xstream/1.4.10 -> BSD https://central.sonatype.com/artifact/dk.brics.automaton/automaton/1.11-8 -> BSD https://central.sonatype.com/artifact/jline/jline/2.14.6 -> BSD https://central.sonatype.com/artifact/org.hamcrest/hamcrest-core/1.3 -> BSD - https://central.sonatype.com/artifact/org.ow2.asm/asm/5.0.4 -> BSD https://central.sonatype.com/artifact/org.ow2.asm/asm/6.0 -> BSD + https://central.sonatype.com/artifact/com.belerweb/pinyin4j/2.5.0 -> BSD ======================================================================== Third party MIT licenses @@ -724,19 +725,44 @@ The text of each license is also included in licenses/LICENSE-[project].txt. https://central.sonatype.com/artifact/com.lihaoyi/fastparse_2.12/2.0.4 -> MIT https://central.sonatype.com/artifact/com.lihaoyi/sourcecode_2.12/0.1.4 -> MIT + https://central.sonatype.com/artifact/io.cucumber/ci-environment/10.0.1 -> MIT + https://central.sonatype.com/artifact/io.cucumber/cucumber-core/7.21.1 -> MIT + https://central.sonatype.com/artifact/io.cucumber/cucumber-expressions/18.0.1 -> MIT + https://central.sonatype.com/artifact/io.cucumber/cucumber-gherkin-messages/7.21.1 -> MIT + https://central.sonatype.com/artifact/io.cucumber/cucumber-gherkin/7.21.1 -> MIT + https://central.sonatype.com/artifact/io.cucumber/cucumber-guice/7.21.1 -> MIT + https://central.sonatype.com/artifact/io.cucumber/cucumber-java/7.21.1 -> MIT + https://central.sonatype.com/artifact/io.cucumber/cucumber-junit/7.21.1 -> MIT + https://central.sonatype.com/artifact/io.cucumber/cucumber-plugin/7.21.1 -> MIT + https://central.sonatype.com/artifact/io.cucumber/datatable/7.21.1 -> MIT + https://central.sonatype.com/artifact/io.cucumber/docstring/7.21.1 -> MIT + https://central.sonatype.com/artifact/io.cucumber/gherkin/31.0.0 -> MIT + https://central.sonatype.com/artifact/io.cucumber/html-formatter/21.9.0 -> MIT + https://central.sonatype.com/artifact/io.cucumber/junit-xml-formatter/0.7.1 -> MIT + https://central.sonatype.com/artifact/io.cucumber/messages/27.2.0 -> MIT + https://central.sonatype.com/artifact/io.cucumber/query/13.6.0 -> MIT + https://central.sonatype.com/artifact/io.cucumber/tag-expressions/6.1.2 -> MIT + https://central.sonatype.com/artifact/io.cucumber/testng-xml-formatter/0.3.1 -> MIT https://central.sonatype.com/artifact/io.github.classgraph/classgraph/4.8.162 -> MIT https://central.sonatype.com/artifact/org.checkerframework/checker-compat-qual/2.5.5 -> MIT https://central.sonatype.com/artifact/org.checkerframework/checker-qual/2.0.0 -> MIT + https://central.sonatype.com/artifact/org.checkerframework/checker-qual/2.5.2 -> MIT https://central.sonatype.com/artifact/org.checkerframework/checker-qual/3.12.0 -> MIT https://central.sonatype.com/artifact/org.checkerframework/checker-qual/3.33.0 -> MIT https://central.sonatype.com/artifact/org.checkerframework/checker-qual/3.5.0 -> MIT https://central.sonatype.com/artifact/org.codehaus.mojo/animal-sniffer-annotations/1.14 -> MIT + https://central.sonatype.com/artifact/org.codehaus.mojo/animal-sniffer-annotations/1.17 -> MIT https://central.sonatype.com/artifact/org.codehaus.mojo/animal-sniffer-annotations/1.18 -> MIT https://central.sonatype.com/artifact/org.codehaus.mojo/animal-sniffer-annotations/1.19 -> MIT + https://central.sonatype.com/artifact/org.codehaus.mojo/animal-sniffer-annotations/1.21 -> MIT https://central.sonatype.com/artifact/org.mockito/mockito-core/3.3.3 -> MIT + https://central.sonatype.com/artifact/org.jruby.jcodings/jcodings/1.0.58 -> MIT + https://central.sonatype.com/artifact/org.jruby.joni/joni/2.2.1 -> MIT https://central.sonatype.com/artifact/org.mockito/mockito-core/3.9.0 -> MIT + https://central.sonatype.com/artifact/org.mockito/mockito-core/5.19.0 -> MIT https://central.sonatype.com/artifact/org.mockito/mockito-junit-jupiter/3.9.0 -> MIT https://central.sonatype.com/artifact/org.projectlombok/lombok/1.18.24 -> MIT + https://central.sonatype.com/artifact/org.projectlombok/lombok/1.18.30 -> MIT https://central.sonatype.com/artifact/org.slf4j/jcl-over-slf4j/1.7.25 -> MIT https://central.sonatype.com/artifact/org.slf4j/jul-to-slf4j/1.7.36 -> MIT https://central.sonatype.com/artifact/org.slf4j/slf4j-api/1.7.21 -> MIT @@ -795,15 +821,19 @@ The following components are provided under the BSD-3-Clause License. See projec The text of each license is also included in licenses/LICENSE-[project].txt. https://central.sonatype.com/artifact/com.google.protobuf/protobuf-java-util/3.17.2 -> BSD-3-Clause + https://central.sonatype.com/artifact/com.google.protobuf/protobuf-java-util/3.22.3 -> BSD-3-Clause https://central.sonatype.com/artifact/com.google.protobuf/protobuf-java/3.11.0 -> BSD-3-Clause https://central.sonatype.com/artifact/com.google.protobuf/protobuf-java/3.17.2 -> BSD-3-Clause https://central.sonatype.com/artifact/com.google.protobuf/protobuf-java/3.21.7 -> BSD-3-Clause https://central.sonatype.com/artifact/com.google.protobuf/protobuf-java/3.5.1 -> BSD-3-Clause + https://central.sonatype.com/artifact/com.google.protobuf/protobuf-java/3.22.3 -> BSD-3-Clause https://central.sonatype.com/artifact/com.sun.activation/jakarta.activation/2.0.0 -> BSD-3-Clause + https://central.sonatype.com/artifact/org.abego.treelayout/org.abego.treelayout.core/1.0.3 -> BSD-3-Clause + https://central.sonatype.com/artifact/org.antlr/antlr4-runtime/4.9.1 -> BSD-3-Clause https://central.sonatype.com/artifact/org.hamcrest/hamcrest/2.2 -> BSD-3-Clause - https://central.sonatype.com/artifact/org.ow2.asm/asm-analysis/9.2 -> BSD-3-Clause - https://central.sonatype.com/artifact/org.ow2.asm/asm-commons/9.2 -> BSD-3-Clause - https://central.sonatype.com/artifact/org.ow2.asm/asm-tree/9.2 -> BSD-3-Clause + https://central.sonatype.com/artifact/org.hamcrest/hamcrest/3.0 -> BSD-3-Clause + https://central.sonatype.com/artifact/org.ow2.asm/asm-commons/9.4 -> BSD-3-Clause + https://central.sonatype.com/artifact/org.ow2.asm/asm-tree/9.4 -> BSD-3-Clause https://central.sonatype.com/artifact/org.scala-lang.modules/scala-java8-compat_2.12/0.8.0 -> BSD-3-Clause https://central.sonatype.com/artifact/org.scala-lang/scala-library/2.12.7 -> BSD-3-Clause https://central.sonatype.com/artifact/org.scala-lang/scala-reflect/2.12.7 -> BSD-3-Clause @@ -864,20 +894,16 @@ Third party EPL 1.0 licenses The following components are provided under the EPL 1.0 License. See project link for details. The text of each license is also included in licenses/LICENSE-[project].txt. + https://central.sonatype.com/artifact/ch.qos.logback/logback-classic/1.2.13 -> EPL 1.0 (shaded in Arthas 4.1.5) + https://central.sonatype.com/artifact/ch.qos.logback/logback-core/1.2.13 -> EPL 1.0 (shaded in Arthas 4.1.5) https://central.sonatype.com/artifact/junit/junit/4.13.1 -> EPL 1.0 https://central.sonatype.com/artifact/junit/junit/4.13.2 -> EPL 1.0 -======================================================================== -Third party Public Domain licenses -======================================================================== -The following components are provided under the Public Domain License. See project link for details. -The text of each license is also included in licenses/LICENSE-[project].txt. - - https://central.sonatype.com/artifact/xmlpull/xmlpull/1.1.3.1 -> Public Domain - https://central.sonatype.com/artifact/xpp3/xpp3_min/1.1.4c -> Public Domain # # jansi 2.4.0 (https://github.com/fusesource/jansi) # # This product includes software developed by the Jansi project (http://fusesource.github.io/jansi/). # See licenses/LICENSE-jansi-2.4.0.txt for license details (Apache License 2.0 or LGPL 3.0+). + +Ivy 2.6.0 includes third-party BSD and MIT licensed code; see licenses/LICENSE-ivy-2.6.0.txt. diff --git a/install-dist/release-docs/NOTICE b/install-dist/release-docs/NOTICE index 99e69b7f11..2f6fa03e5e 100644 --- a/install-dist/release-docs/NOTICE +++ b/install-dist/release-docs/NOTICE @@ -52,7 +52,7 @@ 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. +THE SOFTWARE. ======================================================================== @@ -155,153 +155,6 @@ the License. ======================================================================== -Apache TinkerPop NOTICE - -======================================================================== -Apache TinkerPop -Copyright 2015-2022 The Apache Software Foundation. - ------------------------------------------------------------------------- -Activiti ------------------------------------------------------------------------- -Activiti BPM Platform -Copyright 2010-2014 Alfresco Software, Ltd. - -gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphml/GraphMLWriterHelper.java -contains DelegatingXMLStreamWriter.java and IndentingXMLStreamWriter.java from -https://github.com/Activiti/Activiti/tree/activiti-5.16/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/converter - ------------------------------------------------------------------------- -Apache Kerby ------------------------------------------------------------------------- -Apache Kerby -Copyright 2015-2017 The Apache Software Foundation - -gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/auth/JaasKrbUtil.java -from -https://github.com/apache/directory-kerby/blob/kerby-all-1.0.0/kerby-kerb/kerb-simplekdc/src/main/java/org/apache/kerby/kerberos/kerb/client/JaasKrbUtil.java - - -======================================================================== - -gremlin-console NOTICE - -======================================================================== - -Apache TinkerPop :: Gremlin Console -Copyright 2013-2021 Apache Software Foundation - -======================================================================== - -gremlin-core NOTICE - -======================================================================== -This product contains code form the JanusGraph Project: - -Apache TinkerPop :: Gremlin Core -Copyright 2013-2021 Apache Software Foundation - -======================================================================== - -gremlin-driver NOTICE - -======================================================================== - -Apache TinkerPop :: Gremlin Driver -Copyright 2013-2021 Apache Software Foundation - -======================================================================== - -gremlin-groovy NOTICE - -======================================================================== - -Apache TinkerPop :: Gremlin Groovy -Copyright 2013-2021 Apache Software Foundation - -======================================================================== - -gremlin-server NOTICE - -======================================================================== - -Apache TinkerPop :: Gremlin Server -Copyright 2013-2021 Apache Software Foundation - -======================================================================== - -gremlin-shaded NOTICE - -======================================================================== - -Apache TinkerPop :: Gremlin Shaded -Copyright 2013-2021 Apache Software Foundation - -======================================================================== - -gremlin-test NOTICE - -======================================================================== - -Apache TinkerPop :: Gremlin Test -Copyright 2013-2021 Apache Software Foundation - - -======================================================================== - -groovy-2.5.14 NOTICE - -======================================================================== -Apache Groovy -Copyright 2003-2020 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - -This product includes/uses ANTLR (http://www.antlr2.org/) -developed by Terence Parr 1989-2006 -======================================================================== - -groovy-cli-picocli NOTICE - -======================================================================== -Apache Groovy -Copyright 2003-2020 The Apache Software Foundation - -======================================================================== - -groovy-console NOTICE - -======================================================================== -Apache Groovy -Copyright 2003-2020 The Apache Software Foundation - -======================================================================== - -groovy-swing NOTICE - -======================================================================== -Apache Groovy -Copyright 2003-2020 The Apache Software Foundation - -======================================================================== - -groovy-templates NOTICE - -======================================================================== -Apache Groovy -Copyright 2003-2020 The Apache Software Foundation - -======================================================================== - -groovy-xml NOTICE - -======================================================================== -Apache Groovy -Copyright 2003-2020 The Apache Software Foundation - -======================================================================== - hbase-endpoint NOTICE ======================================================================== @@ -452,27 +305,6 @@ The Apache Software Foundation (http://www.apache.org/). ======================================================================== -ivy NOTICE - -======================================================================== -Apache Ivy (TM) -Copyright 2007-2014 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - -Portions of Ivy were originally developed at -Jayasoft SARL (http://www.jayasoft.fr/) -and are licensed to the Apache Software Foundation under the -"Software Grant License Agreement" - -SSH and SFTP support is provided by the JCraft JSch package, -which is open source software, available under -the terms of a BSD style license. -The original software and related information is available -at http://www.jcraft.com/jsch/. -======================================================================== - jackson-core NOTICE ======================================================================== @@ -517,33 +349,6 @@ in some artifacts (usually source distributions); but is always available from the source code management (SCM) system project uses. ======================================================================== -jackson-dataformat-yaml NOTICE - -======================================================================== -# Jackson JSON processor - -Jackson is a high-performance, Free/Open Source JSON processing library. -It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has -been in development since 2007. -It is currently developed by a community of developers, as well as supported -commercially by FasterXML.com. - -## Licensing - -Jackson core and extension components may be licensed under different licenses. -To find the details that apply to this artifact see the accompanying LICENSE file. -For more information, including possible other licensing options, contact -FasterXML.com (http://fasterxml.com). - -## Credits - -A list of contributors may be found from CREDITS file, which is included -in some artifacts (usually source distributions); but is always available -from the source code management (SCM) system project uses. - - -======================================================================== - jackson-jaxrs-json-provider-2.12.1 NOTICE ======================================================================== @@ -625,12 +430,12 @@ from the source code management (SCM) system project uses. ======================================================================== -kerb-admin NOTICE +log4j-api NOTICE ======================================================================== -Kerby-kerb Admin -Copyright 2014-2019 The Apache Software Foundation +Apache Log4j API +Copyright 1999-1969 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). @@ -638,25 +443,27 @@ The Apache Software Foundation (http://www.apache.org/). ======================================================================== -kerb-client NOTICE +log4j-core NOTICE ======================================================================== - -Kerby-kerb Client -Copyright 2014-2019 The Apache Software Foundation +Apache Log4j Core +Copyright 1999-2012 Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). +ResolverUtil.java +Copyright 2005-2006 Tim Fennell + ======================================================================== -kerb-common NOTICE +log4j-slf4j-impl NOTICE ======================================================================== -Kerby-kerb Common -Copyright 2014-2019 The Apache Software Foundation +Apache Log4j SLF4J Binding +Copyright 1999-1969 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). @@ -664,25 +471,27 @@ The Apache Software Foundation (http://www.apache.org/). ======================================================================== -kerb-core NOTICE +objenesis NOTICE ======================================================================== -Kerby-kerb core -Copyright 2014-2019 The Apache Software Foundation +// ------------------------------------------------------------------ +// NOTICE file corresponding to the section 4d of The Apache License, +// Version 2.0, in this case for Objenesis +// ------------------------------------------------------------------ -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). +Objenesis +Copyright 2006-2017 Joe Walnes, Henri Tremblay, Leonardo Mesquita ======================================================================== -kerb-crypto NOTICE +Commons-beanutils NOTICE ======================================================================== -Kerby-kerb Crypto -Copyright 2014-2019 The Apache Software Foundation +Apache Commons BeanUtils +Copyright 2000-2019 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). @@ -690,244 +499,32 @@ The Apache Software Foundation (http://www.apache.org/). ======================================================================== -kerb-identity NOTICE + Commons-cli NOTICE -======================================================================== + ======================================================================== -Kerby-kerb Identity -Copyright 2014-2019 The Apache Software Foundation +Apache Commons CLI +Copyright 2002-2021 The Apache Software Foundation This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). +The Apache Software Foundation (https://www.apache.org/). ======================================================================== -kerb-server NOTICE + Commons-codec NOTICE -======================================================================== + ======================================================================== -Kerby-kerb Server -Copyright 2014-2019 The Apache Software Foundation +Apache Commons Codec +Copyright 2002-2009 The Apache Software Foundation -This product includes software developed at +This product includes software developed by The Apache Software Foundation (http://www.apache.org/). - -======================================================================== - -kerb-simplekdc NOTICE - -======================================================================== - -Kerb Simple Kdc -Copyright 2014-2019 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -======================================================================== - -kerb-util NOTICE - -======================================================================== - -Kerby-kerb Util -Copyright 2014-2019 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -======================================================================== - -kerby-asn1 NOTICE - -======================================================================== - -Kerby ASN1 Project -Copyright 2014-2019 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -======================================================================== - -kerby-config NOTICE - -======================================================================== - -Kerby Config -Copyright 2014-2019 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -======================================================================== - -kerby-pkix NOTICE - -======================================================================== - -Kerby PKIX Project -Copyright 2014-2019 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -======================================================================== - -kerby-util NOTICE - -======================================================================== - -Kerby Util -Copyright 2014-2019 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -======================================================================== - -kerby-xdr NOTICE - -======================================================================== - -Kerby XDR Project -Copyright 2014-2019 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -======================================================================== - -log4j-api NOTICE - -======================================================================== - -Apache Log4j API -Copyright 1999-1969 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -======================================================================== - -log4j-core NOTICE - -======================================================================== -Apache Log4j Core -Copyright 1999-2012 Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - -ResolverUtil.java -Copyright 2005-2006 Tim Fennell - - -======================================================================== - -log4j-slf4j-impl NOTICE - -======================================================================== - -Apache Log4j SLF4J Binding -Copyright 1999-1969 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -======================================================================== - -objenesis NOTICE - -======================================================================== - -// ------------------------------------------------------------------ -// NOTICE file corresponding to the section 4d of The Apache License, -// Version 2.0, in this case for Objenesis -// ------------------------------------------------------------------ - -Objenesis -Copyright 2006-2017 Joe Walnes, Henri Tremblay, Leonardo Mesquita - - -======================================================================== - -tinkergraph-gremlin NOTICE - -======================================================================== - -Apache TinkerPop :: TinkerGraph Gremlin -Copyright 2013-2021 Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -======================================================================== - -token-provider NOTICE - -======================================================================== - -Token provider -Copyright 2014-2019 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -======================================================================== - -Commons-beanutils NOTICE - -======================================================================== - -Apache Commons BeanUtils -Copyright 2000-2019 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - - -======================================================================== - - Commons-cli NOTICE - - ======================================================================== - -Apache Commons CLI -Copyright 2002-2021 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (https://www.apache.org/). - - -======================================================================== - - Commons-codec NOTICE - - ======================================================================== - -Apache Commons Codec -Copyright 2002-2009 The Apache Software Foundation - -This product includes software developed by -The Apache Software Foundation (http://www.apache.org/). - --------------------------------------------------------------------------------- -src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java contains -test data from http://aspell.sourceforge.net/test/batch0.tab. +-------------------------------------------------------------------------------- +src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java contains +test data from http://aspell.sourceforge.net/test/batch0.tab. Copyright (C) 2002 Kevin Atkinson (kevina@gnu.org). Verbatim copying and distribution of this entire article is permitted in any medium, @@ -1083,27 +680,6 @@ BSD - Scala License Scala Library -======================================================================== - -Groovy NOTICE - -======================================================================== - -Apache Groovy -Copyright 2003-2022 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - -The Java source files in src/main/java/org/apache/groovy/util/concurrent/concurrentlinkedhashmap/ -are from https://github.com/ben-manes/concurrentlinkedhashmap and the following notice applies: -Copyright 2010-2012 Google Inc. All Rights Reserved. - -The Java source file src/main/java/org/apache/groovy/util/concurrent/ConcurrentReferenceHashMap -is from https://github.com/hazelcast/hazelcast and the following notice applies: -Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. - - ======================================================================== hppc NOTICE @@ -1407,7 +983,7 @@ jcommander NOTICE ======================================================================== -JCommander Copyright Notices +JCommander Copyright Notices ============================ Copyright 2010 Cedric Beust @@ -1560,7 +1136,7 @@ is derived from Unicode data such as the Unicode Character Database. See http://unicode.org/copyright.html for more details. The Morfologik analyzer (morfologik) includes BSD-licensed software -developed by Dawid Weiss and Marcin Miłkowski +developed by Dawid Weiss and Marcin Miłkowski (https://github.com/morfologik/morfologik-stemming) and uses data from the BSD-licensed dictionary of Polish (SGJP, http://sgjp.pl/morfeusz/). @@ -1683,7 +1259,7 @@ limitations under the License. ----------------------------------------------------------------------- This product contains a modified portion of 'Catapult', an open source -Trace Event viewer for Chome, Linux, and Android applications, which can +Trace Event viewer for Chome, Linux, and Android applications, which can be obtained at: * LICENSE: @@ -1727,7 +1303,7 @@ zt-zip NOTICE ======================================================================== -Copyright 2012 ZeroTurnaround LLC. +Copyright 2012 ZeroTurnaround LLC. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -1766,3 +1342,1989 @@ Copyright 2020-2021 SmartBear Software Inc. # This product includes software developed by the Jansi project (http://fusesource.github.io/jansi/). # See licenses/LICENSE-jansi-2.4.0.txt for license details (Apache License 2.0 or LGPL 3.0+). +======================================================================== + +Apache TinkerPop 3.8.1 NOTICE + +======================================================================== + +Apache TinkerPop +Copyright 2015-2026 The Apache Software Foundation. + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +This product includes/uses ANTLR (http://www.antlr2.org/) +developed by Terence Parr 1989-2006 +------------------------------------------------------------------------ +Activiti +------------------------------------------------------------------------ +Activiti BPM Platform +Copyright 2010-2014 Alfresco Software, Ltd. + +gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphml/GraphMLWriterHelper.java +contains DelegatingXMLStreamWriter.java and IndentingXMLStreamWriter.java from +https://github.com/Activiti/Activiti/tree/activiti-5.16/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/converter + +------------------------------------------------------------------------ +Apache Kerby +------------------------------------------------------------------------ +Apache Kerby +Copyright 2015-2017 The Apache Software Foundation + +gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/auth/JaasKrbUtil.java +from +https://github.com/apache/directory-kerby/blob/kerby-all-1.0.0/kerby-kerb/kerb-simplekdc/src/main/java/org/apache/kerby/kerberos/kerb/client/JaasKrbUtil.java + +======================================================================== + +audience-annotations-0.13.0 NOTICE + +======================================================================== + +Apache Yetus - Audience Annotations +Copyright 2015-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +byte-buddy-1.17.6 NOTICE + +======================================================================== + +Copyright 2014 - Present Rafael Winterhalter + +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. + +======================================================================== + +byte-buddy-agent-1.17.6 NOTICE + +======================================================================== + +Copyright 2014 - Present Rafael Winterhalter + +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. + +======================================================================== + +commons-configuration2-2.10.1 NOTICE + +======================================================================== + +Apache Commons Configuration +Copyright 2001-2024 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). + +======================================================================== + +commons-configuration2-2.9.0 NOTICE + +======================================================================== + +Apache Commons Configuration +Copyright 2001-2023 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). + +======================================================================== + +commons-text-1.11.0 NOTICE + +======================================================================== + +Apache Commons Text +Copyright 2014-2023 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). + +======================================================================== + +cucumber-core-7.21.1 NOTICE + +======================================================================== + +# FastDoubleParser + +This is a Java port of Daniel Lemire's fast_float project. +This project provides parsers for double, float, BigDecimal and BigInteger values. + +## Copyright + +Copyright © 2024 Werner Randelshofer, Switzerland. + +## Licensing + +This code is licensed under MIT License. +https://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE +(The file 'LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +Some portions of the code have been derived from other projects. +All these projects require that we include a copyright notice, and some require that we also include some text of their +license file. + +fast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License. +https://github.com/lemire/fast_double_parser +https://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL +(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +fast_float, Copyright (c) 2021 The fast_float authors. MIT License. +https://github.com/fastfloat/fast_float +https://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT +(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +bigint, Copyright 2020 Tim Buktu. 2-clause BSD License. +https://github.com/tbuktu/bigint/tree/floatfft +https://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE +https://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE +(We only use those portions of the bigint project that can be licensed under 2-clause BSD License.) +(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Copyright + +Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) + +## Licensing + +Jackson 2.x core and extension components are licensed under Apache License 2.0 +To find the details that apply to this artifact see the accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS(-2.x) file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +## FastDoubleParser + +jackson-core bundles a shaded copy of FastDoubleParser . +That code is available under an MIT license +under the following copyright. + +See FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser +and the licenses and copyrights that apply to that code. + +Jackson components are licensed under Apache (Software) License, version 2.0, +as per accompanying LICENSE file. + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +======================================================================== + +gremlin-console-3.8.1 NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Console +Copyright 2013-2022 Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +gremlin-core-3.8.1 NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Core +Copyright 2013-2022 Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +gremlin-driver-3.8.1 NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Driver +Copyright 2013-2022 Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +gremlin-groovy-3.8.1 NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Groovy +Copyright 2013-2022 Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +gremlin-language-3.8.1 NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Language +Copyright 2013-2022 Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +gremlin-server-3.8.1 NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Server +Copyright 2013-2022 Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +gremlin-shaded-3.8.1 NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Shaded +Copyright 2015-2026 Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +------------------------------------------------------------------------ +Jackson-Databind, Jackson-Annotation and Jackson-Core 2.15.2 +------------------------------------------------------------------------ +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +* FastDoubleParser + + jackson-core bundles a shaded copy of FastDoubleParser . + That code is available under an MIT license + under the following copyright. + + Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. + + Some portions of the code have been derived from other projects. + + fast_double_parser, Copyright (c) 2022 Daniel Lemire. Apache 2.0 License. + https://github.com/fastfloat/fast_float + + fast_float, Copyright (c) 2021 The fast_float authors. Apache 2.0 License. + https://github.com/fastfloat/fast_float + + bigint, Copyright 2020 Tim Buktu. 2-clause BSD License. + https://github.com/tbuktu/bigint/tree/floatfft + (We only use those portions of the bigint project that can be licensed under 2-clause BSD License.) + +------------------------------------------------------------------------ +Objenesis 3.3 - shaded in gremlin-shaded to org.apache.tinkerpop.shaded.objenesis +------------------------------------------------------------------------ +Copyright 2006-2022 Joe Walnes, Henri Tremblay, Leonardo Mesquita + +======================================================================== + +gremlin-test-3.8.1 NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Test +Copyright 2013-2022 Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +gremlin-util-3.8.1 NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Util +Copyright 2013-2022 Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +groovy-4.0.25 NOTICE + +======================================================================== + +Apache Groovy +Copyright 2003-2025 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +This product includes/uses ANTLR4 (https://github.com/antlr/antlr4) +Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. + +======================================================================== + +htrace-core4-4.1.0-incubating NOTICE + +======================================================================== + +htrace-core4 +Copyright 2016 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +ivy-2.6.0 NOTICE + +======================================================================== + +Apache Ivy (TM) +Copyright 2007-2026 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +Portions of Ivy were originally developed at +Jayasoft SARL (http://www.jayasoft.fr/) +and are licensed to the Apache Software Foundation under the +"Software Grant License Agreement" + +SSH and SFTP support is provided by the JCraft JSch package, +which is open source software, available under +the terms of a BSD style license. +The original software and related information is available +at http://www.jcraft.com/jsch/. + +The Scp class is based on the SCPClient from the ganymed ssh +library. Which has been distributed under the BSD style license +included in the LICENSE file. + +======================================================================== + +kerb-admin-2.0.1 NOTICE + +======================================================================== + +Kerby-kerb Admin +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerb-client-2.0.1 NOTICE + +======================================================================== + +Kerby-kerb Client +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerb-common-2.0.1 NOTICE + +======================================================================== + +Kerby-kerb Common +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerb-core-2.0.1 NOTICE + +======================================================================== + +Kerby-kerb core +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerb-crypto-2.0.1 NOTICE + +======================================================================== + +Kerby-kerb Crypto +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerb-identity-2.0.1 NOTICE + +======================================================================== + +Kerby-kerb Identity +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerb-server-2.0.1 NOTICE + +======================================================================== + +Kerby-kerb Server +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerb-simplekdc-2.0.1 NOTICE + +======================================================================== + +Kerb Simple Kdc +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerb-util-2.0.1 NOTICE + +======================================================================== + +Kerby-kerb Util +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerby-asn1-2.0.1 NOTICE + +======================================================================== + +Kerby ASN1 Project +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerby-config-2.0.1 NOTICE + +======================================================================== + +Kerby Config +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerby-pkix-2.0.1 NOTICE + +======================================================================== + +Kerby PKIX Project +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerby-util-2.0.1 NOTICE + +======================================================================== + +Kerby Util +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +kerby-xdr-2.0.1 NOTICE + +======================================================================== + +Kerby XDR Project +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +lucene-analyzers-common-7.4.0 NOTICE + +======================================================================== + +Apache Lucene +Copyright 2001-2018 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +Includes software from other Apache Software Foundation projects, +including, but not limited to: + - Apache Ant + - Apache Jakarta Regexp + - Apache Commons + - Apache Xerces + +ICU4J, (under analysis/icu) is licensed under an MIT styles license +and Copyright (c) 1995-2008 International Business Machines Corporation and others + +Some data files (under analysis/icu/src/data) are derived from Unicode data such +as the Unicode Character Database. See http://unicode.org/copyright.html for more +details. + +Brics Automaton (under core/src/java/org/apache/lucene/util/automaton) is +BSD-licensed, created by Anders Møller. See http://www.brics.dk/automaton/ + +The levenshtein automata tables (under core/src/java/org/apache/lucene/util/automaton) were +automatically generated with the moman/finenight FSA library, created by +Jean-Philippe Barrette-LaPierre. This library is available under an MIT license, +see http://sites.google.com/site/rrettesite/moman and +http://bitbucket.org/jpbarrette/moman/overview/ + +The class org.apache.lucene.util.WeakIdentityMap was derived from +the Apache CXF project and is Apache License 2.0. + +The Google Code Prettify is Apache License 2.0. +See http://code.google.com/p/google-code-prettify/ + +JUnit (junit-4.10) is licensed under the Common Public License v. 1.0 +See http://junit.sourceforge.net/cpl-v10.html + +This product includes code (JaspellTernarySearchTrie) from Java Spelling Checkin +g Package (jaspell): http://jaspell.sourceforge.net/ +License: The BSD License (http://www.opensource.org/licenses/bsd-license.php) + +The snowball stemmers in + analysis/common/src/java/net/sf/snowball +were developed by Martin Porter and Richard Boulton. +The snowball stopword lists in + analysis/common/src/resources/org/apache/lucene/analysis/snowball +were developed by Martin Porter and Richard Boulton. +The full snowball package is available from + http://snowball.tartarus.org/ + +The KStem stemmer in + analysis/common/src/org/apache/lucene/analysis/en +was developed by Bob Krovetz and Sergio Guzman-Lara (CIIR-UMass Amherst) +under the BSD-license. + +The Arabic,Persian,Romanian,Bulgarian, Hindi and Bengali analyzers (common) come with a default +stopword list that is BSD-licensed created by Jacques Savoy. These files reside in: +analysis/common/src/resources/org/apache/lucene/analysis/ar/stopwords.txt, +analysis/common/src/resources/org/apache/lucene/analysis/fa/stopwords.txt, +analysis/common/src/resources/org/apache/lucene/analysis/ro/stopwords.txt, +analysis/common/src/resources/org/apache/lucene/analysis/bg/stopwords.txt, +analysis/common/src/resources/org/apache/lucene/analysis/hi/stopwords.txt, +analysis/common/src/resources/org/apache/lucene/analysis/bn/stopwords.txt +See http://members.unine.ch/jacques.savoy/clef/index.html. + +The German,Spanish,Finnish,French,Hungarian,Italian,Portuguese,Russian and Swedish light stemmers +(common) are based on BSD-licensed reference implementations created by Jacques Savoy and +Ljiljana Dolamic. These files reside in: +analysis/common/src/java/org/apache/lucene/analysis/de/GermanLightStemmer.java +analysis/common/src/java/org/apache/lucene/analysis/de/GermanMinimalStemmer.java +analysis/common/src/java/org/apache/lucene/analysis/es/SpanishLightStemmer.java +analysis/common/src/java/org/apache/lucene/analysis/fi/FinnishLightStemmer.java +analysis/common/src/java/org/apache/lucene/analysis/fr/FrenchLightStemmer.java +analysis/common/src/java/org/apache/lucene/analysis/fr/FrenchMinimalStemmer.java +analysis/common/src/java/org/apache/lucene/analysis/hu/HungarianLightStemmer.java +analysis/common/src/java/org/apache/lucene/analysis/it/ItalianLightStemmer.java +analysis/common/src/java/org/apache/lucene/analysis/pt/PortugueseLightStemmer.java +analysis/common/src/java/org/apache/lucene/analysis/ru/RussianLightStemmer.java +analysis/common/src/java/org/apache/lucene/analysis/sv/SwedishLightStemmer.java + +The Stempel analyzer (stempel) includes BSD-licensed software developed +by the Egothor project http://egothor.sf.net/, created by Leo Galambos, Martin Kvapil, +and Edmond Nolan. + +The Polish analyzer (stempel) comes with a default +stopword list that is BSD-licensed created by the Carrot2 project. The file resides +in stempel/src/resources/org/apache/lucene/analysis/pl/stopwords.txt. +See http://project.carrot2.org/license.html. + +The SmartChineseAnalyzer source code (smartcn) was +provided by Xiaoping Gao and copyright 2009 by www.imdict.net. + +WordBreakTestUnicode_*.java (under modules/analysis/common/src/test/) +is derived from Unicode data such as the Unicode Character Database. +See http://unicode.org/copyright.html for more details. + +The Morfologik analyzer (morfologik) includes BSD-licensed software +developed by Dawid Weiss and Marcin Miłkowski (http://morfologik.blogspot.com/). + +Morfologik uses data from Polish ispell/myspell dictionary +(http://www.sjp.pl/slownik/en/) licenced on the terms of (inter alia) +LGPL and Creative Commons ShareAlike. + +Morfologic includes data from BSD-licensed dictionary of Polish (SGJP) +(http://sgjp.pl/morfeusz/) + +Servlet-api.jar and javax.servlet-*.jar are under the CDDL license, the original +source code for this can be found at http://www.eclipse.org/jetty/downloads.php + +=========================================================================== +Kuromoji Japanese Morphological Analyzer - Apache Lucene Integration +=========================================================================== + +This software includes a binary and/or source version of data from + + mecab-ipadic-2.7.0-20070801 + +which can be obtained from + + http://atilika.com/releases/mecab-ipadic/mecab-ipadic-2.7.0-20070801.tar.gz + +or + + http://jaist.dl.sourceforge.net/project/mecab/mecab-ipadic/2.7.0-20070801/mecab-ipadic-2.7.0-20070801.tar.gz + +=========================================================================== +mecab-ipadic-2.7.0-20070801 Notice +=========================================================================== + +Nara Institute of Science and Technology (NAIST), +the copyright holders, disclaims all warranties with regard to this +software, including all implied warranties of merchantability and +fitness, in no event shall NAIST be liable for +any special, indirect or consequential damages or any damages +whatsoever resulting from loss of use, data or profits, whether in an +action of contract, negligence or other tortuous action, arising out +of or in connection with the use or performance of this software. + +A large portion of the dictionary entries +originate from ICOT Free Software. The following conditions for ICOT +Free Software applies to the current dictionary as well. + +Each User may also freely distribute the Program, whether in its +original form or modified, to any third party or parties, PROVIDED +that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear +on, or be attached to, the Program, which is distributed substantially +in the same form as set out herein and that such intended +distribution, if actually made, will neither violate or otherwise +contravene any of the laws and regulations of the countries having +jurisdiction over the User or the intended distribution itself. + +NO WARRANTY + +The program was produced on an experimental basis in the course of the +research and development conducted during the project and is provided +to users as so produced on an experimental basis. Accordingly, the +program is provided without any warranty whatsoever, whether express, +implied, statutory or otherwise. The term "warranty" used herein +includes, but is not limited to, any warranty of the quality, +performance, merchantability and fitness for a particular purpose of +the program and the nonexistence of any infringement or violation of +any right of any third party. + +Each user of the program will agree and understand, and be deemed to +have agreed and understood, that there is no warranty whatsoever for +the program and, accordingly, the entire risk arising from or +otherwise connected with the program is assumed by the user. + +Therefore, neither ICOT, the copyright holder, or any other +organization that participated in or was otherwise related to the +development of the program and their respective officials, directors, +officers and other employees shall be held liable for any and all +damages, including, without limitation, general, special, incidental +and consequential damages, arising out of or otherwise in connection +with the use or inability to use the program or any product, material +or result produced or otherwise obtained by using the program, +regardless of whether they have been advised of, or otherwise had +knowledge of, the possibility of such damages at any time during the +project or thereafter. Each user will be deemed to have agreed to the +foregoing by his or her commencement of use of the program. The term +"use" as used herein includes, but is not limited to, the use, +modification, copying and distribution of the program and the +production of secondary products from the program. + +In the case where the program, whether in its original form or +modified, was distributed or delivered to or received by a user from +any person, organization or entity other than ICOT, unless it makes or +grants independently of ICOT any specific warranty to the user in +writing, such person, organization or entity, will also be exempted +from and not be held liable to the user for any such damages as noted +above as far as the program is concerned. + +=========================================================================== +Nori Korean Morphological Analyzer - Apache Lucene Integration +=========================================================================== + +This software includes a binary and/or source version of data from + + mecab-ko-dic-2.0.3-20170922 + +which can be obtained from + + https://bitbucket.org/eunjeon/mecab-ko-dic/downloads/mecab-ko-dic-2.0.3-20170922.tar.gz + +======================================================================== + +objenesis-3.3 NOTICE + +======================================================================== + +// ------------------------------------------------------------------ +// NOTICE file corresponding to the section 4d of The Apache License, +// Version 2.0, in this case for Objenesis +// ------------------------------------------------------------------ + +Objenesis +Copyright 2006-2022 Joe Walnes, Henri Tremblay, Leonardo Mesquita + +======================================================================== + +tinkergraph-gremlin-3.8.1 NOTICE + +======================================================================== + +Apache TinkerPop :: TinkerGraph Gremlin +Copyright 2013-2022 Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +token-provider-2.0.1 NOTICE + +======================================================================== + +Token provider +Copyright 2014-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +Netty 4.1.137.Final NOTICE + +======================================================================== + + The Netty Project + ================= + +Please visit the Netty web site for more information: + + * https://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: + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +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 contains the extensions to Java Collections Framework which has +been derived from the works by JSR-166 EG, Doug Lea, and Jason T. Greene: + + * LICENSE: + * license/LICENSE.jsr166y.txt (Public Domain) + * HOMEPAGE: + * http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/ + * http://viewvc.jboss.org/cgi-bin/viewvc.cgi/jbosscache/experimental/jsr166/ + +This product contains a modified version of Robert Harder's Public Domain +Base64 Encoder and Decoder, which can be obtained at: + + * LICENSE: + * license/LICENSE.base64.txt (Public Domain) + * HOMEPAGE: + * http://iharder.sourceforge.net/current/java/base64/ + +This product contains a modified portion of 'Webbit', an event based +WebSocket and HTTP server, which can be obtained at: + + * LICENSE: + * license/LICENSE.webbit.txt (BSD License) + * HOMEPAGE: + * https://github.com/joewalnes/webbit + +This product contains a modified portion of 'SLF4J', a simple logging +facade for Java, which can be obtained at: + + * LICENSE: + * license/LICENSE.slf4j.txt (MIT License) + * HOMEPAGE: + * https://www.slf4j.org/ + +This product contains a modified portion of 'Apache Harmony', an open source +Java SE, which can be obtained at: + + * NOTICE: + * license/NOTICE.harmony.txt + * LICENSE: + * license/LICENSE.harmony.txt (Apache License 2.0) + * HOMEPAGE: + * https://archive.apache.org/dist/harmony/ + +This product contains a modified portion of 'jbzip2', a Java bzip2 compression +and decompression library written by Matthew J. Francis. It can be obtained at: + + * LICENSE: + * license/LICENSE.jbzip2.txt (MIT License) + * HOMEPAGE: + * https://code.google.com/p/jbzip2/ + +This product contains a modified portion of 'libdivsufsort', a C API library to construct +the suffix array and the Burrows-Wheeler transformed string for any input string of +a constant-size alphabet written by Yuta Mori. It can be obtained at: + + * LICENSE: + * license/LICENSE.libdivsufsort.txt (MIT License) + * HOMEPAGE: + * https://github.com/y-256/libdivsufsort + +This product contains a modified portion of Nitsan Wakart's 'JCTools', Java Concurrency Tools for the JVM, + which can be obtained at: + + * LICENSE: + * license/LICENSE.jctools.txt (ASL2 License) + * HOMEPAGE: + * https://github.com/JCTools/JCTools + +This product optionally depends on 'JZlib', a re-implementation of zlib in +pure Java, which can be obtained at: + + * LICENSE: + * license/LICENSE.jzlib.txt (BSD style License) + * HOMEPAGE: + * http://www.jcraft.com/jzlib/ + +This product optionally depends on 'Compress-LZF', a Java library for encoding and +decoding data in LZF format, written by Tatu Saloranta. It can be obtained at: + + * LICENSE: + * license/LICENSE.compress-lzf.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/ning/compress + +This product optionally depends on 'lz4', a LZ4 Java compression +and decompression library written by Adrien Grand. It can be obtained at: + + * LICENSE: + * license/LICENSE.lz4.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/jpountz/lz4-java + +This product optionally depends on 'lzma-java', a LZMA Java compression +and decompression library, which can be obtained at: + + * LICENSE: + * license/LICENSE.lzma-java.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/jponge/lzma-java + +This product optionally depends on 'zstd-jni', a zstd-jni Java compression +and decompression library, which can be obtained at: + + * LICENSE: + * license/LICENSE.zstd-jni.txt (BSD) + * HOMEPAGE: + * https://github.com/luben/zstd-jni + +This product contains a modified portion of 'jfastlz', a Java port of FastLZ compression +and decompression library written by William Kinney. It can be obtained at: + + * LICENSE: + * license/LICENSE.jfastlz.txt (MIT License) + * HOMEPAGE: + * https://code.google.com/p/jfastlz/ + +This product contains a modified portion of and optionally depends on 'Protocol Buffers', Google's data +interchange format, which can be obtained at: + + * LICENSE: + * license/LICENSE.protobuf.txt (New BSD License) + * HOMEPAGE: + * https://github.com/google/protobuf + +This product optionally depends on 'Bouncy Castle Crypto APIs' to generate +a temporary self-signed X.509 certificate when the JVM does not provide the +equivalent functionality. It can be obtained at: + + * LICENSE: + * license/LICENSE.bouncycastle.txt (MIT License) + * HOMEPAGE: + * https://www.bouncycastle.org/ + +This product optionally depends on 'Snappy', a compression library produced +by Google Inc, which can be obtained at: + + * LICENSE: + * license/LICENSE.snappy.txt (New BSD License) + * HOMEPAGE: + * https://github.com/google/snappy + +This product optionally depends on 'JBoss Marshalling', an alternative Java +serialization API, which can be obtained at: + + * LICENSE: + * license/LICENSE.jboss-marshalling.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/jboss-remoting/jboss-marshalling + +This product optionally depends on 'Caliper', Google's micro- +benchmarking framework, which can be obtained at: + + * LICENSE: + * license/LICENSE.caliper.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/google/caliper + +This product optionally depends on 'Apache Commons Logging', a logging +framework, which can be obtained at: + + * LICENSE: + * license/LICENSE.commons-logging.txt (Apache License 2.0) + * HOMEPAGE: + * https://commons.apache.org/logging/ + +This product optionally depends on 'Apache Log4J', a logging framework, which +can be obtained at: + + * LICENSE: + * license/LICENSE.log4j.txt (Apache License 2.0) + * HOMEPAGE: + * https://logging.apache.org/log4j/ + +This product optionally depends on 'Aalto XML', an ultra-high performance +non-blocking XML processor, which can be obtained at: + + * LICENSE: + * license/LICENSE.aalto-xml.txt (Apache License 2.0) + * HOMEPAGE: + * https://wiki.fasterxml.com/AaltoHome + +This product contains a modified version of 'HPACK', a Java implementation of +the HTTP/2 HPACK algorithm written by Twitter. It can be obtained at: + + * LICENSE: + * license/LICENSE.hpack.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/twitter/hpack + +This product contains a modified version of 'HPACK', a Java implementation of +the HTTP/2 HPACK algorithm written by Cory Benfield. It can be obtained at: + + * LICENSE: + * license/LICENSE.hyper-hpack.txt (MIT License) + * HOMEPAGE: + * https://github.com/python-hyper/hpack/ + +This product contains a modified version of 'HPACK', a Java implementation of +the HTTP/2 HPACK algorithm written by Tatsuhiro Tsujikawa. It can be obtained at: + + * LICENSE: + * license/LICENSE.nghttp2-hpack.txt (MIT License) + * HOMEPAGE: + * https://github.com/nghttp2/nghttp2/ + +This product contains a modified portion of 'Apache Commons Lang', a Java library +provides utilities for the java.lang API, which can be obtained at: + + * LICENSE: + * license/LICENSE.commons-lang.txt (Apache License 2.0) + * HOMEPAGE: + * https://commons.apache.org/proper/commons-lang/ + + +This product contains the Maven wrapper scripts from 'Maven Wrapper', that provides an easy way to ensure a user has everything necessary to run the Maven build. + + * LICENSE: + * license/LICENSE.mvn-wrapper.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/takari/maven-wrapper + +This product contains the dnsinfo.h header file, that provides a way to retrieve the system DNS configuration on MacOS. +This private header is also used by Apple's open source + mDNSResponder (https://opensource.apple.com/tarballs/mDNSResponder/). + + * LICENSE: + * license/LICENSE.dnsinfo.txt (Apple Public Source License 2.0) + * HOMEPAGE: + * https://www.opensource.apple.com/source/configd/configd-453.19/dnsinfo/dnsinfo.h + +This product optionally depends on 'Brotli4j', Brotli compression and +decompression for Java., which can be obtained at: + + * LICENSE: + * license/LICENSE.brotli4j.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/hyperxpro/Brotli4j + +The Netty license paths above are reproduced as sections in +licenses/LICENSE-netty-4.1.137.Final.txt. + + +======================================================================== + +Netty tcnative 2.0.81.Final NOTICE + +======================================================================== + + The Netty Project + ================= + +Please visit the Netty web site for more information: + + * http://netty.io/ + +Copyright 2016 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. + +------------------------------------------------------------------------------- +This product contains a forked and modified version of Tomcat Native + + * LICENSE: + * license/LICENSE.tomcat-native.txt (Apache License 2.0) + * HOMEPAGE: + * http://tomcat.apache.org/native-doc/ + * https://svn.apache.org/repos/asf/tomcat/native/ + +This product contains the Maven wrapper scripts from 'Maven Wrapper', that provides an easy way to ensure a user has everything necessary to run the Maven build. + + * LICENSE: + * license/LICENSE.mvn-wrapper.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/takari/maven-wrapper + +This product contains code from boringssl. + + * LICENSE + * license/LICENSE.boringssl.txt (Apache License 2.0) + * HOMEPAGE: + * https://boringssl.googlesource.com/boringssl/ + +The referenced license paths are reproduced in +licenses/LICENSE-netty-tcnative-2.0.81.Final.txt. + +======================================================================== + +grpc-netty-shaded 1.55.3 NOTICE + +======================================================================== + + The Netty Project + ================= + +Please visit the Netty web site for more information: + + * http://netty.io/ + +Copyright 2016 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. + +------------------------------------------------------------------------------- +This product contains a forked and modified version of Tomcat Native + + * LICENSE: + * license/LICENSE.tomcat-native.txt (Apache License 2.0) + * HOMEPAGE: + * http://tomcat.apache.org/native-doc/ + * https://svn.apache.org/repos/asf/tomcat/native/ + +This product contains the Maven wrapper scripts from 'Maven Wrapper', that provides an easy way to ensure a user has everything necessary to run the Maven build. + + * LICENSE: + * license/LICENSE.mvn-wrapper.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/takari/maven-wrapper + +This product contains small piece of code to support AIX, taken from netbsd. + + * LICENSE: + * license/LICENSE.aix-netbsd.txt (OpenSSL License) + * HOMEPAGE: + * https://ftp.netbsd.org/pub/NetBSD/NetBSD-current/src/crypto/external/bsd/openssl/dist + + +This product contains code from boringssl. + + * LICENSE (Combination ISC and OpenSSL license) + * license/LICENSE.boringssl.txt (Combination ISC and OpenSSL license) + * HOMEPAGE: + * https://boringssl.googlesource.com/boringssl/ + +The referenced license paths are reproduced in +licenses/LICENSE-grpc-netty-shaded-1.55.3.txt. +======================================================================== + +spring-boot-jarmode-layertools-2.7.1 NOTICE + +======================================================================== + +Spring Boot 2.7.1 +Copyright (c) 2012-2022 Pivotal, Inc. + +This product is licensed to you under the Apache License, Version 2.0 +(the "License"). You may not use this product except in compliance with +the License. + +======================================================================== + +Apache TinkerPop NOTICE + +======================================================================== +Apache TinkerPop +Copyright 2015-2022 The Apache Software Foundation. + +------------------------------------------------------------------------ +Activiti +------------------------------------------------------------------------ +Activiti BPM Platform +Copyright 2010-2014 Alfresco Software, Ltd. + +gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphml/GraphMLWriterHelper.java +contains DelegatingXMLStreamWriter.java and IndentingXMLStreamWriter.java from +https://github.com/Activiti/Activiti/tree/activiti-5.16/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/converter + +------------------------------------------------------------------------ +Apache Kerby +------------------------------------------------------------------------ +Apache Kerby +Copyright 2015-2017 The Apache Software Foundation + +gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/auth/JaasKrbUtil.java +from +https://github.com/apache/directory-kerby/blob/kerby-all-1.0.0/kerby-kerb/kerb-simplekdc/src/main/java/org/apache/kerby/kerberos/kerb/client/JaasKrbUtil.java + + + +======================================================================== + +gremlin-console NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Console +Copyright 2013-2021 Apache Software Foundation + + +======================================================================== + +gremlin-core NOTICE + +======================================================================== +This product contains code form the JanusGraph Project: + +Apache TinkerPop :: Gremlin Core +Copyright 2013-2021 Apache Software Foundation + + +======================================================================== + +gremlin-driver NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Driver +Copyright 2013-2021 Apache Software Foundation + + +======================================================================== + +gremlin-groovy NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Groovy +Copyright 2013-2021 Apache Software Foundation + + +======================================================================== + +gremlin-server NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Server +Copyright 2013-2021 Apache Software Foundation + + +======================================================================== + +gremlin-shaded NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Shaded +Copyright 2013-2021 Apache Software Foundation + + +======================================================================== + +gremlin-test NOTICE + +======================================================================== + +Apache TinkerPop :: Gremlin Test +Copyright 2013-2021 Apache Software Foundation + + + +======================================================================== + +groovy-cli-picocli NOTICE + +======================================================================== +Apache Groovy +Copyright 2003-2020 The Apache Software Foundation + + +======================================================================== + +groovy-console NOTICE + +======================================================================== +Apache Groovy +Copyright 2003-2020 The Apache Software Foundation + + +======================================================================== + +groovy-swing NOTICE + +======================================================================== +Apache Groovy +Copyright 2003-2020 The Apache Software Foundation + + +======================================================================== + +groovy-templates NOTICE + +======================================================================== +Apache Groovy +Copyright 2003-2020 The Apache Software Foundation + + +======================================================================== + +groovy-xml NOTICE + +======================================================================== +Apache Groovy +Copyright 2003-2020 The Apache Software Foundation + + +======================================================================== + +kerb-admin NOTICE + +======================================================================== + +Kerby-kerb Admin +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerb-client NOTICE + +======================================================================== + +Kerby-kerb Client +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerb-common NOTICE + +======================================================================== + +Kerby-kerb Common +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerb-core NOTICE + +======================================================================== + +Kerby-kerb core +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerb-crypto NOTICE + +======================================================================== + +Kerby-kerb Crypto +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerb-identity NOTICE + +======================================================================== + +Kerby-kerb Identity +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerb-server NOTICE + +======================================================================== + +Kerby-kerb Server +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerb-simplekdc NOTICE + +======================================================================== + +Kerb Simple Kdc +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerb-util NOTICE + +======================================================================== + +Kerby-kerb Util +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerby-asn1 NOTICE + +======================================================================== + +Kerby ASN1 Project +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerby-config NOTICE + +======================================================================== + +Kerby Config +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerby-pkix NOTICE + +======================================================================== + +Kerby PKIX Project +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerby-util NOTICE + +======================================================================== + +Kerby Util +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +kerby-xdr NOTICE + +======================================================================== + +Kerby XDR Project +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +tinkergraph-gremlin NOTICE + +======================================================================== + +Apache TinkerPop :: TinkerGraph Gremlin +Copyright 2013-2021 Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +token-provider NOTICE + +======================================================================== + +Token provider +Copyright 2014-2019 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + + + +======================================================================== + +Groovy NOTICE + +======================================================================== + +Apache Groovy +Copyright 2003-2022 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +The Java source files in src/main/java/org/apache/groovy/util/concurrent/concurrentlinkedhashmap/ +are from https://github.com/ben-manes/concurrentlinkedhashmap and the following notice applies: +Copyright 2010-2012 Google Inc. All Rights Reserved. + +The Java source file src/main/java/org/apache/groovy/util/concurrent/ConcurrentReferenceHashMap +is from https://github.com/hazelcast/hazelcast and the following notice applies: +Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. + + +======================================================================== + +Spring Framework 5.3.27 NOTICE + +======================================================================== + +Spring Framework 5.3.27 +Copyright (c) 2002-2023 Pivotal, Inc. + +This product is licensed to you under the Apache License, Version 2.0 +(the "License"). You may not use this product except in compliance with +the License. + +This product may include a number of subcomponents with separate +copyright notices and license terms. Your use of the source code for +these subcomponents is subject to the terms and conditions of the +subcomponent's license, as noted in the license.txt file. + + +======================================================================== + +Spring Boot 2.5.15 NOTICE + +======================================================================== + +Spring Boot 2.5.15 +Copyright (c) 2012-2022 Pivotal, Inc. + +This product is licensed to you under the Apache License, Version 2.0 +(the "License"). You may not use this product except in compliance with +the License. + + +======================================================================== + +Jetty 9.4.51 NOTICE + +======================================================================== + +============================================================== + Jetty Web Container + Copyright 1995-2018 Mort Bay Consulting Pty Ltd. +============================================================== + +The Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd +unless otherwise noted. + +Jetty is dual licensed under both + + * The Apache 2.0 License + http://www.apache.org/licenses/LICENSE-2.0.html + + and + + * The Eclipse Public 1.0 License + http://www.eclipse.org/legal/epl-v10.html + +Jetty may be distributed under either license. + +------ +Eclipse + +The following artifacts are EPL. + * org.eclipse.jetty.orbit:org.eclipse.jdt.core + +The following artifacts are EPL and ASL2. + * org.eclipse.jetty.orbit:javax.security.auth.message + + +The following artifacts are EPL and CDDL 1.0. + * org.eclipse.jetty.orbit:javax.mail.glassfish + + +------ +Oracle + +The following artifacts are CDDL + GPLv2 with classpath exception. +https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html + + * javax.servlet:javax.servlet-api + * javax.annotation:javax.annotation-api + * javax.transaction:javax.transaction-api + * javax.websocket:javax.websocket-api + +------ +Oracle OpenJDK + +If ALPN is used to negotiate HTTP/2 connections, then the following +artifacts may be included in the distribution or downloaded when ALPN +module is selected. + + * java.sun.security.ssl + +These artifacts replace/modify OpenJDK classes. The modififications +are hosted at github and both modified and original are under GPL v2 with +classpath exceptions. +http://openjdk.java.net/legal/gplv2+ce.html + + +------ +OW2 + +The following artifacts are licensed by the OW2 Foundation according to the +terms of http://asm.ow2.org/license.html + +org.ow2.asm:asm-commons +org.ow2.asm:asm + + +------ +Apache + +The following artifacts are ASL2 licensed. + +org.apache.taglibs:taglibs-standard-spec +org.apache.taglibs:taglibs-standard-impl + + +------ +MortBay + +The following artifacts are ASL2 licensed. Based on selected classes from +following Apache Tomcat jars, all ASL2 licensed. + +org.mortbay.jasper:apache-jsp + org.apache.tomcat:tomcat-jasper + org.apache.tomcat:tomcat-juli + org.apache.tomcat:tomcat-jsp-api + org.apache.tomcat:tomcat-el-api + org.apache.tomcat:tomcat-jasper-el + org.apache.tomcat:tomcat-api + org.apache.tomcat:tomcat-util-scan + org.apache.tomcat:tomcat-util + +org.mortbay.jasper:apache-el + org.apache.tomcat:tomcat-jasper-el + org.apache.tomcat:tomcat-el-api + + +------ +Mortbay + +The following artifacts are CDDL + GPLv2 with classpath exception. + +https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html + +org.eclipse.jetty.toolchain:jetty-schemas + +------ +Assorted + +The UnixCrypt.java code implements the one way cryptography used by +Unix systems for simple password protection. Copyright 1996 Aki Yoshida, +modified April 2001 by Iris Van den Broeke, Daniel Deville. +Permission to use, copy, modify and distribute UnixCrypt +for non-commercial or commercial purposes and without fee is +granted provided that the copyright notice appears in all copies. + + +======================================================================== + +tomcat-embed-el-9.0.75 NOTICE + +======================================================================== + +Apache Tomcat +Copyright 1999-2023 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== + +jackson-dataformat-yaml-2.15.2 NOTICE + +======================================================================== +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Copyright + +Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) + +## Licensing + +Jackson components are licensed under Apache (Software) License, version 2.0, +as per accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +======================================================================== + +jackson-datatype-jdk8-2.15.2 NOTICE + +======================================================================== +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Licensing + +Jackson components are licensed under Apache (Software) License, version 2.0, +as per accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +======================================================================== + +jackson-datatype-jsr310-2.15.2 NOTICE + +======================================================================== +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Licensing + +Jackson components are licensed under Apache (Software) License, version 2.0, +as per accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +======================================================================== + +jackson-module-parameter-names-2.15.2 NOTICE + +======================================================================== +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Licensing + +Jackson components are licensed under Apache (Software) License, version 2.0, +as per accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +======================================================================== + +commons-text-1.10.0 NOTICE + +======================================================================== + +Apache Commons Text +Copyright 2014-2022 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). + +======================================================================== + +Arthas 4.1.5 embedded Jackson 2.18.1 NOTICE + +======================================================================== + +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Copyright + +Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) + +## Licensing + +Jackson 2.x core and extension components are licensed under Apache License 2.0 +To find the details that apply to this artifact see the accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS(-2.x) file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +## FastDoubleParser + +jackson-core bundles a shaded copy of FastDoubleParser . +That code is available under an MIT license +under the following copyright. + +Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. + +See FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser +and the licenses and copyrights that apply to that code. + +======================================================================== + +Arthas 4.1.5 embedded FastDoubleParser NOTICE + +======================================================================== + +# FastDoubleParser + +This is a Java port of Daniel Lemire's fast_float project. +This project provides parsers for double, float, BigDecimal and BigInteger values. + +## Copyright + +Copyright © 2024 Werner Randelshofer, Switzerland. + +## Licensing + +This code is licensed under MIT License. +https://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE +(The file 'LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +Some portions of the code have been derived from other projects. +All these projects require that we include a copyright notice, and some require that we also include some text of their +license file. + +fast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License. +https://github.com/lemire/fast_double_parser +https://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL +(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +fast_float, Copyright (c) 2021 The fast_float authors. MIT License. +https://github.com/fastfloat/fast_float +https://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT +(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +bigint, Copyright 2020 Tim Buktu. 2-clause BSD License. +https://github.com/tbuktu/bigint/tree/floatfft +https://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE +https://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE +(We only use those portions of the bigint project that can be licensed under 2-clause BSD License.) +(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +======================================================================== + +Arthas 4.1.5 embedded Logback 1.2.13 NOTICE + +======================================================================== + +Logback: the reliable, generic, fast and flexible logging framework. +Copyright (C) 1999-2015, QOS.ch. All rights reserved. diff --git a/install-dist/release-docs/licenses/LICENSE-animal-sniffer-annotations-1.17.txt b/install-dist/release-docs/licenses/LICENSE-animal-sniffer-annotations-1.17.txt new file mode 100644 index 0000000000..016a46a9ab --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-animal-sniffer-annotations-1.17.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2008 Kohsuke Kawaguchi and codehaus.org. + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-animal-sniffer-annotations-1.21.txt b/install-dist/release-docs/licenses/LICENSE-animal-sniffer-annotations-1.21.txt new file mode 100644 index 0000000000..016a46a9ab --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-animal-sniffer-annotations-1.21.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2008 Kohsuke Kawaguchi and codehaus.org. + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-antlr4-runtime-4.9.1.txt b/install-dist/release-docs/licenses/LICENSE-antlr4-runtime-4.9.1.txt new file mode 100644 index 0000000000..2042d1bda6 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-antlr4-runtime-4.9.1.txt @@ -0,0 +1,52 @@ +[The "BSD 3-clause license"] +Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +===== + +MIT License for codepointat.js from https://git.io/codepointat +MIT License for fromcodepoint.js from https://git.io/vDW1m + +Copyright Mathias Bynens + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-arthas-packaging-4.1.5.txt b/install-dist/release-docs/licenses/LICENSE-arthas-packaging-4.1.5.txt new file mode 100644 index 0000000000..a82f7b908a --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-arthas-packaging-4.1.5.txt @@ -0,0 +1,630 @@ +======================================================================== +Arthas 4.1.5 LICENSE +======================================================================== + + + 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. + +======================================================================== +FastDoubleParser LICENSE (referenced commit in the bundled NOTICE) +======================================================================== + +MIT License + +Copyright (c) 2023 Werner Randelshofer, Switzerland. + +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. + +======================================================================== +FastDoubleParser thirdparty-LICENSE (bundled in arthas-core.jar) +======================================================================== + +------ +Third-party license for +fast_float, Copyright (c) 2021 The fast_float authors. MIT License. +https://github.com/fastfloat/fast_float +https://github.com/fastfloat/fast_float/blob/35d523195bf7d57aba0e735ad6eba1e6f71ba8d6/LICENSE-MIT + +MIT License + +Copyright (c) 2021 The fast_float authors + +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. +------ +Third-party license for +bigint, Copyright 2020 Tim Buktu. 2-clause BSD License. +https://github.com/tbuktu/bigint/tree/floatfft +https://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE +https://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE +(We only use those portions of the bigint project that can be licensed under 2-clause BSD License.) + +2-clause BSD License + +Copyright 2022 Tim Buktu + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +fast_double_parser LICENSE.BSL (referenced commit in the bundled NOTICE) +======================================================================== + +Copyright (c) Daniel Lemire + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +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, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +======================================================================== +ASM 9.9 BSD LICENSE (shaded through repackage-asm 0.0.19) +======================================================================== + + + ASM: a very small and fast Java bytecode manipulation framework + Copyright (c) 2000-2011 INRIA, France Telecom + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +RSyntaxTextArea 3.3.4 BSD LICENSE (bundled in arthas-core.jar) +======================================================================== + +Copyright (c) 2021, Robert Futrell +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the author nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +Logback 1.2.13 LICENSE (EPL 1.0 option selected) +======================================================================== + +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. + +======================================================================== +Eclipse Public License 1.0 (shared standard text) +======================================================================== + +Eclipse Public License - v 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC +LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM +CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial code and + documentation distributed under this Agreement, and + b) in the case of each subsequent Contributor: + + i) changes to the Program, and + + ii) additions to the Program; + + where such changes and/or additions to the Program originate from and are +distributed by that particular Contributor. A Contribution 'originates' from a +Contributor if it was added to the Program by such Contributor itself or anyone +acting on such Contributor's behalf. Contributions do not include additions to +the Program which: (i) are separate modules of software distributed in +conjunction with the Program under their own license agreement, and (ii) are +not derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents " mean patent claims licensable by a Contributor which are +necessarily infringed by the use or sale of its Contribution alone or when +combined with the Program. + +"Program" means the Contributions distributed in accordance with this Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby grants +Recipient a non-exclusive, worldwide, royalty-free copyright license to +reproduce, prepare derivative works of, publicly display, publicly perform, +distribute and sublicense the Contribution of such Contributor, if any, and +such derivative works, in source code and object code form. + + b) Subject to the terms of this Agreement, each Contributor hereby grants +Recipient a non-exclusive, worldwide, royalty-free patent license under +Licensed Patents to make, use, sell, offer to sell, import and otherwise +transfer the Contribution of such Contributor, if any, in source code and +object code form. This patent license shall apply to the combination of the +Contribution and the Program if, at the time the Contribution is added by the +Contributor, such addition of the Contribution causes such combination to be +covered by the Licensed Patents. The patent license shall not apply to any +other combinations which include the Contribution. No hardware per se is +licensed hereunder. + + c) Recipient understands that although each Contributor grants the +licenses to its Contributions set forth herein, no assurances are provided by +any Contributor that the Program does not infringe the patent or other +intellectual property rights of any other entity. Each Contributor disclaims +any liability to Recipient for claims brought by any other entity based on +infringement of intellectual property rights or otherwise. As a condition to +exercising the rights and licenses granted hereunder, each Recipient hereby +assumes sole responsibility to secure any other intellectual property rights +needed, if any. For example, if a third party patent license is required to +allow Recipient to distribute the Program, it is Recipient's responsibility to +acquire that license before distributing the Program. + + d) Each Contributor represents that to its knowledge it has sufficient +copyright rights in its Contribution, if any, to grant the copyright license +set forth in this Agreement. + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form under +its own license agreement, provided that: + + a) it complies with the terms and conditions of this Agreement; and + + b) its license agreement: + + i) effectively disclaims on behalf of all Contributors all warranties and +conditions, express and implied, including warranties or conditions of title +and non-infringement, and implied warranties or conditions of merchantability +and fitness for a particular purpose; + + ii) effectively excludes on behalf of all Contributors all liability for +damages, including direct, indirect, special, incidental and consequential +damages, such as lost profits; + + iii) states that any provisions which differ from this Agreement are +offered by that Contributor alone and not by any other party; and + + iv) states that source code for the Program is available from such +Contributor, and informs licensees how to obtain it in a reasonable manner on +or through a medium customarily used for software exchange. + +When the Program is made available in source code form: + + a) it must be made available under this Agreement; and + + b) a copy of this Agreement must be included with each copy of the +Program. + +Contributors may not remove or alter any copyright notices contained within the +Program. + +Each Contributor must identify itself as the originator of its Contribution, if +any, in a manner that reasonably allows subsequent Recipients to identify the +originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities with +respect to end users, business partners and the like. While this license is +intended to facilitate the commercial use of the Program, the Contributor who +includes the Program in a commercial product offering should do so in a manner +which does not create potential liability for other Contributors. Therefore, if +a Contributor includes the Program in a commercial product offering, such +Contributor ("Commercial Contributor") hereby agrees to defend and indemnify +every other Contributor ("Indemnified Contributor") against any losses, damages +and costs (collectively "Losses") arising from claims, lawsuits and other legal +actions brought by a third party against the Indemnified Contributor to the +extent caused by the acts or omissions of such Commercial Contributor in +connection with its distribution of the Program in a commercial product +offering. The obligations in this section do not apply to any claims or Losses +relating to any actual or alleged intellectual property infringement. In order +to qualify, an Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial Contributor +to control, and cooperate with the Commercial Contributor in, the defense and +any related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product +offering, Product X. That Contributor is then a Commercial Contributor. If that +Commercial Contributor then makes performance claims, or offers warranties +related to Product X, those performance claims and warranties are such +Commercial Contributor's responsibility alone. Under this section, the +Commercial Contributor would have to defend claims against the other +Contributors related to those performance claims and warranties, and if a court +requires any other Contributor to pay any damages as a result, the Commercial +Contributor must pay those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED 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. Each +Recipient is solely responsible for determining the appropriateness of using +and distributing the Program and assumes all risks associated with its exercise +of rights under this Agreement, including but not limited to the risks and +costs of program errors, compliance with applicable laws, damage to or loss of +data, programs or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY +CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS +GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under applicable +law, it shall not affect the validity or enforceability of the remainder of the +terms of this Agreement, and without further action by the parties hereto, such +provision shall be reformed to the minimum extent necessary to make such +provision valid and enforceable. + +If Recipient institutes patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software or +hardware) infringes such Recipient's patent(s), then such Recipient's rights +granted under Section 2(b) shall terminate as of the date such litigation is +filed. + +All Recipient's rights under this Agreement shall terminate if it fails to +comply with any of the material terms or conditions of this Agreement and does +not cure such failure in a reasonable period of time after becoming aware of +such noncompliance. If all Recipient's rights under this Agreement terminate, +Recipient agrees to cease use and distribution of the Program as soon as +reasonably practicable. However, Recipient's obligations under this Agreement +and any licenses granted by Recipient relating to the Program shall continue +and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in +order to avoid inconsistency the Agreement is copyrighted and may only be +modified in the following manner. The Agreement Steward reserves the right to +publish new versions (including revisions) of this Agreement from time to time. +No one other than the Agreement Steward has the right to modify this Agreement. +The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to +serve as the Agreement Steward to a suitable separate entity. Each new version +of the Agreement will be given a distinguishing version number. The Program +(including Contributions) may always be distributed subject to the version of +the Agreement under which it was received. In addition, after a new version of +the Agreement is published, Contributor may elect to distribute the Program +(including its Contributions) under the new version. Except as expressly stated +in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to +the intellectual property of any Contributor under this Agreement, whether +expressly, by implication, estoppel or otherwise. All rights in the Program not +expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to this +Agreement will bring a legal action under this Agreement more than one year +after the cause of action arose. Each party waives its rights to a jury trial +in any resulting litigation. + +Logback 1.2.13 source: +https://github.com/qos-ch/logback/tree/v_1.2.13 + +Arthas repackaged Logback source (includes relocated source files): +https://repo.maven.apache.org/maven2/com/alibaba/arthas/arthas-repackage-logger/0.0.19/arthas-repackage-logger-0.0.19-sources.jar diff --git a/install-dist/release-docs/licenses/LICENSE-asm-analysis-9.2.txt b/install-dist/release-docs/licenses/LICENSE-asm-commons-9.4.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-asm-analysis-9.2.txt rename to install-dist/release-docs/licenses/LICENSE-asm-commons-9.4.txt diff --git a/install-dist/release-docs/licenses/LICENSE-asm-tree-9.2.txt b/install-dist/release-docs/licenses/LICENSE-asm-tree-9.2.txt deleted file mode 100644 index 5941467be0..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-asm-tree-9.2.txt +++ /dev/null @@ -1 +0,0 @@ -https://asm.ow2.io/license.html diff --git a/install-dist/release-docs/licenses/LICENSE-asm-commons-9.2.txt b/install-dist/release-docs/licenses/LICENSE-asm-tree-9.4.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-asm-commons-9.2.txt rename to install-dist/release-docs/licenses/LICENSE-asm-tree-9.4.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jackson-datatype-jdk8-2.12.6.txt b/install-dist/release-docs/licenses/LICENSE-byte-buddy-1.17.6.txt similarity index 83% rename from install-dist/release-docs/licenses/LICENSE-jackson-datatype-jdk8-2.12.6.txt rename to install-dist/release-docs/licenses/LICENSE-byte-buddy-1.17.6.txt index d645695673..3dda5d1318 100644 --- a/install-dist/release-docs/licenses/LICENSE-jackson-datatype-jdk8-2.12.6.txt +++ b/install-dist/release-docs/licenses/LICENSE-byte-buddy-1.17.6.txt @@ -1,5 +1,45 @@ - - Apache License +======================================================================== +META-INF/licenses/ASM +======================================================================== + + +ASM: a very small and fast Java bytecode manipulation framework +Copyright (c) 2000-2011 INRIA, France Telecom +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +META-INF/LICENSE +======================================================================== + + +This product bundles ASM 9.8, which is available under a "3-clause BSD" +license. For details, see licenses/ASM. For more information visit ${asm.url}. + +Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -175,28 +215,3 @@ 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. diff --git a/install-dist/release-docs/licenses/LICENSE-checker-qual-2.5.2.txt b/install-dist/release-docs/licenses/LICENSE-checker-qual-2.5.2.txt new file mode 100644 index 0000000000..89de354795 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-checker-qual-2.5.2.txt @@ -0,0 +1,17 @@ +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. diff --git a/install-dist/release-docs/licenses/LICENSE-ci-environment-10.0.1.txt b/install-dist/release-docs/licenses/LICENSE-ci-environment-10.0.1.txt new file mode 100644 index 0000000000..725ba9f4ac --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-ci-environment-10.0.1.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Cucumber Ltd + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-cucumber-core-7.21.1.txt b/install-dist/release-docs/licenses/LICENSE-cucumber-core-7.21.1.txt new file mode 100644 index 0000000000..bc6f985cc8 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-cucumber-core-7.21.1.txt @@ -0,0 +1,507 @@ +======================================================================== +Cucumber LICENSE +======================================================================== + + +MIT License + +Copyright (c) 2008 Aslak Hellesøy and contributors +Copyright (c) 2013 Cucumber Ltd and contributors + +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. + +======================================================================== +META-INF/FastDoubleParser-LICENSE +======================================================================== + + +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. + +======================================================================== +META-INF/thirdparty-LICENSE +======================================================================== + + +------ +Third-party license for +fast_float, Copyright (c) 2021 The fast_float authors. MIT License. +https://github.com/fastfloat/fast_float +https://github.com/fastfloat/fast_float/blob/35d523195bf7d57aba0e735ad6eba1e6f71ba8d6/LICENSE-MIT + +MIT License + +Copyright (c) 2021 The fast_float authors + +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. +------ +Third-party license for +bigint, Copyright 2020 Tim Buktu. 2-clause BSD License. +https://github.com/tbuktu/bigint/tree/floatfft +https://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE +https://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE +(We only use those portions of the bigint project that can be licensed under 2-clause BSD License.) + +2-clause BSD License + +Copyright 2022 Tim Buktu + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +Apache License 2.0 for shaded Jackson 2.18.2 +======================================================================== + + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-cucumber-expressions-18.0.1.txt b/install-dist/release-docs/licenses/LICENSE-cucumber-expressions-18.0.1.txt new file mode 100644 index 0000000000..a5e1512d0f --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-cucumber-expressions-18.0.1.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Cucumber Ltd and contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-cucumber-gherkin-7.21.1.txt b/install-dist/release-docs/licenses/LICENSE-cucumber-gherkin-7.21.1.txt new file mode 100644 index 0000000000..d6954ccab2 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-cucumber-gherkin-7.21.1.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2008 Aslak Hellesøy and contributors +Copyright (c) 2013 Cucumber Ltd and contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-cucumber-gherkin-messages-7.21.1.txt b/install-dist/release-docs/licenses/LICENSE-cucumber-gherkin-messages-7.21.1.txt new file mode 100644 index 0000000000..d6954ccab2 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-cucumber-gherkin-messages-7.21.1.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2008 Aslak Hellesøy and contributors +Copyright (c) 2013 Cucumber Ltd and contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-cucumber-guice-7.21.1.txt b/install-dist/release-docs/licenses/LICENSE-cucumber-guice-7.21.1.txt new file mode 100644 index 0000000000..d6954ccab2 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-cucumber-guice-7.21.1.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2008 Aslak Hellesøy and contributors +Copyright (c) 2013 Cucumber Ltd and contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-cucumber-java-7.21.1.txt b/install-dist/release-docs/licenses/LICENSE-cucumber-java-7.21.1.txt new file mode 100644 index 0000000000..d6954ccab2 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-cucumber-java-7.21.1.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2008 Aslak Hellesøy and contributors +Copyright (c) 2013 Cucumber Ltd and contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-cucumber-junit-7.21.1.txt b/install-dist/release-docs/licenses/LICENSE-cucumber-junit-7.21.1.txt new file mode 100644 index 0000000000..d6954ccab2 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-cucumber-junit-7.21.1.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2008 Aslak Hellesøy and contributors +Copyright (c) 2013 Cucumber Ltd and contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-cucumber-plugin-7.21.1.txt b/install-dist/release-docs/licenses/LICENSE-cucumber-plugin-7.21.1.txt new file mode 100644 index 0000000000..d6954ccab2 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-cucumber-plugin-7.21.1.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2008 Aslak Hellesøy and contributors +Copyright (c) 2013 Cucumber Ltd and contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-datatable-7.21.1.txt b/install-dist/release-docs/licenses/LICENSE-datatable-7.21.1.txt new file mode 100644 index 0000000000..d6954ccab2 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-datatable-7.21.1.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2008 Aslak Hellesøy and contributors +Copyright (c) 2013 Cucumber Ltd and contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-docstring-7.21.1.txt b/install-dist/release-docs/licenses/LICENSE-docstring-7.21.1.txt new file mode 100644 index 0000000000..d6954ccab2 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-docstring-7.21.1.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2008 Aslak Hellesøy and contributors +Copyright (c) 2013 Cucumber Ltd and contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-gherkin-31.0.0.txt b/install-dist/release-docs/licenses/LICENSE-gherkin-31.0.0.txt new file mode 100644 index 0000000000..58780c68e1 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-gherkin-31.0.0.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Cucumber Ltd, Gaspar Nagy, Björn Rasmusson, Peter Sergeant, and contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-gremlin-shaded-3.8.1.txt b/install-dist/release-docs/licenses/LICENSE-gremlin-shaded-3.8.1.txt new file mode 100644 index 0000000000..acb3e1ed3b --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-gremlin-shaded-3.8.1.txt @@ -0,0 +1,279 @@ +======================================================================== +META-INF/licenses/kryo +======================================================================== + + +Copyright (c) 2008-2023, Nathan Sweet All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +META-INF/licenses/minlog +======================================================================== + + +Copyright (c) 2008, Nathan Sweet +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +META-INF/licenses/bigint +======================================================================== + + +Copyright 2022 Tim Buktu + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +META-INF/LICENSE +======================================================================== + + +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. + +======================================================================== +BSD-style Licenses +======================================================================== + +The Apache TinkerPop project bundles the following components under the BSD License: + + Kryo (com.esotericsoftware:kryo-shaded:3.0.3 - https://github.com/EsotericSoftware/kryo) + - shaded in gremlin-shaded to org.apache.tinkerpop.shaded.kryo + - for details, see licenses/kryo + minlog (com.esotericsoftware:minlog:1.3.0 - https://github.com/EsotericSoftware/minlog) + - shaded in gremlin-shaded to org.apache.tinkerpop.shaded.minlog + - for details, see licenses/minlog diff --git a/install-dist/release-docs/licenses/LICENSE-spring-context-support-5.3.20.txt b/install-dist/release-docs/licenses/LICENSE-groovy-4.0.25.txt similarity index 83% rename from install-dist/release-docs/licenses/LICENSE-spring-context-support-5.3.20.txt rename to install-dist/release-docs/licenses/LICENSE-groovy-4.0.25.txt index 0a077a3135..8919770514 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-context-support-5.3.20.txt +++ b/install-dist/release-docs/licenses/LICENSE-groovy-4.0.25.txt @@ -1,6 +1,11 @@ - Apache License +======================================================================== +META-INF/LICENSE +======================================================================== + + +Apache License Version 2.0, January 2004 - https://www.apache.org/licenses/ + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -192,7 +197,7 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - https://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -200,20 +205,61 @@ See the License for the specific language governing permissions and limitations under the License. -======================================================================= +------------------------------------------------------------------------ + +ANTLR 4 License + +Antlr4 is released under a BSD 3-clause license. +See licenses/antlr4-license.txt for details. + +------------------------------------------------------------------------ + +ASM License -SPRING FRAMEWORK 5.3.20 SUBCOMPONENTS: +ASM uses a 3-clause BSD license. For details, see licenses/asm-license.txt. -Spring Framework 5.3.20 includes a number of subcomponents -with separate copyright notices and license terms. The product that -includes this file does not necessarily use all the open source -subcomponents referred to below. Your use of the source -code for these subcomponents is subject to the terms and -conditions of the following licenses. +======================================================================== +META-INF/licenses/antlr4-license.txt +======================================================================== ->>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): +ANTLR 4 License +[The "BSD 3-clause license"] +Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +META-INF/licenses/asm-license.txt +======================================================================== + + +ASM License + +ASM: a very small and fast Java bytecode manipulation framework Copyright (c) 2000-2011 INRIA, France Telecom All rights reserved. @@ -243,47 +289,3 @@ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -Copyright (c) 1999-2009, OW2 Consortium - - ->>> CGLIB 3.3 (cglib:cglib:3.3): - -Per the LICENSE file in the CGLIB JAR distribution downloaded from -https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, -CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which -is included above. - - ->>> Objenesis 3.2 (org.objenesis:objenesis:3.2): - -Per the LICENSE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html, Objenesis 3.2 is licensed under the -Apache License, version 2.0, the text of which is included above. - -Per the NOTICE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html and corresponding to section 4d of the -Apache License, Version 2.0, in this case for Objenesis: - -Objenesis -Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita - - -=============================================================================== - -To the extent any open source components are licensed under the EPL and/or -other similar licenses that require the source code and/or modifications to -source code to be made available (as would be noted above), you may obtain a -copy of the source code corresponding to the binaries for such open source -components and modifications thereto, if any, (the "Source Files"), by -downloading the Source Files from https://spring.io/projects, Pivotal's website -at https://network.pivotal.io/open-source, or by sending a request, with your -name and address to: Pivotal Software, Inc., 875 Howard Street, 5th floor, San -Francisco, CA 94103, Attention: General Counsel. All such requests should -clearly specify: OPEN SOURCE FILES REQUEST, Attention General Counsel. Pivotal -can mail a copy of the Source Files to you on a CD or equivalent physical -medium. - -This offer to obtain a copy of the Source Files is valid for three years from -the date you acquired this Software product. Alternatively, the Source Files -may accompany the Software. diff --git a/install-dist/release-docs/licenses/LICENSE-grpc-netty-shaded-1.55.3.txt b/install-dist/release-docs/licenses/LICENSE-grpc-netty-shaded-1.55.3.txt new file mode 100644 index 0000000000..3889f0c1e5 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-grpc-netty-shaded-1.55.3.txt @@ -0,0 +1,973 @@ +======================================================================== +LICENSE.txt +======================================================================== + + 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. + +======================================================================== +license/LICENSE.aix-netbsd.txt +======================================================================== + + + LICENSE ISSUES + ============== + + The OpenSSL toolkit stays under a double license, i.e. both the conditions of + the OpenSSL License and the original SSLeay license apply to the toolkit. + See below for the actual license texts. + + OpenSSL License + --------------- + +/* ==================================================================== + * Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + + +======================================================================== +license/LICENSE.boringssl.txt +======================================================================== + +BoringSSL is a fork of OpenSSL. As such, large parts of it fall under OpenSSL +licensing. Files that are completely new have a Google copyright and an ISC +license. This license is reproduced at the bottom of this file. +Contributors to BoringSSL are required to follow the CLA rules for Chromium: +https://cla.developers.google.com/clas +Files in third_party/ have their own licenses, as described therein. The MIT +license, for third_party/fiat, which, unlike other third_party directories, is +compiled into non-test libraries, is included below. +The OpenSSL toolkit stays under a dual license, i.e. both the conditions of the +OpenSSL License and the original SSLeay license apply to the toolkit. See below +for the actual license texts. Actually both licenses are BSD-style Open Source +licenses. In case of any license issues related to OpenSSL please contact +openssl-core@openssl.org. +The following are Google-internal bug numbers where explicit permission from +some authors is recorded for use of their work. (This is purely for our own +record keeping.) + 27287199 + 27287880 + 27287883 + OpenSSL License + --------------- +/* ==================================================================== + * Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + Original SSLeay License + ----------------------- +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ +ISC license used for completely new code in BoringSSL: +/* Copyright (c) 2015, Google Inc. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +The code in third_party/fiat carries the MIT license: +Copyright (c) 2015-2016 the fiat-crypto authors (see +https://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS). +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. +Licenses for support code +------------------------- +Parts of the TLS test suite are under the Go license. This code is not included +in BoringSSL (i.e. libcrypto and libssl) when compiled, however, so +distributing code linked against BoringSSL does not trigger this license: +Copyright (c) 2009 The Go Authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +BoringSSL uses the Chromium test infrastructure to run a continuous build, +trybots etc. The scripts which manage this, and the script for generating build +metadata, are under the Chromium license. Distributing code linked against +BoringSSL does not trigger this license. +Copyright 2015 The Chromium Authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +======================================================================== +license/LICENSE.mvn-wrapper.txt +======================================================================== + + + 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. + +======================================================================== +license/LICENSE.tomcat-native.txt +======================================================================== + + 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. diff --git a/install-dist/release-docs/licenses/LICENSE-hamcrest-3.0.txt b/install-dist/release-docs/licenses/LICENSE-hamcrest-3.0.txt new file mode 100644 index 0000000000..c43f6b7b92 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-hamcrest-3.0.txt @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2000-2024, www.hamcrest.org + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/install-dist/release-docs/licenses/LICENSE-html-formatter-21.9.0.txt b/install-dist/release-docs/licenses/LICENSE-html-formatter-21.9.0.txt new file mode 100644 index 0000000000..2f4202ee27 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-html-formatter-21.9.0.txt @@ -0,0 +1,99 @@ +======================================================================== +Cucumber LICENSE +======================================================================== + + +MIT License + +Copyright (c) 2022 SmartBear Software and contributors + +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. + +======================================================================== +io/cucumber/htmlformatter/main.js.LICENSE.txt +======================================================================== + + +/*! + * Determine if an object is a Buffer + * + * @author Feross Aboukhadijeh + * @license MIT + */ + +/*! + * repeat-string + * + * Copyright (c) 2014-2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + +/*! ***************************************************************************** +Copyright (C) Microsoft. All rights reserved. +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 + +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ + +/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * @license React + * scheduler.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** @license React v17.0.2 + * react-is.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ diff --git a/install-dist/release-docs/licenses/LICENSE-ivy-2.6.0.txt b/install-dist/release-docs/licenses/LICENSE-ivy-2.6.0.txt new file mode 100644 index 0000000000..d5d48cfdef --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-ivy-2.6.0.txt @@ -0,0 +1,290 @@ + 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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +------------------------------------------------------------------------------ +License for JCraft JSch package +------------------------------------------------------------------------------ +Copyright (c) 2002,2003,2004,2005,2006,2007 Atsuhiko Yamanaka, JCraft,Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------------------ +License for jQuery +------------------------------------------------------------------------------ +Copyright (c) 2007 John Resig, http://jquery.com/ + +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. + +------------------------------------------------------------------------------ +License for ganymed ssh library +------------------------------------------------------------------------------ + +Copyright (c) 2005 - 2006 Swiss Federal Institute of Technology (ETH Zurich), + Department of Computer Science (http://www.inf.ethz.ch), + Christian Plattner. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +a.) Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +b.) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +c.) Neither the name of ETH Zurich nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE diff --git a/install-dist/release-docs/licenses/LICENSE-arthas-agent-attach-3.7.1.txt b/install-dist/release-docs/licenses/LICENSE-jackson-databind-2.12.7.1.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-arthas-agent-attach-3.7.1.txt rename to install-dist/release-docs/licenses/LICENSE-jackson-databind-2.12.7.1.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jackson-dataformat-yaml-2.9.3.txt b/install-dist/release-docs/licenses/LICENSE-jackson-dataformat-yaml-2.15.2.txt similarity index 86% rename from install-dist/release-docs/licenses/LICENSE-jackson-dataformat-yaml-2.9.3.txt rename to install-dist/release-docs/licenses/LICENSE-jackson-dataformat-yaml-2.15.2.txt index 8d5775d40c..cd0270b9fb 100644 --- a/install-dist/release-docs/licenses/LICENSE-jackson-dataformat-yaml-2.9.3.txt +++ b/install-dist/release-docs/licenses/LICENSE-jackson-dataformat-yaml-2.15.2.txt @@ -1,7 +1,7 @@ This copy of Jackson JSON processor YAML module 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. +specific rights regarding derivative works. You may obtain a copy of the License at: diff --git a/install-dist/release-docs/licenses/LICENSE-arthas-packaging-3.7.1.txt b/install-dist/release-docs/licenses/LICENSE-jackson-datatype-jdk8-2.12.7.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-arthas-packaging-3.7.1.txt rename to install-dist/release-docs/licenses/LICENSE-jackson-datatype-jdk8-2.12.7.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jackson-datatype-jdk8-2.15.2.txt b/install-dist/release-docs/licenses/LICENSE-jackson-datatype-jdk8-2.15.2.txt new file mode 100644 index 0000000000..0a97ea4ffb --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-jackson-datatype-jdk8-2.15.2.txt @@ -0,0 +1,8 @@ +This copy of Jackson JSON processor Java 8 datatype module 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 derivative works. + +You may obtain a copy of the License at: + +http://www.apache.org/licenses/LICENSE-2.0 diff --git a/install-dist/release-docs/licenses/LICENSE-jackson-datatype-jsr310-2.12.6.txt b/install-dist/release-docs/licenses/LICENSE-jackson-datatype-jsr310-2.12.7.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jackson-datatype-jsr310-2.12.6.txt rename to install-dist/release-docs/licenses/LICENSE-jackson-datatype-jsr310-2.12.7.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jackson-module-parameter-names-2.12.6.txt b/install-dist/release-docs/licenses/LICENSE-jackson-module-parameter-names-2.12.6.txt deleted file mode 100644 index d645695673..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-jackson-module-parameter-names-2.12.6.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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. diff --git a/install-dist/release-docs/licenses/LICENSE-commons-configuration2-2.8.0.txt b/install-dist/release-docs/licenses/LICENSE-jackson-module-parameter-names-2.12.7.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-commons-configuration2-2.8.0.txt rename to install-dist/release-docs/licenses/LICENSE-jackson-module-parameter-names-2.12.7.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jackson-module-parameter-names-2.15.2.txt b/install-dist/release-docs/licenses/LICENSE-jackson-module-parameter-names-2.15.2.txt new file mode 100644 index 0000000000..f49b9ee83a --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-jackson-module-parameter-names-2.15.2.txt @@ -0,0 +1,8 @@ +This copy of Jackson JSON processor Java 8 parameter names module 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 derivative works. + +You may obtain a copy of the License at: + +http://www.apache.org/licenses/LICENSE-2.0 diff --git a/install-dist/release-docs/licenses/LICENSE-javassist-3.24.0-GA.txt b/install-dist/release-docs/licenses/LICENSE-javassist-3.24.0-GA.txt deleted file mode 100644 index d645695673..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-javassist-3.24.0-GA.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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. diff --git a/install-dist/release-docs/licenses/LICENSE-javassist-3.25.0-GA.txt b/install-dist/release-docs/licenses/LICENSE-javassist-3.25.0-GA.txt deleted file mode 100644 index d645695673..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-javassist-3.25.0-GA.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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. diff --git a/install-dist/release-docs/licenses/LICENSE-javax-websocket-client-impl-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-javax-websocket-client-impl-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-javax-websocket-client-impl-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-javax-websocket-client-impl-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-javax-websocket-server-impl-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-javax-websocket-server-impl-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-javax-websocket-server-impl-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-javax-websocket-server-impl-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jcodings-1.0.58.txt b/install-dist/release-docs/licenses/LICENSE-jcodings-1.0.58.txt new file mode 100644 index 0000000000..f85e365de0 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-jcodings-1.0.58.txt @@ -0,0 +1,17 @@ +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. diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-annotations-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-annotations-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-annotations-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-annotations-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-client-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-client-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-client-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-client-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-continuation-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-continuation-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-continuation-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-continuation-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-http-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-http-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-http-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-http-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-io-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-io-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-io-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-io-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-plus-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-plus-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-plus-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-plus-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-security-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-security-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-security-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-security-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-server-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-server-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-server-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-server-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-servlet-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-servlet-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-servlet-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-servlet-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-servlets-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-servlets-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-servlets-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-servlets-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-util-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-util-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-util-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-util-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-util-ajax-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-util-ajax-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-util-ajax-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-util-ajax-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-webapp-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-webapp-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-webapp-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-webapp-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-jetty-xml-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-jetty-xml-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jetty-xml-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-jetty-xml-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-joni-2.2.1.txt b/install-dist/release-docs/licenses/LICENSE-joni-2.2.1.txt new file mode 100644 index 0000000000..9341f05eee --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-joni-2.2.1.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 JRuby Team + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-jsonassert-1.5.0.txt b/install-dist/release-docs/licenses/LICENSE-jsonassert-1.5.0.txt deleted file mode 100644 index d645695673..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-jsonassert-1.5.0.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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. diff --git a/install-dist/release-docs/licenses/LICENSE-jackson-databind-2.12.6.1.txt b/install-dist/release-docs/licenses/LICENSE-jsonassert-1.5.1.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jackson-databind-2.12.6.1.txt rename to install-dist/release-docs/licenses/LICENSE-jsonassert-1.5.1.txt diff --git a/install-dist/release-docs/licenses/LICENSE-junit-xml-formatter-0.7.1.txt b/install-dist/release-docs/licenses/LICENSE-junit-xml-formatter-0.7.1.txt new file mode 100644 index 0000000000..51c51e4655 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-junit-xml-formatter-0.7.1.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) M.P. Korstanje + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-log4j-api-2.15.0.txt b/install-dist/release-docs/licenses/LICENSE-log4j-api-2.15.0.txt deleted file mode 100644 index d645695673..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-log4j-api-2.15.0.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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. diff --git a/install-dist/release-docs/licenses/LICENSE-log4j-core-2.15.0.txt b/install-dist/release-docs/licenses/LICENSE-log4j-core-2.15.0.txt deleted file mode 100644 index 6279e5206d..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-log4j-core-2.15.0.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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 1999-2005 The Apache Software Foundation - - 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/install-dist/release-docs/licenses/LICENSE-log4j-slf4j-impl-2.15.0.txt b/install-dist/release-docs/licenses/LICENSE-log4j-slf4j-impl-2.15.0.txt deleted file mode 100644 index d645695673..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-log4j-slf4j-impl-2.15.0.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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. diff --git a/install-dist/release-docs/licenses/LICENSE-lombok-1.18.30.txt b/install-dist/release-docs/licenses/LICENSE-lombok-1.18.30.txt new file mode 100644 index 0000000000..465d9bf8f7 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-lombok-1.18.30.txt @@ -0,0 +1,104 @@ +Copyright (C) 2009-2021 The Project Lombok Authors. + +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. + +============================================================================== +Licenses for included components: + +org.ow2.asm:asm +org.ow2.asm:asm-analysis +org.ow2.asm:asm-commons +org.ow2.asm:asm-tree +org.ow2.asm:asm-util +ASM: a very small and fast Java bytecode manipulation framework + Copyright (c) 2000-2011 INRIA, France Telecom + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +rzwitserloot/com.zwitserloot.cmdreader + + Copyright © 2010 Reinier Zwitserloot. + + 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. + +------------------------------------------------------------------------------ + +projectlombok/lombok.patcher + + Copyright (C) 2009-2021 The Project Lombok Authors. + + 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. + +------------------------------------------------------------------------------ diff --git a/install-dist/release-docs/licenses/LICENSE-lucene-analyzers-common-7.4.0.txt b/install-dist/release-docs/licenses/LICENSE-lucene-analyzers-common-7.4.0.txt new file mode 100644 index 0000000000..f8da3a9d2d --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-lucene-analyzers-common-7.4.0.txt @@ -0,0 +1,474 @@ +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. + + + +Some code in core/src/java/org/apache/lucene/util/UnicodeUtil.java was +derived from unicode conversion examples available at +http://www.unicode.org/Public/PROGRAMS/CVTUTF. Here is the copyright +from those sources: + +/* + * Copyright 2001-2004 Unicode, Inc. + * + * Disclaimer + * + * This source code is provided as is by Unicode, Inc. No claims are + * made as to fitness for any particular purpose. No warranties of any + * kind are expressed or implied. The recipient agrees to determine + * applicability of information provided. If this file has been + * purchased on magnetic or optical media from Unicode, Inc., the + * sole remedy for any claim will be exchange of defective media + * within 90 days of receipt. + * + * Limitations on Rights to Redistribute This Code + * + * Unicode, Inc. hereby grants the right to freely use the information + * supplied in this file in the creation of products supporting the + * Unicode Standard, and to make copies of this file in any form + * for internal or external distribution as long as this notice + * remains attached. + */ + + +Some code in core/src/java/org/apache/lucene/util/ArrayUtil.java was +derived from Python 2.4.2 sources available at +http://www.python.org. Full license is here: + + http://www.python.org/download/releases/2.4.2/license/ + +Some code in core/src/java/org/apache/lucene/util/UnicodeUtil.java was +derived from Python 3.1.2 sources available at +http://www.python.org. Full license is here: + + http://www.python.org/download/releases/3.1.2/license/ + +Some code in core/src/java/org/apache/lucene/util/automaton was +derived from Brics automaton sources available at +www.brics.dk/automaton/. Here is the copyright from those sources: + +/* + * Copyright (c) 2001-2009 Anders Moeller + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +The levenshtein automata tables in core/src/java/org/apache/lucene/util/automaton +were automatically generated with the moman/finenight FSA package. +Here is the copyright for those sources: + +# Copyright (c) 2010, Jean-Philippe Barrette-LaPierre, +# +# 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. + +Some code in core/src/java/org/apache/lucene/util/UnicodeUtil.java was +derived from ICU (http://www.icu-project.org) +The full license is available here: + http://source.icu-project.org/repos/icu/icu/trunk/license.html + +/* + * Copyright (C) 1999-2010, International Business Machines + * Corporation and others. 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, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * provided that the above copyright notice(s) and this permission notice appear + * in all copies of the Software and that both the above copyright notice(s) and + * this permission notice appear in supporting documentation. + * + * 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 OF THIRD PARTY RIGHTS. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE + * LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR + * ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Except as contained in this notice, the name of a copyright holder shall not + * be used in advertising or otherwise to promote the sale, use or other + * dealings in this Software without prior written authorization of the + * copyright holder. + */ + +The following license applies to the Snowball stemmers: + +Copyright (c) 2001, Dr Martin Porter +Copyright (c) 2002, Richard Boulton +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holders nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The following license applies to the KStemmer: + +Copyright © 2003, +Center for Intelligent Information Retrieval, +University of Massachusetts, Amherst. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. The names "Center for Intelligent Information Retrieval" and +"University of Massachusetts" must not be used to endorse or promote products +derived from this software without prior written permission. To obtain +permission, contact info@ciir.cs.umass.edu. + +THIS SOFTWARE IS PROVIDED BY UNIVERSITY OF MASSACHUSETTS AND OTHER CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +The following license applies to the Morfologik project: + +Copyright (c) 2006 Dawid Weiss +Copyright (c) 2007-2011 Dawid Weiss, Marcin Miłkowski +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Morfologik nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +The dictionary comes from Morfologik project. Morfologik uses data from +Polish ispell/myspell dictionary hosted at http://www.sjp.pl/slownik/en/ and +is licenced on the terms of (inter alia) LGPL and Creative Commons +ShareAlike. The part-of-speech tags were added in Morfologik project and +are not found in the data from sjp.pl. The tagset is similar to IPI PAN +tagset. + +--- + +The following license applies to the Morfeusz project, +used by org.apache.lucene.analysis.morfologik. + +BSD-licensed dictionary of Polish (SGJP) +http://sgjp.pl/morfeusz/ + +Copyright © 2011 Zygmunt Saloni, Włodzimierz Gruszczyński, +Marcin Woliński, Robert Wołosz + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDERS “AS IS” AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/install-dist/release-docs/licenses/LICENSE-messages-27.2.0.txt b/install-dist/release-docs/licenses/LICENSE-messages-27.2.0.txt new file mode 100644 index 0000000000..520b896f74 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-messages-27.2.0.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Cucumber Ltd and contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-mockito-core-5.19.0.txt b/install-dist/release-docs/licenses/LICENSE-mockito-core-5.19.0.txt new file mode 100644 index 0000000000..5a311f7c5c --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-mockito-core-5.19.0.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2007 Mockito contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-netty-4.1.137.Final.txt b/install-dist/release-docs/licenses/LICENSE-netty-4.1.137.Final.txt new file mode 100644 index 0000000000..3e7894e338 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-netty-4.1.137.Final.txt @@ -0,0 +1,3044 @@ +======================================================================== +LICENSE.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================== +license/LICENSE.aalto-xml.txt +======================================================================== + + +This copy of Aalto XML processor 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: + +https://www.apache.org/licenses/ + +A copy is also included with both the downloadable source code package +and jar that contains class bytecodes, as file "ASL 2.0". In both cases, +that file should be located next to this file: in source distribution +the location should be "release-notes/asl"; and in jar "META-INF/" + +======================================================================== +license/LICENSE.base64.txt +======================================================================== + + +The person or persons who have associated work with this document (the +"Dedicator" or "Certifier") hereby either (a) certifies that, to the best of +his knowledge, the work of authorship identified is in the public domain of +the country from which the work is published, or (b) hereby dedicates whatever +copyright the dedicators holds in the work of authorship identified below (the +"Work") to the public domain. A certifier, moreover, dedicates any copyright +interest he may have in the associated work, and for these purposes, is +described as a "dedicator" below. + +A certifier has taken reasonable steps to verify the copyright status of this +work. Certifier recognizes that his good faith efforts may not shield him from +liability if in fact the work certified is not in the public domain. + +Dedicator makes this dedication for the benefit of the public at large and to +the detriment of the Dedicator's heirs and successors. Dedicator intends this +dedication to be an overt act of relinquishment in perpetuate of all present +and future rights under copyright law, whether vested or contingent, in the +Work. Dedicator understands that such relinquishment of all rights includes +the relinquishment of all rights to enforce (by lawsuit or otherwise) those +copyrights in the Work. + +Dedicator recognizes that, once placed in the public domain, the Work may be +freely reproduced, distributed, transmitted, used, modified, built upon, or +otherwise exploited by anyone for any purpose, commercial or non-commercial, +and in any way, including by methods that have not yet been invented or +conceived. + +======================================================================== +license/LICENSE.bouncycastle.txt +======================================================================== + + +The MIT License (MIT) + +Copyright (c) 2000 - 2013 The Legion of the Bouncy Castle Inc. + (https://www.bouncycastle.org) + +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. + +======================================================================== +license/LICENSE.brotli4j.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================== +license/LICENSE.caliper.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================== +license/LICENSE.commons-lang.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + +======================================================================== +license/LICENSE.commons-logging.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + +======================================================================== +license/LICENSE.compress-lzf.txt +======================================================================== + + +Copyright 2009-2010 Ning, Inc. + +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 https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS,WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations under +the License. + +======================================================================== +license/LICENSE.dnsinfo.txt +======================================================================== + + +/* + * Copyright (c) 2004-2006, 2008, 2009, 2011 Apple Inc. All rights reserved. + * + * @APPLE_LICENSE_HEADER_START@ + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * https://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + * + * @APPLE_LICENSE_HEADER_END@ + */ + +======================================================================== +license/LICENSE.harmony.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + +======================================================================== +license/LICENSE.hpack.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================== +license/LICENSE.hyper-hpack.txt +======================================================================== + + +The MIT License (MIT) + +Copyright (c) 2014 Cory Benfield + +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. + +======================================================================== +license/LICENSE.jboss-marshalling.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + +======================================================================== +license/LICENSE.jbzip2.txt +======================================================================== + + +Copyright (c) 2010-2011 Matthew J. Francis and Contributors of the jbzip2 Project + +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. + +======================================================================== +license/LICENSE.jctools.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + +======================================================================== +license/LICENSE.jfastlz.txt +======================================================================== + + +The MIT License + +Copyright (c) 2009 William Kinney + +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. + +======================================================================== +license/LICENSE.jsr166y.txt +======================================================================== + + +The person or persons who have associated work with this document (the +"Dedicator" or "Certifier") hereby either (a) certifies that, to the best of +his knowledge, the work of authorship identified is in the public domain of +the country from which the work is published, or (b) hereby dedicates whatever +copyright the dedicators holds in the work of authorship identified below (the +"Work") to the public domain. A certifier, moreover, dedicates any copyright +interest he may have in the associated work, and for these purposes, is +described as a "dedicator" below. + +A certifier has taken reasonable steps to verify the copyright status of this +work. Certifier recognizes that his good faith efforts may not shield him from +liability if in fact the work certified is not in the public domain. + +Dedicator makes this dedication for the benefit of the public at large and to +the detriment of the Dedicator's heirs and successors. Dedicator intends this +dedication to be an overt act of relinquishment in perpetuity of all present +and future rights under copyright law, whether vested or contingent, in the +Work. Dedicator understands that such relinquishment of all rights includes +the relinquishment of all rights to enforce (by lawsuit or otherwise) those +copyrights in the Work. + +Dedicator recognizes that, once placed in the public domain, the Work may be +freely reproduced, distributed, transmitted, used, modified, built upon, or +otherwise exploited by anyone for any purpose, commercial or non-commercial, +and in any way, including by methods that have not yet been invented or +conceived. + +======================================================================== +license/LICENSE.jzlib.txt +======================================================================== + + +Copyright (c) 2000,2001,2002,2003,2004 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +license/LICENSE.libdivsufsort.txt +======================================================================== + + +Copyright (c) 2003-2008 Yuta Mori 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. + +======================================================================== +license/LICENSE.log4j.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + +======================================================================== +license/LICENSE.lz4.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================== +license/LICENSE.lzma-java.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================== +license/LICENSE.mvn-wrapper.txt +======================================================================== + + +Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================== +license/LICENSE.nghttp2-hpack.txt +======================================================================== + + +The MIT License + +Copyright (c) 2012, 2014, 2015, 2016 Tatsuhiro Tsujikawa +Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors + +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. + +======================================================================== +license/LICENSE.protobuf.txt +======================================================================== + + +Protocol Buffers - Google's data interchange format +Copyright 2013 Google Inc. All rights reserved. +https://developers.google.com/protocol-buffers/ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. + +======================================================================== +license/LICENSE.slf4j.txt +======================================================================== + + +/* + * Copyright (c) 2004-2007 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. + */ + +======================================================================== +license/LICENSE.snappy.txt +======================================================================== + + +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. +* Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +license/LICENSE.webbit.txt +======================================================================== + + +(BSD License: https://www.opensource.org/licenses/bsd-license) + +Copyright (c) 2011, Joe Walnes, Aslak Hellesøy and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the +following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of the Webbit nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +license/LICENSE.zstd-jni.txt +======================================================================== + + +Zstd-jni: JNI bindings to Zstd Library + +Copyright (c) 2015-present, Luben Karavelov/ All rights reserved. + +BSD License + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/install-dist/release-docs/licenses/LICENSE-netty-tcnative-2.0.81.Final.txt b/install-dist/release-docs/licenses/LICENSE-netty-tcnative-2.0.81.Final.txt new file mode 100644 index 0000000000..2439050582 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-netty-tcnative-2.0.81.Final.txt @@ -0,0 +1,861 @@ +======================================================================== +LICENSE.txt +======================================================================== + + 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. + +======================================================================== +license/LICENSE.boringssl.txt +======================================================================== + + + 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. + + +Licenses for support code +------------------------- + +Parts of the TLS test suite are under the Go license. This code is not included +in BoringSSL (i.e. libcrypto and libssl) when compiled, however, so +distributing code linked against BoringSSL does not trigger this license: + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +license/LICENSE.mvn-wrapper.txt +======================================================================== + + + 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. + +======================================================================== +license/LICENSE.tomcat-native.txt +======================================================================== + + 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. diff --git a/install-dist/release-docs/licenses/LICENSE-org.abego.treelayout.core-1.0.3.txt b/install-dist/release-docs/licenses/LICENSE-org.abego.treelayout.core-1.0.3.txt new file mode 100644 index 0000000000..1620af43b3 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-org.abego.treelayout.core-1.0.3.txt @@ -0,0 +1,27 @@ +[The "BSD license"] +Copyright (c) 2011, abego Software GmbH, Germany (http://www.abego.org) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of the abego Software GmbH nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/install-dist/release-docs/licenses/LICENSE-powermock-api-mockito2-2.0.0-RC.3.txt b/install-dist/release-docs/licenses/LICENSE-powermock-api-mockito2-2.0.0-RC.3.txt deleted file mode 100644 index 0cf7145a38..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-powermock-api-mockito2-2.0.0-RC.3.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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 2007-2017 PowerMock Contributors - - 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/install-dist/release-docs/licenses/LICENSE-powermock-api-support-2.0.0-RC.3.txt b/install-dist/release-docs/licenses/LICENSE-powermock-api-support-2.0.0-RC.3.txt deleted file mode 100644 index 0cf7145a38..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-powermock-api-support-2.0.0-RC.3.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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 2007-2017 PowerMock Contributors - - 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/install-dist/release-docs/licenses/LICENSE-powermock-classloading-base-2.0.0-RC.3.txt b/install-dist/release-docs/licenses/LICENSE-powermock-classloading-base-2.0.0-RC.3.txt deleted file mode 100644 index 0cf7145a38..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-powermock-classloading-base-2.0.0-RC.3.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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 2007-2017 PowerMock Contributors - - 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/install-dist/release-docs/licenses/LICENSE-powermock-classloading-xstream-2.0.0-RC.3.txt b/install-dist/release-docs/licenses/LICENSE-powermock-classloading-xstream-2.0.0-RC.3.txt deleted file mode 100644 index 0cf7145a38..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-powermock-classloading-xstream-2.0.0-RC.3.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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 2007-2017 PowerMock Contributors - - 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/install-dist/release-docs/licenses/LICENSE-powermock-core-2.0.0-RC.3.txt b/install-dist/release-docs/licenses/LICENSE-powermock-core-2.0.0-RC.3.txt deleted file mode 100644 index 0cf7145a38..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-powermock-core-2.0.0-RC.3.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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 2007-2017 PowerMock Contributors - - 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/install-dist/release-docs/licenses/LICENSE-powermock-module-junit4-2.0.0-RC.3.txt b/install-dist/release-docs/licenses/LICENSE-powermock-module-junit4-2.0.0-RC.3.txt deleted file mode 100644 index 0cf7145a38..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-powermock-module-junit4-2.0.0-RC.3.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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 2007-2017 PowerMock Contributors - - 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/install-dist/release-docs/licenses/LICENSE-powermock-module-junit4-common-2.0.0-RC.3.txt b/install-dist/release-docs/licenses/LICENSE-powermock-module-junit4-common-2.0.0-RC.3.txt deleted file mode 100644 index 0cf7145a38..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-powermock-module-junit4-common-2.0.0-RC.3.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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 2007-2017 PowerMock Contributors - - 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/install-dist/release-docs/licenses/LICENSE-powermock-module-junit4-rule-2.0.0-RC.3.txt b/install-dist/release-docs/licenses/LICENSE-powermock-module-junit4-rule-2.0.0-RC.3.txt deleted file mode 100644 index 0cf7145a38..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-powermock-module-junit4-rule-2.0.0-RC.3.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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 2007-2017 PowerMock Contributors - - 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/install-dist/release-docs/licenses/LICENSE-powermock-reflect-2.0.0-RC.3.txt b/install-dist/release-docs/licenses/LICENSE-powermock-reflect-2.0.0-RC.3.txt deleted file mode 100644 index 0cf7145a38..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-powermock-reflect-2.0.0-RC.3.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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 2007-2017 PowerMock Contributors - - 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/install-dist/release-docs/licenses/LICENSE-protobuf-java-3.22.3.txt b/install-dist/release-docs/licenses/LICENSE-protobuf-java-3.22.3.txt new file mode 100644 index 0000000000..19b305b000 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-protobuf-java-3.22.3.txt @@ -0,0 +1,32 @@ +Copyright 2008 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. diff --git a/install-dist/release-docs/licenses/LICENSE-protobuf-java-util-3.22.3.txt b/install-dist/release-docs/licenses/LICENSE-protobuf-java-util-3.22.3.txt new file mode 100644 index 0000000000..19b305b000 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-protobuf-java-util-3.22.3.txt @@ -0,0 +1,32 @@ +Copyright 2008 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. diff --git a/install-dist/release-docs/licenses/LICENSE-query-13.6.0.txt b/install-dist/release-docs/licenses/LICENSE-query-13.6.0.txt new file mode 100644 index 0000000000..725ba9f4ac --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-query-13.6.0.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Cucumber Ltd + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-snakeyaml-1.28.txt b/install-dist/release-docs/licenses/LICENSE-snakeyaml-1.28.txt deleted file mode 100644 index d645695673..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-snakeyaml-1.28.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-aop-5.3.27.txt b/install-dist/release-docs/licenses/LICENSE-spring-aop-5.3.27.txt index d645695673..a0299cad4d 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-aop-5.3.27.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-aop-5.3.27.txt @@ -1,7 +1,6 @@ - Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -193,10 +192,98 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +======================================================================= + +SPRING FRAMEWORK 5.3.27 SUBCOMPONENTS: + +Spring Framework 5.3.27 includes a number of subcomponents +with separate copyright notices and license terms. The product that +includes this file does not necessarily use all the open source +subcomponents referred to below. Your use of the source +code for these subcomponents is subject to the terms and +conditions of the following licenses. + + +>>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): + +Copyright (c) 2000-2011 INRIA, France Telecom +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +Copyright (c) 1999-2009, OW2 Consortium + + +>>> CGLIB 3.3 (cglib:cglib:3.3): + +Per the LICENSE file in the CGLIB JAR distribution downloaded from +https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, +CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which +is included above. + + +>>> Objenesis 3.2 (org.objenesis:objenesis:3.2): + +Per the LICENSE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html, Objenesis 3.2 is licensed under the +Apache License, version 2.0, the text of which is included above. + +Per the NOTICE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html and corresponding to section 4d of the +Apache License, Version 2.0, in this case for Objenesis: + +Objenesis +Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita + + +=============================================================================== + +To the extent any open source components are licensed under the EPL and/or +other similar licenses that require the source code and/or modifications to +source code to be made available (as would be noted above), you may obtain a +copy of the source code corresponding to the binaries for such open source +components and modifications thereto, if any, (the "Source Files"), by +downloading the Source Files from https://spring.io/projects, Pivotal's website +at https://network.pivotal.io/open-source, or by sending a request, with your +name and address to: Pivotal Software, Inc., 875 Howard Street, 5th floor, San +Francisco, CA 94103, Attention: General Counsel. All such requests should +clearly specify: OPEN SOURCE FILES REQUEST, Attention General Counsel. Pivotal +can mail a copy of the Source Files to you on a CD or equivalent physical +medium. + +This offer to obtain a copy of the Source Files is valid for three years from +the date you acquired this Software product. Alternatively, the Source Files +may accompany the Software. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-beans-5.3.27.txt b/install-dist/release-docs/licenses/LICENSE-spring-beans-5.3.27.txt index d645695673..a0299cad4d 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-beans-5.3.27.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-beans-5.3.27.txt @@ -1,7 +1,6 @@ - Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -193,10 +192,98 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +======================================================================= + +SPRING FRAMEWORK 5.3.27 SUBCOMPONENTS: + +Spring Framework 5.3.27 includes a number of subcomponents +with separate copyright notices and license terms. The product that +includes this file does not necessarily use all the open source +subcomponents referred to below. Your use of the source +code for these subcomponents is subject to the terms and +conditions of the following licenses. + + +>>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): + +Copyright (c) 2000-2011 INRIA, France Telecom +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +Copyright (c) 1999-2009, OW2 Consortium + + +>>> CGLIB 3.3 (cglib:cglib:3.3): + +Per the LICENSE file in the CGLIB JAR distribution downloaded from +https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, +CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which +is included above. + + +>>> Objenesis 3.2 (org.objenesis:objenesis:3.2): + +Per the LICENSE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html, Objenesis 3.2 is licensed under the +Apache License, version 2.0, the text of which is included above. + +Per the NOTICE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html and corresponding to section 4d of the +Apache License, Version 2.0, in this case for Objenesis: + +Objenesis +Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita + + +=============================================================================== + +To the extent any open source components are licensed under the EPL and/or +other similar licenses that require the source code and/or modifications to +source code to be made available (as would be noted above), you may obtain a +copy of the source code corresponding to the binaries for such open source +components and modifications thereto, if any, (the "Source Files"), by +downloading the Source Files from https://spring.io/projects, Pivotal's website +at https://network.pivotal.io/open-source, or by sending a request, with your +name and address to: Pivotal Software, Inc., 875 Howard Street, 5th floor, San +Francisco, CA 94103, Attention: General Counsel. All such requests should +clearly specify: OPEN SOURCE FILES REQUEST, Attention General Counsel. Pivotal +can mail a copy of the Source Files to you on a CD or equivalent physical +medium. + +This offer to obtain a copy of the Source Files is valid for three years from +the date you acquired this Software product. Alternatively, the Source Files +may accompany the Software. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-boot-actuator-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-2.5.15.txt similarity index 99% rename from install-dist/release-docs/licenses/LICENSE-spring-boot-actuator-2.5.14.txt rename to install-dist/release-docs/licenses/LICENSE-spring-boot-2.5.15.txt index 823c1c8e98..ff77379631 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-actuator-2.5.14.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-2.5.15.txt @@ -199,4 +199,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 + limitations under the License. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-boot-actuator-autoconfigure-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-actuator-2.5.15.txt similarity index 99% rename from install-dist/release-docs/licenses/LICENSE-spring-boot-actuator-autoconfigure-2.5.14.txt rename to install-dist/release-docs/licenses/LICENSE-spring-boot-actuator-2.5.15.txt index 823c1c8e98..ff77379631 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-actuator-autoconfigure-2.5.14.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-actuator-2.5.15.txt @@ -199,4 +199,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 + limitations under the License. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-boot-autoconfigure-2.5.0.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-actuator-autoconfigure-2.5.15.txt similarity index 99% rename from install-dist/release-docs/licenses/LICENSE-spring-boot-autoconfigure-2.5.0.txt rename to install-dist/release-docs/licenses/LICENSE-spring-boot-actuator-autoconfigure-2.5.15.txt index 823c1c8e98..ff77379631 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-autoconfigure-2.5.0.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-actuator-autoconfigure-2.5.15.txt @@ -199,4 +199,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 + limitations under the License. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-boot-autoconfigure-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-autoconfigure-2.5.15.txt similarity index 99% rename from install-dist/release-docs/licenses/LICENSE-spring-boot-autoconfigure-2.5.14.txt rename to install-dist/release-docs/licenses/LICENSE-spring-boot-autoconfigure-2.5.15.txt index 823c1c8e98..ff77379631 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-autoconfigure-2.5.14.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-autoconfigure-2.5.15.txt @@ -199,4 +199,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 + limitations under the License. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-boot-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-jarmode-layertools-2.7.1.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-spring-boot-2.5.14.txt rename to install-dist/release-docs/licenses/LICENSE-spring-boot-jarmode-layertools-2.7.1.txt diff --git a/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-2.5.0.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-2.5.0.txt deleted file mode 100644 index 823c1c8e98..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-2.5.0.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-2.5.14.txt deleted file mode 100644 index 823c1c8e98..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-2.5.14.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-2.5.15.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-2.5.15.txt new file mode 100644 index 0000000000..ff77379631 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-2.5.15.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-actuator-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-actuator-2.5.14.txt deleted file mode 100644 index 823c1c8e98..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-actuator-2.5.14.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-actuator-2.5.15.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-actuator-2.5.15.txt new file mode 100644 index 0000000000..ff77379631 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-actuator-2.5.15.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-jetty-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-jetty-2.5.14.txt deleted file mode 100644 index 823c1c8e98..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-jetty-2.5.14.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-jetty-2.5.15.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-jetty-2.5.15.txt new file mode 100644 index 0000000000..ff77379631 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-jetty-2.5.15.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-json-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-json-2.5.14.txt deleted file mode 100644 index 823c1c8e98..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-json-2.5.14.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-json-2.5.15.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-json-2.5.15.txt new file mode 100644 index 0000000000..ff77379631 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-json-2.5.15.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-log4j2-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-log4j2-2.5.14.txt deleted file mode 100644 index 823c1c8e98..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-log4j2-2.5.14.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-log4j2-2.5.15.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-log4j2-2.5.15.txt new file mode 100644 index 0000000000..ff77379631 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-log4j2-2.5.15.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-test-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-test-2.5.14.txt deleted file mode 100644 index 823c1c8e98..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-test-2.5.14.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-test-2.5.15.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-test-2.5.15.txt new file mode 100644 index 0000000000..ff77379631 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-test-2.5.15.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-web-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-web-2.5.14.txt deleted file mode 100644 index 823c1c8e98..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-web-2.5.14.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-web-2.5.15.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-web-2.5.15.txt new file mode 100644 index 0000000000..ff77379631 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-starter-web-2.5.15.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-test-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-test-2.5.14.txt deleted file mode 100644 index 823c1c8e98..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-test-2.5.14.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-test-2.5.15.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-test-2.5.15.txt new file mode 100644 index 0000000000..ff77379631 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-test-2.5.15.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-test-autoconfigure-2.5.14.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-test-autoconfigure-2.5.14.txt deleted file mode 100644 index 823c1c8e98..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-boot-test-autoconfigure-2.5.14.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-boot-test-autoconfigure-2.5.15.txt b/install-dist/release-docs/licenses/LICENSE-spring-boot-test-autoconfigure-2.5.15.txt new file mode 100644 index 0000000000..ff77379631 --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-spring-boot-test-autoconfigure-2.5.15.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://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 + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/install-dist/release-docs/licenses/LICENSE-spring-context-5.3.27.txt b/install-dist/release-docs/licenses/LICENSE-spring-context-5.3.27.txt index d645695673..a0299cad4d 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-context-5.3.27.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-context-5.3.27.txt @@ -1,7 +1,6 @@ - Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -193,10 +192,98 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +======================================================================= + +SPRING FRAMEWORK 5.3.27 SUBCOMPONENTS: + +Spring Framework 5.3.27 includes a number of subcomponents +with separate copyright notices and license terms. The product that +includes this file does not necessarily use all the open source +subcomponents referred to below. Your use of the source +code for these subcomponents is subject to the terms and +conditions of the following licenses. + + +>>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): + +Copyright (c) 2000-2011 INRIA, France Telecom +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +Copyright (c) 1999-2009, OW2 Consortium + + +>>> CGLIB 3.3 (cglib:cglib:3.3): + +Per the LICENSE file in the CGLIB JAR distribution downloaded from +https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, +CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which +is included above. + + +>>> Objenesis 3.2 (org.objenesis:objenesis:3.2): + +Per the LICENSE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html, Objenesis 3.2 is licensed under the +Apache License, version 2.0, the text of which is included above. + +Per the NOTICE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html and corresponding to section 4d of the +Apache License, Version 2.0, in this case for Objenesis: + +Objenesis +Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita + + +=============================================================================== + +To the extent any open source components are licensed under the EPL and/or +other similar licenses that require the source code and/or modifications to +source code to be made available (as would be noted above), you may obtain a +copy of the source code corresponding to the binaries for such open source +components and modifications thereto, if any, (the "Source Files"), by +downloading the Source Files from https://spring.io/projects, Pivotal's website +at https://network.pivotal.io/open-source, or by sending a request, with your +name and address to: Pivotal Software, Inc., 875 Howard Street, 5th floor, San +Francisco, CA 94103, Attention: General Counsel. All such requests should +clearly specify: OPEN SOURCE FILES REQUEST, Attention General Counsel. Pivotal +can mail a copy of the Source Files to you on a CD or equivalent physical +medium. + +This offer to obtain a copy of the Source Files is valid for three years from +the date you acquired this Software product. Alternatively, the Source Files +may accompany the Software. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-aop-5.3.20.txt b/install-dist/release-docs/licenses/LICENSE-spring-context-support-5.3.27.txt similarity index 99% rename from install-dist/release-docs/licenses/LICENSE-spring-aop-5.3.20.txt rename to install-dist/release-docs/licenses/LICENSE-spring-context-support-5.3.27.txt index 0a077a3135..a0299cad4d 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-aop-5.3.20.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-context-support-5.3.27.txt @@ -202,9 +202,9 @@ ======================================================================= -SPRING FRAMEWORK 5.3.20 SUBCOMPONENTS: +SPRING FRAMEWORK 5.3.27 SUBCOMPONENTS: -Spring Framework 5.3.20 includes a number of subcomponents +Spring Framework 5.3.27 includes a number of subcomponents with separate copyright notices and license terms. The product that includes this file does not necessarily use all the open source subcomponents referred to below. Your use of the source diff --git a/install-dist/release-docs/licenses/LICENSE-spring-core-5.3.20.txt b/install-dist/release-docs/licenses/LICENSE-spring-core-5.3.20.txt deleted file mode 100644 index 0a077a3135..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-core-5.3.20.txt +++ /dev/null @@ -1,289 +0,0 @@ - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -======================================================================= - -SPRING FRAMEWORK 5.3.20 SUBCOMPONENTS: - -Spring Framework 5.3.20 includes a number of subcomponents -with separate copyright notices and license terms. The product that -includes this file does not necessarily use all the open source -subcomponents referred to below. Your use of the source -code for these subcomponents is subject to the terms and -conditions of the following licenses. - - ->>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): - -Copyright (c) 2000-2011 INRIA, France Telecom -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holders nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -THE POSSIBILITY OF SUCH DAMAGE. - -Copyright (c) 1999-2009, OW2 Consortium - - ->>> CGLIB 3.3 (cglib:cglib:3.3): - -Per the LICENSE file in the CGLIB JAR distribution downloaded from -https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, -CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which -is included above. - - ->>> Objenesis 3.2 (org.objenesis:objenesis:3.2): - -Per the LICENSE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html, Objenesis 3.2 is licensed under the -Apache License, version 2.0, the text of which is included above. - -Per the NOTICE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html and corresponding to section 4d of the -Apache License, Version 2.0, in this case for Objenesis: - -Objenesis -Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita - - -=============================================================================== - -To the extent any open source components are licensed under the EPL and/or -other similar licenses that require the source code and/or modifications to -source code to be made available (as would be noted above), you may obtain a -copy of the source code corresponding to the binaries for such open source -components and modifications thereto, if any, (the "Source Files"), by -downloading the Source Files from https://spring.io/projects, Pivotal's website -at https://network.pivotal.io/open-source, or by sending a request, with your -name and address to: Pivotal Software, Inc., 875 Howard Street, 5th floor, San -Francisco, CA 94103, Attention: General Counsel. All such requests should -clearly specify: OPEN SOURCE FILES REQUEST, Attention General Counsel. Pivotal -can mail a copy of the Source Files to you on a CD or equivalent physical -medium. - -This offer to obtain a copy of the Source Files is valid for three years from -the date you acquired this Software product. Alternatively, the Source Files -may accompany the Software. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-core-5.3.27.txt b/install-dist/release-docs/licenses/LICENSE-spring-core-5.3.27.txt index d645695673..a0299cad4d 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-core-5.3.27.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-core-5.3.27.txt @@ -1,7 +1,6 @@ - Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -193,10 +192,98 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +======================================================================= + +SPRING FRAMEWORK 5.3.27 SUBCOMPONENTS: + +Spring Framework 5.3.27 includes a number of subcomponents +with separate copyright notices and license terms. The product that +includes this file does not necessarily use all the open source +subcomponents referred to below. Your use of the source +code for these subcomponents is subject to the terms and +conditions of the following licenses. + + +>>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): + +Copyright (c) 2000-2011 INRIA, France Telecom +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +Copyright (c) 1999-2009, OW2 Consortium + + +>>> CGLIB 3.3 (cglib:cglib:3.3): + +Per the LICENSE file in the CGLIB JAR distribution downloaded from +https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, +CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which +is included above. + + +>>> Objenesis 3.2 (org.objenesis:objenesis:3.2): + +Per the LICENSE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html, Objenesis 3.2 is licensed under the +Apache License, version 2.0, the text of which is included above. + +Per the NOTICE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html and corresponding to section 4d of the +Apache License, Version 2.0, in this case for Objenesis: + +Objenesis +Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita + + +=============================================================================== + +To the extent any open source components are licensed under the EPL and/or +other similar licenses that require the source code and/or modifications to +source code to be made available (as would be noted above), you may obtain a +copy of the source code corresponding to the binaries for such open source +components and modifications thereto, if any, (the "Source Files"), by +downloading the Source Files from https://spring.io/projects, Pivotal's website +at https://network.pivotal.io/open-source, or by sending a request, with your +name and address to: Pivotal Software, Inc., 875 Howard Street, 5th floor, San +Francisco, CA 94103, Attention: General Counsel. All such requests should +clearly specify: OPEN SOURCE FILES REQUEST, Attention General Counsel. Pivotal +can mail a copy of the Source Files to you on a CD or equivalent physical +medium. + +This offer to obtain a copy of the Source Files is valid for three years from +the date you acquired this Software product. Alternatively, the Source Files +may accompany the Software. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-expression-5.3.20.txt b/install-dist/release-docs/licenses/LICENSE-spring-expression-5.3.20.txt deleted file mode 100644 index 0a077a3135..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-expression-5.3.20.txt +++ /dev/null @@ -1,289 +0,0 @@ - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -======================================================================= - -SPRING FRAMEWORK 5.3.20 SUBCOMPONENTS: - -Spring Framework 5.3.20 includes a number of subcomponents -with separate copyright notices and license terms. The product that -includes this file does not necessarily use all the open source -subcomponents referred to below. Your use of the source -code for these subcomponents is subject to the terms and -conditions of the following licenses. - - ->>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): - -Copyright (c) 2000-2011 INRIA, France Telecom -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holders nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -THE POSSIBILITY OF SUCH DAMAGE. - -Copyright (c) 1999-2009, OW2 Consortium - - ->>> CGLIB 3.3 (cglib:cglib:3.3): - -Per the LICENSE file in the CGLIB JAR distribution downloaded from -https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, -CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which -is included above. - - ->>> Objenesis 3.2 (org.objenesis:objenesis:3.2): - -Per the LICENSE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html, Objenesis 3.2 is licensed under the -Apache License, version 2.0, the text of which is included above. - -Per the NOTICE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html and corresponding to section 4d of the -Apache License, Version 2.0, in this case for Objenesis: - -Objenesis -Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita - - -=============================================================================== - -To the extent any open source components are licensed under the EPL and/or -other similar licenses that require the source code and/or modifications to -source code to be made available (as would be noted above), you may obtain a -copy of the source code corresponding to the binaries for such open source -components and modifications thereto, if any, (the "Source Files"), by -downloading the Source Files from https://spring.io/projects, Pivotal's website -at https://network.pivotal.io/open-source, or by sending a request, with your -name and address to: Pivotal Software, Inc., 875 Howard Street, 5th floor, San -Francisco, CA 94103, Attention: General Counsel. All such requests should -clearly specify: OPEN SOURCE FILES REQUEST, Attention General Counsel. Pivotal -can mail a copy of the Source Files to you on a CD or equivalent physical -medium. - -This offer to obtain a copy of the Source Files is valid for three years from -the date you acquired this Software product. Alternatively, the Source Files -may accompany the Software. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-expression-5.3.27.txt b/install-dist/release-docs/licenses/LICENSE-spring-expression-5.3.27.txt index d645695673..a0299cad4d 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-expression-5.3.27.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-expression-5.3.27.txt @@ -1,7 +1,6 @@ - Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -193,10 +192,98 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +======================================================================= + +SPRING FRAMEWORK 5.3.27 SUBCOMPONENTS: + +Spring Framework 5.3.27 includes a number of subcomponents +with separate copyright notices and license terms. The product that +includes this file does not necessarily use all the open source +subcomponents referred to below. Your use of the source +code for these subcomponents is subject to the terms and +conditions of the following licenses. + + +>>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): + +Copyright (c) 2000-2011 INRIA, France Telecom +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +Copyright (c) 1999-2009, OW2 Consortium + + +>>> CGLIB 3.3 (cglib:cglib:3.3): + +Per the LICENSE file in the CGLIB JAR distribution downloaded from +https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, +CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which +is included above. + + +>>> Objenesis 3.2 (org.objenesis:objenesis:3.2): + +Per the LICENSE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html, Objenesis 3.2 is licensed under the +Apache License, version 2.0, the text of which is included above. + +Per the NOTICE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html and corresponding to section 4d of the +Apache License, Version 2.0, in this case for Objenesis: + +Objenesis +Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita + + +=============================================================================== + +To the extent any open source components are licensed under the EPL and/or +other similar licenses that require the source code and/or modifications to +source code to be made available (as would be noted above), you may obtain a +copy of the source code corresponding to the binaries for such open source +components and modifications thereto, if any, (the "Source Files"), by +downloading the Source Files from https://spring.io/projects, Pivotal's website +at https://network.pivotal.io/open-source, or by sending a request, with your +name and address to: Pivotal Software, Inc., 875 Howard Street, 5th floor, San +Francisco, CA 94103, Attention: General Counsel. All such requests should +clearly specify: OPEN SOURCE FILES REQUEST, Attention General Counsel. Pivotal +can mail a copy of the Source Files to you on a CD or equivalent physical +medium. + +This offer to obtain a copy of the Source Files is valid for three years from +the date you acquired this Software product. Alternatively, the Source Files +may accompany the Software. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-jcl-5.3.20.txt b/install-dist/release-docs/licenses/LICENSE-spring-jcl-5.3.20.txt deleted file mode 100644 index 0a077a3135..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-jcl-5.3.20.txt +++ /dev/null @@ -1,289 +0,0 @@ - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -======================================================================= - -SPRING FRAMEWORK 5.3.20 SUBCOMPONENTS: - -Spring Framework 5.3.20 includes a number of subcomponents -with separate copyright notices and license terms. The product that -includes this file does not necessarily use all the open source -subcomponents referred to below. Your use of the source -code for these subcomponents is subject to the terms and -conditions of the following licenses. - - ->>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): - -Copyright (c) 2000-2011 INRIA, France Telecom -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holders nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -THE POSSIBILITY OF SUCH DAMAGE. - -Copyright (c) 1999-2009, OW2 Consortium - - ->>> CGLIB 3.3 (cglib:cglib:3.3): - -Per the LICENSE file in the CGLIB JAR distribution downloaded from -https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, -CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which -is included above. - - ->>> Objenesis 3.2 (org.objenesis:objenesis:3.2): - -Per the LICENSE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html, Objenesis 3.2 is licensed under the -Apache License, version 2.0, the text of which is included above. - -Per the NOTICE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html and corresponding to section 4d of the -Apache License, Version 2.0, in this case for Objenesis: - -Objenesis -Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita - - -=============================================================================== - -To the extent any open source components are licensed under the EPL and/or -other similar licenses that require the source code and/or modifications to -source code to be made available (as would be noted above), you may obtain a -copy of the source code corresponding to the binaries for such open source -components and modifications thereto, if any, (the "Source Files"), by -downloading the Source Files from https://spring.io/projects, Pivotal's website -at https://network.pivotal.io/open-source, or by sending a request, with your -name and address to: Pivotal Software, Inc., 875 Howard Street, 5th floor, San -Francisco, CA 94103, Attention: General Counsel. All such requests should -clearly specify: OPEN SOURCE FILES REQUEST, Attention General Counsel. Pivotal -can mail a copy of the Source Files to you on a CD or equivalent physical -medium. - -This offer to obtain a copy of the Source Files is valid for three years from -the date you acquired this Software product. Alternatively, the Source Files -may accompany the Software. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-jcl-5.3.27.txt b/install-dist/release-docs/licenses/LICENSE-spring-jcl-5.3.27.txt index d645695673..a0299cad4d 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-jcl-5.3.27.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-jcl-5.3.27.txt @@ -1,7 +1,6 @@ - Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -193,10 +192,98 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +======================================================================= + +SPRING FRAMEWORK 5.3.27 SUBCOMPONENTS: + +Spring Framework 5.3.27 includes a number of subcomponents +with separate copyright notices and license terms. The product that +includes this file does not necessarily use all the open source +subcomponents referred to below. Your use of the source +code for these subcomponents is subject to the terms and +conditions of the following licenses. + + +>>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): + +Copyright (c) 2000-2011 INRIA, France Telecom +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +Copyright (c) 1999-2009, OW2 Consortium + + +>>> CGLIB 3.3 (cglib:cglib:3.3): + +Per the LICENSE file in the CGLIB JAR distribution downloaded from +https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, +CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which +is included above. + + +>>> Objenesis 3.2 (org.objenesis:objenesis:3.2): + +Per the LICENSE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html, Objenesis 3.2 is licensed under the +Apache License, version 2.0, the text of which is included above. + +Per the NOTICE file in the Objenesis ZIP distribution downloaded from +http://objenesis.org/download.html and corresponding to section 4d of the +Apache License, Version 2.0, in this case for Objenesis: + +Objenesis +Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita + + +=============================================================================== + +To the extent any open source components are licensed under the EPL and/or +other similar licenses that require the source code and/or modifications to +source code to be made available (as would be noted above), you may obtain a +copy of the source code corresponding to the binaries for such open source +components and modifications thereto, if any, (the "Source Files"), by +downloading the Source Files from https://spring.io/projects, Pivotal's website +at https://network.pivotal.io/open-source, or by sending a request, with your +name and address to: Pivotal Software, Inc., 875 Howard Street, 5th floor, San +Francisco, CA 94103, Attention: General Counsel. All such requests should +clearly specify: OPEN SOURCE FILES REQUEST, Attention General Counsel. Pivotal +can mail a copy of the Source Files to you on a CD or equivalent physical +medium. + +This offer to obtain a copy of the Source Files is valid for three years from +the date you acquired this Software product. Alternatively, the Source Files +may accompany the Software. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-web-5.3.20.txt b/install-dist/release-docs/licenses/LICENSE-spring-web-5.3.20.txt deleted file mode 100644 index 0a077a3135..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-web-5.3.20.txt +++ /dev/null @@ -1,289 +0,0 @@ - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -======================================================================= - -SPRING FRAMEWORK 5.3.20 SUBCOMPONENTS: - -Spring Framework 5.3.20 includes a number of subcomponents -with separate copyright notices and license terms. The product that -includes this file does not necessarily use all the open source -subcomponents referred to below. Your use of the source -code for these subcomponents is subject to the terms and -conditions of the following licenses. - - ->>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): - -Copyright (c) 2000-2011 INRIA, France Telecom -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holders nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -THE POSSIBILITY OF SUCH DAMAGE. - -Copyright (c) 1999-2009, OW2 Consortium - - ->>> CGLIB 3.3 (cglib:cglib:3.3): - -Per the LICENSE file in the CGLIB JAR distribution downloaded from -https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, -CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which -is included above. - - ->>> Objenesis 3.2 (org.objenesis:objenesis:3.2): - -Per the LICENSE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html, Objenesis 3.2 is licensed under the -Apache License, version 2.0, the text of which is included above. - -Per the NOTICE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html and corresponding to section 4d of the -Apache License, Version 2.0, in this case for Objenesis: - -Objenesis -Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita - - -=============================================================================== - -To the extent any open source components are licensed under the EPL and/or -other similar licenses that require the source code and/or modifications to -source code to be made available (as would be noted above), you may obtain a -copy of the source code corresponding to the binaries for such open source -components and modifications thereto, if any, (the "Source Files"), by -downloading the Source Files from https://spring.io/projects, Pivotal's website -at https://network.pivotal.io/open-source, or by sending a request, with your -name and address to: Pivotal Software, Inc., 875 Howard Street, 5th floor, San -Francisco, CA 94103, Attention: General Counsel. All such requests should -clearly specify: OPEN SOURCE FILES REQUEST, Attention General Counsel. Pivotal -can mail a copy of the Source Files to you on a CD or equivalent physical -medium. - -This offer to obtain a copy of the Source Files is valid for three years from -the date you acquired this Software product. Alternatively, the Source Files -may accompany the Software. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-beans-5.3.20.txt b/install-dist/release-docs/licenses/LICENSE-spring-web-5.3.27.txt similarity index 99% rename from install-dist/release-docs/licenses/LICENSE-spring-beans-5.3.20.txt rename to install-dist/release-docs/licenses/LICENSE-spring-web-5.3.27.txt index 0a077a3135..a0299cad4d 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-beans-5.3.20.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-web-5.3.27.txt @@ -202,9 +202,9 @@ ======================================================================= -SPRING FRAMEWORK 5.3.20 SUBCOMPONENTS: +SPRING FRAMEWORK 5.3.27 SUBCOMPONENTS: -Spring Framework 5.3.20 includes a number of subcomponents +Spring Framework 5.3.27 includes a number of subcomponents with separate copyright notices and license terms. The product that includes this file does not necessarily use all the open source subcomponents referred to below. Your use of the source diff --git a/install-dist/release-docs/licenses/LICENSE-spring-webmvc-5.3.20.txt b/install-dist/release-docs/licenses/LICENSE-spring-webmvc-5.3.20.txt deleted file mode 100644 index 0a077a3135..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-spring-webmvc-5.3.20.txt +++ /dev/null @@ -1,289 +0,0 @@ - Apache License - Version 2.0, January 2004 - https://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 - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -======================================================================= - -SPRING FRAMEWORK 5.3.20 SUBCOMPONENTS: - -Spring Framework 5.3.20 includes a number of subcomponents -with separate copyright notices and license terms. The product that -includes this file does not necessarily use all the open source -subcomponents referred to below. Your use of the source -code for these subcomponents is subject to the terms and -conditions of the following licenses. - - ->>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1): - -Copyright (c) 2000-2011 INRIA, France Telecom -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holders nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -THE POSSIBILITY OF SUCH DAMAGE. - -Copyright (c) 1999-2009, OW2 Consortium - - ->>> CGLIB 3.3 (cglib:cglib:3.3): - -Per the LICENSE file in the CGLIB JAR distribution downloaded from -https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar, -CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which -is included above. - - ->>> Objenesis 3.2 (org.objenesis:objenesis:3.2): - -Per the LICENSE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html, Objenesis 3.2 is licensed under the -Apache License, version 2.0, the text of which is included above. - -Per the NOTICE file in the Objenesis ZIP distribution downloaded from -http://objenesis.org/download.html and corresponding to section 4d of the -Apache License, Version 2.0, in this case for Objenesis: - -Objenesis -Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita - - -=============================================================================== - -To the extent any open source components are licensed under the EPL and/or -other similar licenses that require the source code and/or modifications to -source code to be made available (as would be noted above), you may obtain a -copy of the source code corresponding to the binaries for such open source -components and modifications thereto, if any, (the "Source Files"), by -downloading the Source Files from https://spring.io/projects, Pivotal's website -at https://network.pivotal.io/open-source, or by sending a request, with your -name and address to: Pivotal Software, Inc., 875 Howard Street, 5th floor, San -Francisco, CA 94103, Attention: General Counsel. All such requests should -clearly specify: OPEN SOURCE FILES REQUEST, Attention General Counsel. Pivotal -can mail a copy of the Source Files to you on a CD or equivalent physical -medium. - -This offer to obtain a copy of the Source Files is valid for three years from -the date you acquired this Software product. Alternatively, the Source Files -may accompany the Software. diff --git a/install-dist/release-docs/licenses/LICENSE-spring-context-5.3.20.txt b/install-dist/release-docs/licenses/LICENSE-spring-webmvc-5.3.27.txt similarity index 99% rename from install-dist/release-docs/licenses/LICENSE-spring-context-5.3.20.txt rename to install-dist/release-docs/licenses/LICENSE-spring-webmvc-5.3.27.txt index 0a077a3135..a0299cad4d 100644 --- a/install-dist/release-docs/licenses/LICENSE-spring-context-5.3.20.txt +++ b/install-dist/release-docs/licenses/LICENSE-spring-webmvc-5.3.27.txt @@ -202,9 +202,9 @@ ======================================================================= -SPRING FRAMEWORK 5.3.20 SUBCOMPONENTS: +SPRING FRAMEWORK 5.3.27 SUBCOMPONENTS: -Spring Framework 5.3.20 includes a number of subcomponents +Spring Framework 5.3.27 includes a number of subcomponents with separate copyright notices and license terms. The product that includes this file does not necessarily use all the open source subcomponents referred to below. Your use of the source diff --git a/install-dist/release-docs/licenses/LICENSE-tag-expressions-6.1.2.txt b/install-dist/release-docs/licenses/LICENSE-tag-expressions-6.1.2.txt new file mode 100644 index 0000000000..a5e1512d0f --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-tag-expressions-6.1.2.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Cucumber Ltd and contributors + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-testng-xml-formatter-0.3.1.txt b/install-dist/release-docs/licenses/LICENSE-testng-xml-formatter-0.3.1.txt new file mode 100644 index 0000000000..1e1df39fdc --- /dev/null +++ b/install-dist/release-docs/licenses/LICENSE-testng-xml-formatter-0.3.1.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Cucumber + +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. diff --git a/install-dist/release-docs/licenses/LICENSE-tomcat-embed-el-9.0.63.txt b/install-dist/release-docs/licenses/LICENSE-tomcat-embed-el-9.0.63.txt deleted file mode 100644 index d645695673..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-tomcat-embed-el-9.0.63.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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. diff --git a/install-dist/release-docs/licenses/LICENSE-jackson-dataformat-yaml-2.11.2.txt b/install-dist/release-docs/licenses/LICENSE-tomcat-embed-el-9.0.75.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-jackson-dataformat-yaml-2.11.2.txt rename to install-dist/release-docs/licenses/LICENSE-tomcat-embed-el-9.0.75.txt diff --git a/install-dist/release-docs/licenses/LICENSE-websocket-api-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-websocket-api-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-websocket-api-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-websocket-api-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-websocket-client-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-websocket-client-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-websocket-client-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-websocket-client-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-websocket-common-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-websocket-common-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-websocket-common-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-websocket-common-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-websocket-server-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-websocket-server-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-websocket-server-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-websocket-server-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-websocket-servlet-9.4.46.v20220331.txt b/install-dist/release-docs/licenses/LICENSE-websocket-servlet-9.4.51.v20230217.txt similarity index 100% rename from install-dist/release-docs/licenses/LICENSE-websocket-servlet-9.4.46.v20220331.txt rename to install-dist/release-docs/licenses/LICENSE-websocket-servlet-9.4.51.v20230217.txt diff --git a/install-dist/release-docs/licenses/LICENSE-xmlpull-1.1.3.1.txt b/install-dist/release-docs/licenses/LICENSE-xmlpull-1.1.3.1.txt deleted file mode 100644 index d412a90f28..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-xmlpull-1.1.3.1.txt +++ /dev/null @@ -1 +0,0 @@ -http://www.xmlpull.org/v1/download/unpacked/LICENSE.txt diff --git a/install-dist/release-docs/licenses/LICENSE-xpp3_min-1.1.4c.txt b/install-dist/release-docs/licenses/LICENSE-xpp3_min-1.1.4c.txt deleted file mode 100644 index 3a5baed068..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-xpp3_min-1.1.4c.txt +++ /dev/null @@ -1 +0,0 @@ -http://creativecommons.org/licenses/publicdomain diff --git a/install-dist/release-docs/licenses/LICENSE-xstream-1.4.10.txt b/install-dist/release-docs/licenses/LICENSE-xstream-1.4.10.txt deleted file mode 100644 index 01f3f3133e..0000000000 --- a/install-dist/release-docs/licenses/LICENSE-xstream-1.4.10.txt +++ /dev/null @@ -1,28 +0,0 @@ -(BSD Style License) - -Copyright (c) 2003-2006, Joe Walnes -Copyright (c) 2006-2020, XStream Committers -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list of -conditions and the following disclaimer. Redistributions in binary form must reproduce -the above copyright notice, this list of conditions and the following disclaimer in -the documentation and/or other materials provided with the distribution. - -Neither the name of XStream nor the names of its contributors may be used to endorse -or promote products derived from this software without specific prior written -permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT -SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR -BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY -WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -DAMAGE. diff --git a/install-dist/scripts/apache-release.sh b/install-dist/scripts/apache-release.sh index 0eb721ae8b..fc82f4e6a9 100755 --- a/install-dist/scripts/apache-release.sh +++ b/install-dist/scripts/apache-release.sh @@ -89,7 +89,7 @@ cd ${SVN_DIR} || exit ##### 4.3 check status & add files svn status -svn add --parents "${RELEASE_VERSION}"/apache-${REPO}-* +svn add --parents "${RELEASE_VERSION}"/apache-${REPO}-"${RELEASE_VERSION}"* ## check status again svn status diff --git a/install-dist/scripts/dependency/check_dependencies.sh b/install-dist/scripts/dependency/check_dependencies.sh index 0fadfa3132..47a7ff5ae1 100644 --- a/install-dist/scripts/dependency/check_dependencies.sh +++ b/install-dist/scripts/dependency/check_dependencies.sh @@ -16,20 +16,12 @@ # limitations under the License. # -BASE_PATH=$( - cd "$(dirname "$0")" || exit 1 - pwd -) +set -euo pipefail -# check whether there are new third-party dependencies by diff command, -# diff generated 'current-dependencies.txt' file with 'known-dependencies.txt' file. -diff -w -B -U0 <(sort <"${BASE_PATH}"/known-dependencies.txt) \ - <(sort <"${BASE_PATH}"/current-dependencies.txt) >${BASE_PATH}/result.txt +BASE_PATH=$(cd "$(dirname "$0")" && pwd) +KNOWN_FILE=${1:-$BASE_PATH/known-dependencies.txt} +CURRENT_FILE=${2:-$BASE_PATH/current-dependencies.txt} -# if has new third-party,the Action will fail and print diff -if [ -s "${BASE_PATH}"/result.txt ]; then - cat "${BASE_PATH}"/result.txt - exit 1 -else - echo 'All third dependencies is known!' -fi +# Missing/malformed inputs and scan failures must not produce a green diff. +python3 "$BASE_PATH/dependency_inventory.py" check \ + --known "$KNOWN_FILE" --current "$CURRENT_FILE" diff --git a/install-dist/scripts/dependency/dependency_inventory.py b/install-dist/scripts/dependency/dependency_inventory.py new file mode 100644 index 0000000000..f80728487a --- /dev/null +++ b/install-dist/scripts/dependency/dependency_inventory.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Inventory the union of Maven runtime dependencies and actually shipped jars. + +Keep the historical filename exclusions for project artifacts. Boot repackaging +can inject jars or include provided dependencies absent from Maven's runtime +scope, so collect flat and nested distribution libraries as well. Comparison +remains strict: removals and additions both require updating the approved list. +""" + +import argparse +import difflib +import io +import os +from pathlib import Path +import re +import sys +import tempfile +import zipfile + + +class InventoryFailure(Exception): + pass + + +def third_party(name): + # Preserve the former egrep -v '^hg|hugegraph|hubble' inventory convention. + return not re.search(r'^hg|hugegraph|hubble', name) + + +def validate_name(name): + if not re.fullmatch(r'[A-Za-z0-9_.+-]+\.jar', name): + raise InventoryFailure(f'Invalid dependency jar filename: {name}') + + +def scan_shipped_jar(source, name, origin, result): + validate_name(name) + try: + with zipfile.ZipFile(source) as archive: + if third_party(name): + result.add(name) + for entry in archive.namelist(): + if entry.startswith(('BOOT-INF/lib/', 'WEB-INF/lib/')) and entry.endswith('.jar'): + scan_shipped_jar(io.BytesIO(archive.read(entry)), Path(entry).name, + origin + '!/' + entry, result) + except (OSError, zipfile.BadZipFile, RuntimeError) as error: + raise InventoryFailure(f'Cannot inspect shipped jar {origin}: {error}') from error + + +def collect(runtime, root, revision): + if not re.fullmatch(r'[A-Za-z0-9_.+-]+', revision): + raise InventoryFailure(f'Invalid distribution revision: {revision}') + if not runtime.is_dir(): + raise InventoryFailure(f'Missing Maven runtime dependency directory: {runtime}') + files = sorted(runtime.iterdir()) + if not files: + raise InventoryFailure(f'Empty Maven runtime dependency directory: {runtime}') + result = set() + for path in files: + validate_name(path.name) + try: + # Runtime copy has already resolved each artifact; validate its jar + # without treating nested project packaging as new Maven coordinates. + with zipfile.ZipFile(path): + pass + except (OSError, zipfile.BadZipFile) as error: + raise InventoryFailure(f'Cannot inspect resolved jar {path}: {error}') from error + if third_party(path.name): + result.add(path.name) + for kind in ('server', 'pd', 'store'): + distribution = root / ('hugegraph-' + kind) / f'apache-hugegraph-{kind}-{revision}' + library = distribution / 'lib' + jars = sorted(library.rglob('*.jar')) + if not library.is_dir() or not jars: + raise InventoryFailure(f'Missing packaged {kind} dependency jars: {library}') + for directory in ('ext', 'plugins'): + jars.extend(sorted((distribution / directory).rglob('*.jar'))) + for path in jars: + scan_shipped_jar(path, path.name, str(path), result) + if not result: + raise InventoryFailure('No third-party dependencies found') + return result + + +def read_inventory(path): + values = [] + for line in path.read_text().splitlines(): + name = line.strip() + if name: + validate_name(name) + values.append(name) + if not values: + raise InventoryFailure(f'Empty dependency inventory: {path}') + if len(values) != len(set(values)): + raise InventoryFailure(f'Duplicate dependency inventory entries: {path}') + return sorted(values) + + +def compare(known, current): + approved, observed = read_inventory(known), read_inventory(current) + difference = ''.join(difflib.unified_diff([name + '\n' for name in approved], + [name + '\n' for name in observed], + fromfile=str(known), tofile=str(current), n=0)) + if difference: + print(difference, end='') + return 1 + print('All third-party dependencies match the approved runtime and shipped inventory.') + return 0 + + +def write_inventory(output, names): + output.parent.mkdir(parents=True, exist_ok=True) + temporary = None + try: + with tempfile.NamedTemporaryFile(mode='w', dir=output.parent, delete=False) as handle: + temporary = Path(handle.name) + handle.write('\n'.join(sorted(names)) + '\n') + os.replace(temporary, output) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest='command', required=True) + producer = commands.add_parser('collect') + producer.add_argument('--runtime', type=Path, required=True) + producer.add_argument('--root', type=Path, required=True) + producer.add_argument('--revision', required=True) + producer.add_argument('--output', type=Path, required=True) + checker = commands.add_parser('check') + checker.add_argument('--known', type=Path, required=True) + checker.add_argument('--current', type=Path, required=True) + args = parser.parse_args() + try: + if args.command == 'check': + return compare(args.known, args.current) + names = collect(args.runtime, args.root, args.revision) + write_inventory(args.output, names) + print(f'Wrote {len(names)} runtime and shipped dependency filenames to {args.output}') + return 0 + except (InventoryFailure, OSError, UnicodeError) as error: + print(f'Dependency inventory FAILED: {error}', file=sys.stderr) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/install-dist/scripts/dependency/known-dependencies.txt b/install-dist/scripts/dependency/known-dependencies.txt index 29151532a6..17f97dc5be 100644 --- a/install-dist/scripts/dependency/known-dependencies.txt +++ b/install-dist/scripts/dependency/known-dependencies.txt @@ -1,41 +1,50 @@ +HdrHistogram-2.1.12.jar +LatencyUtils-2.0.3.jar +RoaringBitmap-0.9.38.jar accessors-smart-1.2.jar android-json-0.0.20131108.vaadin1.jar animal-sniffer-annotations-1.14.jar -animal-sniffer-annotations-1.18.jar -animal-sniffer-annotations-1.19.jar +animal-sniffer-annotations-1.17.jar +animal-sniffer-annotations-1.21.jar annotations-13.0.jar annotations-24.0.1.jar annotations-4.1.1.4.jar ansj_seg-5.1.6.jar +antlr4-runtime-4.9.1.jar aopalliance-repackaged-3.0.1.jar apiguardian-api-1.1.0.jar -arthas-agent-attach-3.6.4.jar -arthas-agent-attach-3.7.1.jar -arthas-packaging-3.6.4.jar -arthas-packaging-3.7.1.jar -asm-5.0.4.jar +apiguardian-api-1.1.2.jar +arthas-agent-attach-4.1.5.jar +arthas-agent.jar +arthas-boot.jar +arthas-client.jar +arthas-core.jar +arthas-packaging-4.1.5.jar +arthas-spy.jar asm-6.0.jar -asm-analysis-9.2.jar -asm-commons-9.2.jar -asm-tree-9.2.jar +asm-commons-9.4.jar +asm-tree-9.4.jar assertj-core-3.19.0.jar ast-9.0-9.0.20190305.jar audience-annotations-0.13.0.jar -automaton-1.11-8.jar auto-service-annotations-1.0.jar +automaton-1.11-8.jar +ayza-10.0.0.jar +ayza-for-netty-10.0.0.jar bolt-1.6.2.jar bolt-1.6.4.jar byte-buddy-1.10.20.jar -byte-buddy-1.10.5.jar +byte-buddy-1.17.6.jar byte-buddy-agent-1.10.20.jar -byte-buddy-agent-1.10.5.jar -byte-buddy-agent-1.11.6.jar +byte-buddy-agent-1.17.6.jar +byte-buddy-agent-1.18.3.jar caffeine-2.3.1.jar -checker-compat-qual-2.5.5.jar checker-qual-2.0.0.jar +checker-qual-2.5.2.jar checker-qual-3.12.0.jar checker-qual-3.33.0.jar checker-qual-3.5.0.jar +ci-environment-10.0.1.jar classgraph-4.8.162.jar commons-beanutils-1.9.4.jar commons-cli-1.5.0.jar @@ -45,7 +54,7 @@ commons-collections-3.2.2.jar commons-collections4-4.4.jar commons-compress-1.21.jar commons-configuration-1.10.jar -commons-configuration2-2.8.0.jar +commons-configuration2-2.10.1.jar commons-crypto-1.1.0.jar commons-io-2.12.0.jar commons-io-2.7.jar @@ -56,27 +65,35 @@ commons-lang3-3.13.0.jar commons-lang3-3.18.0.jar commons-logging-1.1.1.jar commons-logging-1.2.jar -commons-pool2-2.0.jar commons-text-1.10.0.jar -commons-text-1.9.jar +commons-text-1.11.0.jar +cucumber-core-7.21.1.jar +cucumber-expressions-18.0.1.jar +cucumber-gherkin-7.21.1.jar +cucumber-gherkin-messages-7.21.1.jar +cucumber-guice-7.21.1.jar +cucumber-java-7.21.1.jar +cucumber-junit-7.21.1.jar +cucumber-plugin-7.21.1.jar cypher-gremlin-extensions-1.0.4.jar +datatable-7.21.1.jar disruptor-3.3.7.jar disruptor-3.4.1.jar +docstring-7.21.1.jar eclipse-collections-10.4.0.jar eclipse-collections-11.1.0.jar eclipse-collections-api-10.4.0.jar eclipse-collections-api-11.1.0.jar error_prone_annotations-2.1.3.jar -error_prone_annotations-2.10.0.jar +error_prone_annotations-2.11.0.jar error_prone_annotations-2.18.0.jar error_prone_annotations-2.3.4.jar -error_prone_annotations-2.4.0.jar error_prone_annotations-2.48.0.jar exp4j-0.4.8.jar expressions-9.0-9.0.20190305.jar failsafe-2.4.1.jar failureaccess-1.0.1.jar -fastjson-1.2.83.jar +fastjson-1.2.84.jar fastparse_2.12-2.0.4.jar fastutil-8.1.0.jar fastutil-8.5.9.jar @@ -84,67 +101,57 @@ findbugs-annotations-1.3.9-1.jar front-end-9.0-9.0.20190305.jar fury-core-0.9.0.jar generex-1.0.2.jar -gremlin-console-3.5.1.jar -gremlin-core-3.5.1.jar -gremlin-driver-3.5.1.jar -gremlin-groovy-3.5.1.jar -gremlin-groovy-test-3.2.11.jar -gremlin-server-3.5.1.jar -gremlin-shaded-3.5.1.jar -gremlin-test-3.5.1.jar +gherkin-31.0.0.jar +gremlin-console-3.8.1.jar +gremlin-core-3.8.1.jar +gremlin-driver-3.8.1.jar +gremlin-groovy-3.8.1.jar +gremlin-language-3.8.1.jar +gremlin-server-3.8.1.jar +gremlin-shaded-3.8.1.jar +gremlin-test-3.8.1.jar +gremlin-util-3.8.1.jar grizzly-framework-3.0.1.jar grizzly-http-3.0.1.jar grizzly-http-server-3.0.1.jar grizzly-http-servlet-3.0.1.jar -groovy-2.5.14-indy.jar -groovy-cli-picocli-2.5.14.jar -groovy-console-2.5.14.jar -groovy-groovysh-2.5.14-indy.jar -groovy-json-2.5.14-indy.jar -groovy-jsr223-2.5.14-indy.jar -groovy-swing-2.5.14.jar -groovy-templates-2.5.14.jar -groovy-xml-2.5.14.jar -grpc-api-1.28.1.jar -grpc-api-1.39.0.jar -grpc-api-1.47.0.jar -grpc-context-1.28.1.jar -grpc-context-1.39.0.jar -grpc-context-1.47.0.jar -grpc-core-1.28.1.jar -grpc-core-1.39.0.jar -grpc-core-1.47.0.jar -grpc-grpclb-1.39.0.jar -grpc-netty-1.39.0.jar -grpc-netty-1.47.0.jar -grpc-netty-shaded-1.28.0.jar -grpc-netty-shaded-1.39.0.jar -grpc-netty-shaded-1.47.0.jar -grpc-protobuf-1.28.0.jar -grpc-protobuf-1.39.0.jar -grpc-protobuf-lite-1.28.0.jar -grpc-protobuf-lite-1.39.0.jar -grpc-services-1.39.0.jar +groovy-4.0.25.jar +groovy-cli-picocli-4.0.25.jar +groovy-console-4.0.25.jar +groovy-groovysh-4.0.25.jar +groovy-json-4.0.25.jar +groovy-jsr223-4.0.25.jar +groovy-swing-4.0.25.jar +groovy-templates-4.0.25.jar +groovy-xml-4.0.25.jar +grpc-api-1.55.3.jar +grpc-context-1.55.3.jar +grpc-core-1.55.3.jar +grpc-grpclb-1.55.3.jar +grpc-netty-1.55.3.jar +grpc-netty-shaded-1.55.3.jar +grpc-protobuf-1.55.3.jar +grpc-protobuf-lite-1.55.3.jar +grpc-services-1.55.3.jar grpc-spring-boot-starter-4.5.5.jar -grpc-stub-1.28.0.jar -grpc-stub-1.39.0.jar -grpc-stub-1.47.0.jar -gson-2.8.6.jar +grpc-stub-1.55.3.jar gson-2.8.9.jar +gson-2.9.0.jar guava-25.1-jre.jar +guava-27.1-jre.jar guava-30.0-jre.jar -guava-30.1-android.jar -guava-31.0.1-android.jar +guava-31.1-android.jar +guava-31.1-jre.jar guava-32.0.1-android.jar hamcrest-2.2.jar +hamcrest-3.0.jar hamcrest-core-1.3.jar -hanlp-portable-1.5.0.jar hanlp-portable-1.8.3.jar hbase-client-2.6.5.jar hbase-common-2.6.5.jar hbase-endpoint-2.6.5.jar -hbase-hadoop2-compat-2.6.5.jar hbase-hadoop-compat-2.6.5.jar +hbase-hadoop2-compat-2.6.5.jar hbase-logging-2.6.5.jar hbase-metrics-2.6.5.jar hbase-metrics-api-2.6.5.jar @@ -157,48 +164,34 @@ hbase-shaded-netty-4.1.13.jar hbase-shaded-protobuf-4.1.13.jar hbase-unsafe-4.1.13.jar hbase-zookeeper-2.6.5.jar -HdrHistogram-2.1.12.jar hessian-3.3.6.jar hessian-3.3.7.jar hk2-api-3.0.1.jar hk2-locator-3.0.1.jar hk2-utils-3.0.1.jar hppc-0.7.1.jar +html-formatter-21.9.0.jar htrace-core4-4.1.0-incubating.jar httpclient-4.5.13.jar httpcore-4.4.13.jar ikanalyzer-2012_u6.jar -ivy-2.4.0.jar +ivy-2.6.0.jar j2objc-annotations-1.1.jar j2objc-annotations-1.3.jar j2objc-annotations-2.8.jar -jackson-annotations-2.13.0.jar -jackson-annotations-2.14.0-rc1.jar jackson-annotations-2.15.2.jar -jackson-core-2.13.0.jar -jackson-core-2.13.2.jar -jackson-core-2.14.0-rc1.jar jackson-core-2.15.2.jar -jackson-databind-2.12.6.1.jar -jackson-databind-2.13.0.jar -jackson-databind-2.13.2.jar -jackson-databind-2.14.0-rc1.jar jackson-databind-2.15.2.jar -jackson-dataformat-yaml-2.11.2.jar -jackson-dataformat-yaml-2.9.3.jar -jackson-datatype-jdk8-2.12.6.jar -jackson-datatype-jsr310-2.11.2.jar -jackson-datatype-jsr310-2.12.6.jar +jackson-dataformat-yaml-2.15.2.jar +jackson-datatype-jdk8-2.15.2.jar +jackson-datatype-jsr310-2.15.2.jar jackson-jakarta-rs-base-2.15.2.jar jackson-jakarta-rs-json-provider-2.15.2.jar -jackson-jaxrs-base-2.14.0-rc1.jar jackson-jaxrs-base-2.15.2.jar -jackson-jaxrs-json-provider-2.14.0-rc1.jar jackson-jaxrs-json-provider-2.15.2.jar jackson-module-jakarta-xmlbind-annotations-2.15.2.jar -jackson-module-jaxb-annotations-2.14.0-rc1.jar jackson-module-jaxb-annotations-2.15.2.jar -jackson-module-parameter-names-2.12.6.jar +jackson-module-parameter-names-2.15.2.jar jakarta.activation-2.0.0.jar jakarta.activation-2.0.1.jar jakarta.activation-api-1.2.2.jar @@ -214,31 +207,26 @@ jakarta.xml.bind-api-2.3.3.jar jakarta.xml.bind-api-3.0.0.jar jamm-0.3.2.jar jansi-2.4.0.jar -javapoet-1.8.0.jar +javaparser-core-3.26.3.jar +javapoet-1.13.0.jar javassist-3.21.0-GA.jar -javassist-3.24.0-GA.jar -javassist-3.25.0-GA.jar javassist-3.28.0-GA.jar javatuples-1.2.jar +javax-websocket-client-impl-9.4.51.v20230217.jar +javax-websocket-server-impl-9.4.51.v20230217.jar javax.activation-api-1.2.0.jar javax.annotation-api-1.3.2.jar javax.json-1.0.jar -javax-websocket-client-impl-9.4.46.v20220331.jar -javax-websocket-server-impl-9.4.46.v20220331.jar jaxb-api-2.3.1.jar jaxb-core-3.0.2.jar jaxb-impl-3.0.2.jar jbcrypt-0.4.jar -jcabi-log-0.14.jar -jcabi-manifests-1.1.jar jcip-annotations-1.0-1.jar jcl-over-slf4j-1.7.25.jar jcodings-1.0.58.jar jcommander-1.30.jar -jcseg-core-2.2.0.jar jcseg-core-2.6.2.jar jctools-core-2.1.1.jar -jedis-2.5.1.jar jersey-apache-connector-3.0.3.jar jersey-client-3.0.3.jar jersey-common-3.0.3.jar @@ -255,26 +243,23 @@ jersey-test-framework-core-3.0.3.jar jersey-test-framework-provider-grizzly2-3.0.3.jar jetcd-common-0.5.9.jar jetcd-core-0.5.9.jar -jetty-annotations-9.4.46.v20220331.jar -jetty-client-9.4.46.v20220331.jar -jetty-continuation-9.4.46.v20220331.jar -jetty-http-9.4.46.v20220331.jar -jetty-io-9.4.46.v20220331.jar -jetty-plus-9.4.46.v20220331.jar -jetty-security-9.4.46.v20220331.jar -jetty-server-9.4.46.v20220331.jar -jetty-servlet-9.4.46.v20220331.jar -jetty-servlets-9.4.46.v20220331.jar -jetty-util-9.4.46.v20220331.jar -jetty-util-ajax-9.4.46.v20220331.jar -jetty-webapp-9.4.46.v20220331.jar -jetty-xml-9.4.46.v20220331.jar +jetty-annotations-9.4.51.v20230217.jar +jetty-client-9.4.51.v20230217.jar +jetty-continuation-9.4.51.v20230217.jar +jetty-http-9.4.51.v20230217.jar +jetty-io-9.4.51.v20230217.jar +jetty-plus-9.4.51.v20230217.jar +jetty-security-9.4.51.v20230217.jar +jetty-server-9.4.51.v20230217.jar +jetty-servlet-9.4.51.v20230217.jar +jetty-servlets-9.4.51.v20230217.jar +jetty-util-9.4.51.v20230217.jar +jetty-util-ajax-9.4.51.v20230217.jar +jetty-webapp-9.4.51.v20230217.jar +jetty-xml-9.4.51.v20230217.jar jieba-analysis-1.0.2.jar -jjwt-api-0.11.2.jar jjwt-api-0.11.5.jar -jjwt-impl-0.11.2.jar jjwt-impl-0.11.5.jar -jjwt-jackson-0.11.2.jar jjwt-jackson-0.11.5.jar jline-2.14.6.jar jna-5.12.1.jar @@ -282,11 +267,10 @@ jna-5.5.0.jar jna-5.7.0.jar joda-time-2.10.8.jar joni-2.2.1.jar -jraft-core-1.3.11.jar -jraft-core-1.3.13.jar -jsonassert-1.5.0.jar +jraft-core-1.3.14.jar json-path-2.5.0.jar json-smart-2.3.jar +jsonassert-1.5.1.jar jsr305-3.0.1.jar jsr305-3.0.2.jar jul-to-slf4j-1.7.36.jar @@ -298,21 +282,22 @@ junit-jupiter-engine-5.7.2.jar junit-jupiter-params-5.7.2.jar junit-platform-commons-1.7.2.jar junit-platform-engine-1.7.2.jar +junit-xml-formatter-0.7.1.jar jvm-attach-api-1.5.jar -kerb-admin-2.0.0.jar -kerb-client-2.0.0.jar -kerb-common-2.0.0.jar -kerb-core-2.0.0.jar -kerb-crypto-2.0.0.jar -kerb-identity-2.0.0.jar -kerb-server-2.0.0.jar -kerb-simplekdc-2.0.0.jar -kerb-util-2.0.0.jar -kerby-asn1-2.0.0.jar -kerby-config-2.0.0.jar -kerby-pkix-2.0.0.jar -kerby-util-2.0.0.jar -kerby-xdr-2.0.0.jar +kerb-admin-2.0.1.jar +kerb-client-2.0.1.jar +kerb-common-2.0.1.jar +kerb-core-2.0.1.jar +kerb-crypto-2.0.1.jar +kerb-identity-2.0.1.jar +kerb-server-2.0.1.jar +kerb-simplekdc-2.0.1.jar +kerb-util-2.0.1.jar +kerby-asn1-2.0.1.jar +kerby-config-2.0.1.jar +kerby-pkix-2.0.1.jar +kerby-util-2.0.1.jar +kerby-xdr-2.0.1.jar kotlin-stdlib-1.6.20.jar kotlin-stdlib-common-1.5.31.jar kotlin-stdlib-jdk7-1.6.10.jar @@ -338,44 +323,36 @@ kubernetes-model-policy-5.6.0.jar kubernetes-model-rbac-5.6.0.jar kubernetes-model-scheduling-5.6.0.jar kubernetes-model-storageclass-5.6.0.jar -LatencyUtils-2.0.3.jar listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar -log4j-api-2.15.0.jar log4j-api-2.17.0.jar log4j-api-2.17.1.jar log4j-api-2.17.2.jar log4j-api-2.18.0.jar -log4j-core-2.15.0.jar log4j-core-2.17.0.jar log4j-core-2.17.1.jar log4j-core-2.17.2.jar log4j-core-2.18.0.jar log4j-jul-2.17.2.jar -log4j-slf4j-impl-2.15.0.jar log4j-slf4j-impl-2.17.0.jar log4j-slf4j-impl-2.17.1.jar log4j-slf4j-impl-2.18.0.jar logging-interceptor-3.12.12.jar logging-interceptor-4.10.0.jar lombok-1.18.24.jar +lombok-1.18.30.jar lookout-api-1.4.1.jar -lucene-analyzers-common-5.2.1.jar lucene-analyzers-common-8.11.2.jar -lucene-analyzers-smartcn-7.4.0.jar lucene-analyzers-smartcn-8.11.2.jar -lucene-backward-codecs-5.2.1.jar -lucene-core-7.4.0.jar lucene-core-8.11.2.jar -lucene-misc-5.2.1.jar lucene-queries-4.7.2.jar lucene-queries-5.2.1.jar lucene-queryparser-4.7.2.jar lucene-queryparser-5.2.1.jar lucene-sandbox-4.7.2.jar lucene-sandbox-5.2.1.jar -lucene-suggest-5.2.1.jar lz4-java-1.4.0.jar lz4-java-1.8.1.jar +messages-27.2.0.jar metrics-annotation-4.2.4.jar metrics-core-3.0.2.jar metrics-core-4.0.2.jar @@ -384,42 +361,56 @@ metrics-jersey3-4.2.4.jar micrometer-core-1.7.12.jar micrometer-registry-prometheus-1.7.12.jar mmseg4j-core-1.10.0.jar -mockito-core-3.3.3.jar mockito-core-3.9.0.jar +mockito-core-5.19.0.jar mockito-junit-jupiter-3.9.0.jar -netty-all-4.1.42.Final.jar -netty-all-4.1.61.Final.jar -netty-buffer-4.1.52.Final.jar -netty-buffer-4.1.72.Final.jar -netty-codec-4.1.52.Final.jar -netty-codec-4.1.72.Final.jar -netty-codec-http2-4.1.52.Final.jar -netty-codec-http2-4.1.72.Final.jar -netty-codec-http-4.1.52.Final.jar -netty-codec-http-4.1.72.Final.jar -netty-codec-socks-4.1.52.Final.jar -netty-codec-socks-4.1.72.Final.jar -netty-common-4.1.52.Final.jar -netty-common-4.1.72.Final.jar -netty-handler-4.1.130.Final.jar -netty-handler-4.1.52.Final.jar -netty-handler-4.1.72.Final.jar -netty-handler-proxy-4.1.52.Final.jar -netty-handler-proxy-4.1.72.Final.jar -netty-resolver-4.1.130.Final.jar -netty-resolver-4.1.52.Final.jar -netty-resolver-4.1.72.Final.jar -netty-tcnative-boringssl-static-2.0.25.Final.jar -netty-tcnative-classes-2.0.46.Final.jar -netty-transport-4.1.52.Final.jar -netty-transport-4.1.72.Final.jar -netty-transport-classes-epoll-4.1.130.Final.jar -netty-transport-native-epoll-4.1.130.Final.jar -netty-transport-native-unix-common-4.1.72.Final.jar -nimbus-jose-jwt-4.41.2.jar +netty-all-4.1.137.Final.jar +netty-buffer-4.1.137.Final.jar +netty-codec-4.1.137.Final.jar +netty-codec-dns-4.1.137.Final.jar +netty-codec-haproxy-4.1.137.Final.jar +netty-codec-http-4.1.137.Final.jar +netty-codec-http2-4.1.137.Final.jar +netty-codec-memcache-4.1.137.Final.jar +netty-codec-mqtt-4.1.137.Final.jar +netty-codec-redis-4.1.137.Final.jar +netty-codec-smtp-4.1.137.Final.jar +netty-codec-socks-4.1.137.Final.jar +netty-codec-stomp-4.1.137.Final.jar +netty-codec-xml-4.1.137.Final.jar +netty-common-4.1.137.Final.jar +netty-handler-4.1.137.Final.jar +netty-handler-proxy-4.1.137.Final.jar +netty-handler-ssl-ocsp-4.1.137.Final.jar +netty-resolver-4.1.137.Final.jar +netty-resolver-dns-4.1.137.Final.jar +netty-resolver-dns-classes-macos-4.1.137.Final.jar +netty-resolver-dns-native-macos-4.1.137.Final-osx-aarch_64.jar +netty-resolver-dns-native-macos-4.1.137.Final-osx-x86_64.jar +netty-tcnative-boringssl-static-2.0.81.Final-linux-aarch_64.jar +netty-tcnative-boringssl-static-2.0.81.Final-linux-x86_64.jar +netty-tcnative-boringssl-static-2.0.81.Final-osx-aarch_64.jar +netty-tcnative-boringssl-static-2.0.81.Final-osx-x86_64.jar +netty-tcnative-boringssl-static-2.0.81.Final-windows-x86_64.jar +netty-tcnative-boringssl-static-2.0.81.Final.jar +netty-tcnative-classes-2.0.81.Final.jar +netty-transport-4.1.137.Final.jar +netty-transport-classes-epoll-4.1.137.Final.jar +netty-transport-classes-kqueue-4.1.137.Final.jar +netty-transport-native-epoll-4.1.137.Final-linux-aarch_64.jar +netty-transport-native-epoll-4.1.137.Final-linux-riscv64.jar +netty-transport-native-epoll-4.1.137.Final-linux-x86_64.jar +netty-transport-native-epoll-4.1.137.Final.jar +netty-transport-native-kqueue-4.1.137.Final-osx-aarch_64.jar +netty-transport-native-kqueue-4.1.137.Final-osx-x86_64.jar +netty-transport-native-unix-common-4.1.137.Final.jar +netty-transport-rxtx-4.1.137.Final.jar +netty-transport-sctp-4.1.137.Final.jar +netty-transport-udt-4.1.137.Final.jar +nimbus-jose-jwt-8.2.1.jar nlp-lang-1.7.7.jar -objenesis-2.6.jar objenesis-3.2.jar +objenesis-3.3.jar ohc-core-0.7.4.jar okhttp-3.12.12.jar okhttp-4.10.0.jar @@ -433,53 +424,36 @@ opentracing-api-0.22.0.jar opentracing-mock-0.22.0.jar opentracing-noop-0.22.0.jar opentracing-util-0.22.0.jar +org.abego.treelayout.core-1.0.3.jar osgi-resource-locator-1.0.3.jar parboiled-core-1.2.0.jar parboiled-scala_2.12-1.2.0.jar parser-9.0-9.0.20190305.jar -perfmark-api-0.19.0.jar -perfmark-api-0.23.0.jar perfmark-api-0.25.0.jar -picocli-4.3.2.jar -pinyin4j-2.5.0.jar -powermock-api-mockito2-2.0.0-RC.3.jar -powermock-api-support-2.0.0-RC.3.jar -powermock-classloading-base-2.0.0-RC.3.jar -powermock-classloading-xstream-2.0.0-RC.3.jar -powermock-core-2.0.0-RC.3.jar -powermock-module-junit4-2.0.0-RC.3.jar -powermock-module-junit4-common-2.0.0-RC.3.jar -powermock-module-junit4-rule-2.0.0-RC.3.jar -powermock-reflect-2.0.0-RC.3.jar -protobuf-java-3.11.0.jar -protobuf-java-3.17.2.jar -protobuf-java-3.21.7.jar -protobuf-java-3.5.1.jar -protobuf-java-util-3.17.2.jar -proto-google-common-protos-1.17.0.jar -proto-google-common-protos-2.0.1.jar +picocli-4.7.6.jar +proto-google-common-protos-2.9.0.jar +protobuf-java-3.22.3.jar +protobuf-java-util-3.22.3.jar protostuff-api-1.6.0.jar protostuff-collectionschema-1.6.0.jar protostuff-core-1.6.0.jar protostuff-runtime-1.6.0.jar +query-13.6.0.jar rewriting-9.0-9.0.20190305.jar -RoaringBitmap-0.9.38.jar -rocksdbjni-6.29.5.jar -rocksdbjni-7.7.3.jar rocksdbjni-8.10.2.jar scala-java8-compat_2.12-0.8.0.jar scala-library-2.12.7.jar scala-reflect-2.12.7.jar shims-0.9.38.jar -simpleclient_common-0.10.0.jar simpleclient-0.10.0.jar +simpleclient_common-0.10.0.jar sjk-agent-0.22.jar sjk-cli-0.22.jar sjk-core-0.22.jar sjk-hflame-0.22.jar +sjk-jfr-standalone-0.7.jar sjk-jfr5-0.5.jar sjk-jfr6-0.7.jar -sjk-jfr-standalone-0.7.jar sjk-json-0.22.jar sjk-nps-0.9.jar sjk-stacktrace-0.22.jar @@ -488,46 +462,35 @@ slf4j-api-1.7.25.jar slf4j-api-1.7.31.jar slf4j-api-1.7.32.jar slf4j-api-2.0.9.jar -snakeyaml-1.18.jar -snakeyaml-1.26.jar -snakeyaml-1.27.jar -snakeyaml-1.28.jar snakeyaml-2.2.jar sofa-common-tools-1.0.12.jar sofa-rpc-all-5.7.6.jar sourcecode_2.12-0.1.4.jar -spring-aop-5.3.20.jar spring-aop-5.3.27.jar -spring-beans-5.3.20.jar spring-beans-5.3.27.jar -spring-boot-2.5.14.jar -spring-boot-actuator-2.5.14.jar -spring-boot-actuator-autoconfigure-2.5.14.jar -spring-boot-autoconfigure-2.5.0.jar -spring-boot-autoconfigure-2.5.14.jar -spring-boot-starter-2.5.0.jar -spring-boot-starter-2.5.14.jar -spring-boot-starter-actuator-2.5.14.jar -spring-boot-starter-jetty-2.5.14.jar -spring-boot-starter-json-2.5.14.jar -spring-boot-starter-log4j2-2.5.14.jar -spring-boot-starter-test-2.5.14.jar -spring-boot-starter-web-2.5.14.jar -spring-boot-test-2.5.14.jar -spring-boot-test-autoconfigure-2.5.14.jar -spring-context-5.3.20.jar +spring-boot-2.5.15.jar +spring-boot-actuator-2.5.15.jar +spring-boot-actuator-autoconfigure-2.5.15.jar +spring-boot-autoconfigure-2.5.15.jar +spring-boot-jarmode-layertools-2.7.1.jar +spring-boot-starter-2.5.15.jar +spring-boot-starter-actuator-2.5.15.jar +spring-boot-starter-jetty-2.5.15.jar +spring-boot-starter-json-2.5.15.jar +spring-boot-starter-log4j2-2.5.15.jar +spring-boot-starter-test-2.5.15.jar +spring-boot-starter-web-2.5.15.jar +spring-boot-test-2.5.15.jar +spring-boot-test-autoconfigure-2.5.15.jar spring-context-5.3.27.jar -spring-context-support-5.3.20.jar -spring-core-5.3.20.jar +spring-context-support-5.3.27.jar spring-core-5.3.27.jar -spring-expression-5.3.20.jar spring-expression-5.3.27.jar -spring-jcl-5.3.20.jar spring-jcl-5.3.27.jar spring-security-core-5.8.3.jar spring-security-crypto-5.8.3.jar -spring-web-5.3.20.jar -spring-webmvc-5.3.20.jar +spring-web-5.3.27.jar +spring-webmvc-5.3.27.jar swagger-annotations-1.5.18.jar swagger-annotations-jakarta-2.2.18.jar swagger-core-1.5.18.jar @@ -536,24 +499,22 @@ swagger-integration-jakarta-2.2.18.jar swagger-jaxrs2-jakarta-2.2.18.jar swagger-models-1.5.18.jar swagger-models-jakarta-2.2.18.jar -tinkergraph-gremlin-3.5.1.jar -token-provider-2.0.0.jar -tomcat-embed-el-9.0.63.jar +tag-expressions-6.1.2.jar +testng-xml-formatter-0.3.1.jar +tinkergraph-gremlin-3.8.1.jar +token-provider-2.0.1.jar +tomcat-embed-el-9.0.75.jar tracer-core-3.0.8.jar translation-1.0.4.jar util-9.0-9.0.20190305.jar validation-api-1.1.0.Final.jar -websocket-api-9.4.46.v20220331.jar -websocket-client-9.4.46.v20220331.jar -websocket-common-9.4.46.v20220331.jar -websocket-server-9.4.46.v20220331.jar -websocket-servlet-9.4.46.v20220331.jar -word-1.3.jar -xmlpull-1.1.3.1.jar +websocket-api-9.4.51.v20230217.jar +websocket-client-9.4.51.v20230217.jar +websocket-common-9.4.51.v20230217.jar +websocket-server-9.4.51.v20230217.jar +websocket-servlet-9.4.51.v20230217.jar xmlunit-core-2.8.4.jar -xpp3_min-1.1.4c.jar -xstream-1.4.10.jar zjsonpatch-0.3.0.jar zookeeper-3.8.6.jar zookeeper-jute-3.8.6.jar -zt-zip-1.14.jar +zt-zip-1.16.jar diff --git a/install-dist/scripts/dependency/regenerate_known_dependencies.sh b/install-dist/scripts/dependency/regenerate_known_dependencies.sh index 5251f0658a..8b6d54bcda 100644 --- a/install-dist/scripts/dependency/regenerate_known_dependencies.sh +++ b/install-dist/scripts/dependency/regenerate_known_dependencies.sh @@ -16,18 +16,27 @@ # limitations under the License. # -BASE_PATH=$(cd "$(dirname $0)" || exit; pwd) -DEP_PATH=$BASE_PATH/all_dependencies +set -euo pipefail + +BASE_PATH=$(cd "$(dirname "$0")" && pwd) +ROOT_PATH=$(cd "$BASE_PATH/../../.." && pwd) FILE_NAME=${1:-known-dependencies.txt} +MAVEN_COMMAND=${MAVEN_COMMAND:-mvn} -if [[ -d $DEP_PATH ]];then - echo "rm -r -f DEP_PATH" - rm -r -f $DEP_PATH +if [[ "$FILE_NAME" = /* ]]; then + OUTPUT_PATH=$FILE_NAME +else + OUTPUT_PATH=$BASE_PATH/$FILE_NAME fi -cd "$BASE_PATH"/../../../ || exit - -mvn dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=$DEP_PATH +# Only remove this invocation's new temporary directory, never a shared folder. +DEPENDENCY_TEMP=$(mktemp -d "${TMPDIR:-/tmp}/hugegraph-dependencies.XXXXXX") +trap 'rm -rf -- "$DEPENDENCY_TEMP"' EXIT -ls $DEP_PATH | egrep -v "^hg|hugegraph|hubble" | sort -n > $BASE_PATH/$FILE_NAME -rm -r -f $DEP_PATH +cd "$ROOT_PATH" +"$MAVEN_COMMAND" -B -ntp dependency:copy-dependencies -DincludeScope=runtime \ + "-DoutputDirectory=$DEPENDENCY_TEMP/runtime" +REVISION=$("$MAVEN_COMMAND" -q -DforceStdout help:evaluate -Dexpression=project.version) +python3 "$BASE_PATH/dependency_inventory.py" collect \ + --runtime "$DEPENDENCY_TEMP/runtime" --root "$ROOT_PATH" \ + --revision "$REVISION" --output "$OUTPUT_PATH" diff --git a/install-dist/scripts/dependency/test_dependency_inventory.py b/install-dist/scripts/dependency/test_dependency_inventory.py new file mode 100644 index 0000000000..65e0c6e130 --- /dev/null +++ b/install-dist/scripts/dependency/test_dependency_inventory.py @@ -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. + +import contextlib +import io +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest +import zipfile + +import dependency_inventory as inventory + + +class DependencyInventoryTest(unittest.TestCase): + + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) / 'checkout' + self.runtime = self.root / 'runtime' + self.runtime.mkdir(parents=True) + self.jar(self.runtime / 'resolved-1.jar') + for kind in ('server', 'pd', 'store'): + directory = self.root / ('hugegraph-' + kind) / f'apache-hugegraph-{kind}-1.7.0/lib' + directory.mkdir(parents=True) + nested = self.jar_bytes({'example/Type.class': b'class fixture'}) + self.jar(directory / 'hugegraph-executable.jar', { + 'BOOT-INF/lib/injected-2.jar': nested, + }) + + def jar_bytes(self, entries=None): + data = io.BytesIO() + with zipfile.ZipFile(data, 'w') as archive: + for name, value in (entries or {'example/Type.class': b'class fixture'}).items(): + archive.writestr(name, value) + return data.getvalue() + + def jar(self, path, entries=None): + path.write_bytes(self.jar_bytes(entries)) + + def test_runtime_and_nested_shipped_union(self): + self.assertEqual(inventory.collect(self.runtime, self.root, '1.7.0'), + {'resolved-1.jar', 'injected-2.jar'}) + + def test_missing_runtime_empty_runtime_and_package_are_failures(self): + (self.root / 'empty').mkdir() + for directory in (self.root / 'absent', self.root / 'empty'): + with self.assertRaises(inventory.InventoryFailure): + inventory.collect(directory, self.root, '1.7.0') + shutil.rmtree(self.root / 'hugegraph-store') + with self.assertRaisesRegex(inventory.InventoryFailure, 'Missing packaged store'): + inventory.collect(self.runtime, self.root, '1.7.0') + + def test_corrupt_nested_or_runtime_jar_is_failure(self): + self.jar(self.root / 'hugegraph-pd/apache-hugegraph-pd-1.7.0/lib/hugegraph-executable.jar', { + 'BOOT-INF/lib/injected-2.jar': b'not a jar', + }) + with self.assertRaisesRegex(inventory.InventoryFailure, 'Cannot inspect shipped jar'): + inventory.collect(self.runtime, self.root, '1.7.0') + (self.runtime / 'resolved-1.jar').write_text('not a jar') + with self.assertRaisesRegex(inventory.InventoryFailure, 'Cannot inspect resolved jar'): + inventory.collect(self.runtime, self.root, '1.7.0') + + def test_checker_strict_additions_removals_and_missing_inputs(self): + known, current = self.root / 'known.txt', self.root / 'current.txt' + known.write_text('one-1.jar\ntwo-2.jar\n') + current.write_text('two-2.jar\none-1.jar\n') + with contextlib.redirect_stdout(io.StringIO()): + self.assertEqual(inventory.compare(known, current), 0) + for value in ('one-1.jar\n', 'one-1.jar\ntwo-2.jar\nnew-3.jar\n'): + current.write_text(value) + self.assertEqual(inventory.compare(known, current), 1) + for value in ('', 'not-a-jar\n', 'one-1.jar\none-1.jar\n'): + current.write_text(value) + with self.assertRaises(inventory.InventoryFailure): + inventory.compare(known, current) + current.unlink() + with self.assertRaises(OSError): + inventory.compare(known, current) + + def script_fixture(self): + directory = self.root / 'install-dist/scripts/dependency' + directory.mkdir(parents=True) + original = Path(__file__).parent + for name in ('regenerate_known_dependencies.sh', 'check_dependencies.sh', 'dependency_inventory.py'): + shutil.copyfile(original / name, directory / name) + maven = self.root / 'fake-mvn' + maven.write_text('''#!/usr/bin/env python3 +import os, pathlib, sys, zipfile +if os.environ.get('FAKE_MAVEN_FAIL'): + sys.exit(int(os.environ['FAKE_MAVEN_FAIL'])) +if 'dependency:copy-dependencies' in sys.argv: + value = next(value for value in sys.argv if value.startswith('-DoutputDirectory=')) + directory = pathlib.Path(value.split('=', 1)[1]) + directory.mkdir(parents=True) + with zipfile.ZipFile(directory / 'resolved-1.jar', 'w') as archive: + archive.writestr('example/Type.class', b'class fixture') +else: + print('1.7.0') +''') + maven.chmod(0o755) + return directory, dict(os.environ, MAVEN_COMMAND=str(maven)) + + def test_shell_producer_consumer_contract(self): + directory, environment = self.script_fixture() + current = self.root / 'target/dependency/current.txt' + generated = subprocess.run(['bash', str(directory / 'regenerate_known_dependencies.sh'), str(current)], + env=environment, capture_output=True, text=True) + self.assertEqual(generated.returncode, 0, generated.stderr) + self.assertEqual(inventory.read_inventory(current), ['injected-2.jar', 'resolved-1.jar']) + known = directory / 'known-dependencies.txt' + known.write_text(current.read_text()) + checked = subprocess.run(['bash', str(directory / 'check_dependencies.sh'), str(known), str(current)], + capture_output=True, text=True) + self.assertEqual(checked.returncode, 0, checked.stderr) + current.unlink() + missing = subprocess.run(['bash', str(directory / 'check_dependencies.sh'), str(known), str(current)], + capture_output=True, text=True) + self.assertNotEqual(missing.returncode, 0) + + def test_shell_maven_failure_does_not_publish_inventory(self): + directory, environment = self.script_fixture() + current = self.root / 'current.txt' + result = subprocess.run(['bash', str(directory / 'regenerate_known_dependencies.sh'), str(current)], + env=dict(environment, FAKE_MAVEN_FAIL='17'), capture_output=True, text=True) + self.assertEqual(result.returncode, 17) + self.assertFalse(current.exists()) + + def test_shell_missing_package_does_not_publish_inventory(self): + directory, environment = self.script_fixture() + shutil.rmtree(self.root / 'hugegraph-store') + current = self.root / 'current.txt' + result = subprocess.run(['bash', str(directory / 'regenerate_known_dependencies.sh'), str(current)], + env=environment, capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('Missing packaged store', result.stderr) + self.assertFalse(current.exists()) + + +if __name__ == '__main__': + unittest.main() diff --git a/install-dist/scripts/shared-foundation/check_shared_foundation.py b/install-dist/scripts/shared-foundation/check_shared_foundation.py new file mode 100644 index 0000000000..0e6ad5c415 --- /dev/null +++ b/install-dist/scripts/shared-foundation/check_shared_foundation.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Check shared ownership, resolved dependencies and shipped classpaths. + +Run after a reactor install/package: + python3 install-dist/scripts/shared-foundation/check_shared_foundation.py --revision 1.8.0 + +--source-only is an explicitly narrower check for fast source-edit iterations. +The full check always regenerates dependency trees; stale tree files are not evidence. +""" + +import argparse +import io +from pathlib import Path +import re +import subprocess +import sys +import xml.etree.ElementTree as ET +import zipfile + + +SOURCE_MODULES = ( + 'hugegraph-server/hugegraph-core', + 'hugegraph-struct', + 'hugegraph-commons/hugegraph-common', + 'hugegraph-commons/hugegraph-rpc', +) +DEPENDENCY_MODULES = ( + 'hugegraph-struct', + 'hugegraph-commons/hugegraph-common', + 'hugegraph-commons/hugegraph-rpc', + 'hugegraph-pd/hg-pd-service', + 'hugegraph-store/hg-store-core', + 'hugegraph-store/hg-store-node', +) +REQUIRED_ARTIFACTS = { + 'server': {'hugegraph-core', 'hugegraph-struct', 'hugegraph-common', 'hugegraph-rpc'}, + 'pd': {'hg-pd-service', 'hg-pd-core', 'hugegraph-common'}, + 'store': {'hg-store-node', 'hg-store-core', 'hugegraph-struct', 'hugegraph-common'}, +} +SCOPES = {'compile', 'runtime', 'provided', 'test', 'system', 'import'} +GROUP = 'org.apache.hugegraph' +SERVER_ARTIFACTS = {'hugegraph-server', 'hugegraph-core', 'hugegraph-api', 'hugegraph-dist', + 'hugegraph-hstore', 'hugegraph-hbase', 'hugegraph-rocksdb', 'hugegraph-example'} +SOURCE_IMPORT_CONFIGS = ( + 'hugegraph-server/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml', + 'hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft1/gremlin-server.yaml', + 'hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft2/gremlin-server.yaml', + 'hugegraph-server/hugegraph-dist/src/assembly/travis/conf-raft3/gremlin-server.yaml', +) + + +class CheckFailure(Exception): + pass + + +def retired_classes(): + path = Path(__file__).with_name('retired-core-classes.txt') + result = {line.strip() for line in path.read_text().splitlines() + if line.strip() and not line.startswith('#')} + if not result or any(not name.startswith(GROUP + '.') for name in result): + raise CheckFailure(f'Missing or invalid retired-owner manifest: {path}') + return result + + +def java_member_classes(source, package): + # Strip comments/literals before matching declarations, so references to a + # retired type in documentation, strings or class literals are harmless. + source = re.sub(r'//[^\n]*|/\*.*?\*/|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'', + '', source, flags=re.DOTALL) + tokens = re.findall(r'[A-Za-z_$][\w$]*|[{}.()]', source) + scopes = [] + pending = None + names = set() + for index, token in enumerate(tokens): + if (token in ('class', 'interface', 'enum') and + (index == 0 or tokens[index - 1] != '.') and + (not scopes or scopes[-1] is not None) and index + 1 < len(tokens) and + re.fullmatch(r'[A-Za-z_$][\w$]*', tokens[index + 1])): + pending = tokens[index + 1] + elif token == '{': + scopes.append(pending) + if pending is not None: + names.add(package + '.' + '$'.join(name for name in scopes if name is not None)) + pending = None + elif token == '}' and scopes: + scopes.pop() + return names + + +def is_retired_class(name, retired): + return any(name == owner or name.startswith(owner + '$') for owner in retired) + + +def server_module_inventory(root): + boundary = (root / 'hugegraph-server').resolve() + projects, visited, active = {}, set(), set() + + def visit(pom): + try: + pom = pom.resolve() + except (OSError, RuntimeError) as error: + raise CheckFailure(f'Cannot resolve declared Server POM: {pom}: {error}') from error + try: + pom.relative_to(boundary) + except ValueError as error: + raise CheckFailure(f'Declared Server POM escapes its reactor: {pom}') from error + if pom in active: + raise CheckFailure(f'Cyclic Server module declaration: {pom}') + if pom in visited: + return + if not pom.is_file(): + raise CheckFailure(f'Missing declared Server POM: {pom}') + try: + project = ET.parse(pom).getroot() + except ET.ParseError as error: + raise CheckFailure(f'Invalid Server module POM: {pom}: {error}') from error + if project.tag.rsplit('}', 1)[-1] != 'project': + raise CheckFailure(f'Invalid Server project element: {pom}') + namespace = project.tag[:-len('project')] + artifact = (project.findtext(namespace + 'artifactId') or '').strip() + if not re.fullmatch(r'[A-Za-z0-9_.-]+', artifact): + raise CheckFailure(f'Missing or nonliteral Server artifactId: {pom}') + if pom.parent in projects and projects[pom.parent] != artifact: + raise CheckFailure(f'Server projects share a source directory: {pom.parent}') + projects[pom.parent] = artifact + active.add(pom) + # Include every possible profile, irrespective of the current host or + # active profiles. Inactive modules must not become a retirement loophole. + modules = project.findall(namespace + 'modules/' + namespace + 'module') + modules.extend(project.findall(namespace + 'profiles/' + namespace + 'profile/' + + namespace + 'modules/' + namespace + 'module')) + for declaration in modules: + value = (declaration.text or '').strip() + if not value or '$' in value: + raise CheckFailure(f'Empty or nonliteral Server module declaration: {pom}: {value}') + target = pom.parent / value + visit(target if target.suffix == '.xml' or target.is_file() else target / 'pom.xml') + active.remove(pom) + visited.add(pom) + + visit(boundary / 'pom.xml') + test_roots = [directory for directory, artifact in projects.items() if artifact == 'hugegraph-test'] + return {directory: artifact for directory, artifact in projects.items() + if not any(directory == test or test in directory.parents for test in test_roots)} + + +def check_sources(root, retired): + root = root.resolve() + owners = {} + counts = {} + production_roots = set() + for module in SOURCE_MODULES: + directories = [root / module / 'src/main' / kind for kind in ('java', 'dev')] + if not any(list(directory.rglob('*.java')) for directory in directories): + raise CheckFailure(f'Missing production sources: {root / module / "src/main"}') + production_roots.update(directories) + # Use the same recursively declared reactor inventory as shipped ownership, + # never search copied distributions or target directories for module POMs. + for module in server_module_inventory(root): + production_roots.update(module / 'src/main' / kind for kind in ('java', 'dev')) + # Generated grpc sources in src/main/java are outside the shared-owner + # scan. Hand-maintained production sources beside them must still be checked. + for pattern in ('hugegraph-*/src/main/dev', 'hugegraph-*/*/src/main/dev'): + production_roots.update(directory for directory in root.glob(pattern) + if directory.relative_to(root).parts[0] != 'hugegraph-server') + for directory in sorted(production_roots): + module = str(directory.parent.parent.parent.relative_to(root)) + files = sorted(directory.rglob('*.java')) + counts[module] = counts.get(module, 0) + len(files) + for path in files: + source = path.read_text() + package = re.search(r'^\s*package\s+([\w.]+)\s*;', source, re.MULTILINE) + if not package: + raise CheckFailure(f'Cannot determine source package: {path}') + name = package.group(1) + '.' + path.stem + if not name.startswith(GROUP + '.'): + continue + if module == 'hugegraph-server' or module.startswith('hugegraph-server/'): + declared = java_member_classes(source, package.group(1)) | {name} + if any(is_retired_class(declaration, retired) for declaration in declared): + raise CheckFailure(f'Retired shared implementation returned to Server: {path}') + if name in owners: + raise CheckFailure(f'Duplicate source FQCN {name}: {owners[name]} and {path}') + owners[name] = path + print(f'Source ownership checked: {counts}') + return owners + + +def class_imports(path): + if not path.is_file(): + raise CheckFailure(f'Missing Gremlin import configuration: {path}') + # This is a deliberately narrow reader for classImports flow/block lists, + # not a YAML implementation. Unknown shapes fail rather than evade checks. + lines = [re.sub(r'#.*$', '', line) for line in path.read_text().splitlines()] + imports = [] + for index, line in enumerate(lines): + match = re.match(r'^(\s*)classImports\s*:\s*(.*)$', line) + if not match: + continue + indent, tail = len(match.group(1)), match.group(2).strip() + if tail.startswith('['): + body = '\n'.join([tail] + lines[index + 1:]) + end = body.find(']') + if end < 0: + raise CheckFailure(f'Unterminated classImports list: {path}') + values = body[1:end].split(',') + elif not tail: + values = [] + for following in lines[index + 1:]: + if not following.strip(): + continue + following_indent = len(following) - len(following.lstrip()) + entry = following.strip() + if following_indent < indent or (following_indent == indent and not entry.startswith('-')): + break + if not entry.startswith('- '): + raise CheckFailure(f'Unsupported classImports entry: {path}: {entry}') + values.append(entry[2:]) + else: + raise CheckFailure(f'Unsupported classImports shape: {path}: {tail}') + for value in values: + name = value.strip().strip('"\'') + if not name: + continue + if not re.fullmatch(r'[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+', name): + raise CheckFailure(f'Invalid classImports class name: {path}: {name}') + imports.append(name) + if not imports: + raise CheckFailure(f'Missing or empty classImports list: {path}') + return imports + + +def check_class_imports(path, available, local_only=False): + imports = class_imports(path) + for name in imports: + # JDK classes are supplied by the runtime, not distribution jars. + # Gremlin startup tests verify runtime loading, beyond this ownership check. + if name.startswith('java.') or (local_only and not name.startswith(GROUP + '.')): + continue + if name not in available: + raise CheckFailure(f'Gremlin classImports class is unavailable: {name} ({path})') + print(f'Gremlin classImports checked: {path} ({len(imports)} imports)') + + +def check_source_imports(root, owners): + for path in SOURCE_IMPORT_CONFIGS: + check_class_imports(root / path, owners, local_only=True) + + +def parse_tree(path, expected_artifact): + if not path.is_file(): + raise CheckFailure(f'Missing resolved dependency tree: {path}') + nodes = [] + for line in path.read_text().splitlines(): + if not line.strip(): + continue + coordinate = re.sub(r'^[\s|+\\-]*', '', line).strip() + coordinate = re.sub(r' \(optional\)$', '', coordinate) + parts = coordinate.split(':') + if (len(parts) < 4 or len(parts) > 6 or + any(not part or re.search(r'\s', part) for part in parts)): + raise CheckFailure(f'Malformed resolved tree line in {path}: {line}') + if nodes and parts[-1] not in SCOPES: + raise CheckFailure(f'Missing dependency scope in {path}: {line}') + nodes.append((parts[0], parts[1])) + if not nodes or nodes[0] != (GROUP, expected_artifact): + raise CheckFailure(f'Wrong or empty dependency tree root: {path}; expected {expected_artifact}') + return nodes + + +def check_tree(path, module): + artifact = module.rsplit('/', 1)[-1] + nodes = parse_tree(path, artifact) + forbidden = set() + if module.startswith(('hugegraph-pd/', 'hugegraph-store/')): + forbidden.add('hugegraph-core') + if artifact == 'hugegraph-struct': + forbidden.update(('hugegraph-core', 'hugegraph-rpc')) + if artifact == 'hugegraph-rpc': + forbidden.update(('hugegraph-core', 'hugegraph-struct')) + if artifact == 'hugegraph-common': + forbidden.update(('hugegraph-core', 'hugegraph-struct', 'hugegraph-server', + 'hugegraph-store', 'hugegraph-pd')) + for group, name in nodes[1:]: + if group != GROUP: + continue + component_dependency = (name.startswith(('hg-pd-', 'hg-store-')) or + name in ('hugegraph-pd', 'hugegraph-store')) + if name in forbidden or (artifact in ('hugegraph-struct', 'hugegraph-common', 'hugegraph-rpc') and + component_dependency): + raise CheckFailure(f'Forbidden resolved dependency of {artifact}: {group}:{name} ({path})') + print(f'Resolved dependency boundary checked: {artifact} ({len(nodes) - 1} dependencies)') + + +def select_tree(lines, artifact): + starts = [i for i, line in enumerate(lines) if line.startswith(GROUP + ':')] + selected = [] + for index, start in enumerate(starts): + if lines[start].split(':')[1] == artifact: + stop = starts[index + 1] if index + 1 < len(starts) else len(lines) + selected.append(lines[start:stop]) + if len(selected) != 1: + raise CheckFailure(f'Expected exactly one current reactor tree for {artifact}') + return '\n'.join(selected[0]) + '\n' + + +def resolve_dependencies(root, maven): + directory = root / 'target/shared-foundation' + directory.mkdir(parents=True, exist_ok=True) + modules = set(DEPENDENCY_MODULES) + # Cover every distributed submodule, including standalone client/transfer + # modules and tests, not only dependencies currently reachable from services. + for parent in ('hugegraph-pd', 'hugegraph-store'): + modules.update(str(path.parent.relative_to(root)) + for path in (root / parent).glob('*/pom.xml')) + for module in modules: + if not (root / module / 'pom.xml').is_file(): + raise CheckFailure(f'Missing expected Maven module: {module}') + output = directory / 'reactor-dependencies.txt' + output.unlink(missing_ok=True) + # Resolve current reactor models, never reuse old tree files. Upstream + # project POMs are read from this checkout rather than a stale local install. + subprocess.run([ + maven, '-B', '-ntp', '-pl', ','.join(sorted(modules)), '-am', + 'org.apache.maven.plugins:maven-dependency-plugin:3.8.1:tree', + '-DoutputType=text', f'-DoutputFile={output}', '-DappendOutput=true', + ], cwd=root, check=True) + if not output.is_file(): + raise CheckFailure(f'Missing resolved reactor dependencies: {output}') + lines = output.read_text().splitlines() + for module in sorted(modules): + artifact = module.rsplit('/', 1)[-1] + selected = directory / (artifact + '.txt') + selected.write_text(select_tree(lines, artifact)) + check_tree(selected, module) + + +def properties(text): + return {key.strip(): value.strip() for line in text.splitlines() + if line.strip() and not line.lstrip().startswith(('#', '!')) and '=' in line + for key, value in [line.split('=', 1)]} + + +def server_artifact_ids(root): + return set(server_module_inventory(root).values()) + + +def scan_jar(source, label, owners, artifacts, retired, distribution, available=None, + expected_revision=None, server_artifacts=None): + if server_artifacts is None: + server_artifacts = SERVER_ARTIFACTS + try: + archive = zipfile.ZipFile(source) + except (OSError, zipfile.BadZipFile) as error: + raise CheckFailure(f'Invalid shipped jar {label}: {error}') from error + with archive: + names = archive.namelist() + identities = set() + for name in names: + if name.startswith('META-INF/maven/') and name.endswith('/pom.properties'): + values = properties(archive.read(name).decode('utf-8')) + if values.get('groupId') == GROUP: + artifact = values.get('artifactId') + if not artifact or not values.get('version'): + raise CheckFailure(f'Incomplete artifact metadata in {label}: {name}') + if expected_revision is not None and values['version'] != expected_revision: + raise CheckFailure(f'Stale shipped artifact {GROUP}:{artifact}:{values["version"]} ' + f'in {label}; expected {expected_revision}') + identities.add(artifact) + artifacts.update(identities) + if distribution in ('pd', 'store') and 'hugegraph-core' in identities: + raise CheckFailure(f'Forbidden hugegraph-core shipped in {distribution}: {label}') + seen_entries = set() + for name in names: + if name.startswith(('BOOT-INF/lib/', 'WEB-INF/lib/')) and name.endswith('.jar'): + scan_jar(io.BytesIO(archive.read(name)), f'{label}!/{name}', owners, + artifacts, retired, distribution, available, expected_revision, server_artifacts) + continue + normalized = re.sub(r'^(?:BOOT-INF/classes/|WEB-INF/classes/|META-INF/versions/\d+/)', '', name) + if available is not None and normalized.endswith('.class'): + available.add(normalized[:-6].replace('/', '.')) + if not normalized.startswith('org/apache/hugegraph/') or not normalized.endswith('.class'): + continue + if name in seen_entries: + raise CheckFailure(f'Duplicate class entry inside jar {label}: {name}') + seen_entries.add(name) + fqcn = normalized[:-6].replace('/', '.') + if identities & server_artifacts and is_retired_class(fqcn, retired): + raise CheckFailure(f'Retired shared class shipped in Server: {fqcn} ({label})') + previous = owners.get(fqcn) + if previous is not None and previous != label: + raise CheckFailure(f'Duplicate shipped FQCN {fqcn}: {previous} and {label}') + # Multi-release variants within the same jar are a single owner. + owners[fqcn] = label + + +def check_distribution(path, kind, retired, expected_revision=None, server_artifacts=None): + library = path / 'lib' + jars = sorted(library.rglob('*.jar')) + if not library.is_dir() or not jars: + raise CheckFailure(f'Missing shipped {kind} lib jars: {library}') + for name in ('ext', 'plugins'): + if (path / name).exists(): + jars.extend(sorted((path / name).rglob('*.jar'))) + owners = {} + artifacts = set() + available = set() + for jar in jars: + scan_jar(jar, str(jar), owners, artifacts, retired, kind, available, expected_revision, server_artifacts) + missing = REQUIRED_ARTIFACTS[kind] - artifacts + if missing: + raise CheckFailure(f'Missing expected artifacts in {kind} distribution: {sorted(missing)}') + if not owners: + raise CheckFailure(f'No HugeGraph classes found in shipped {kind} jars') + if kind == 'server': + check_class_imports(path / 'conf/gremlin-server.yaml', available) + print(f'Shipped {kind} classpath checked: {len(owners)} HugeGraph classes, ' + f'{len(artifacts)} project artifacts') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parents[3]) + parser.add_argument('--source-only', action='store_true') + parser.add_argument('--revision', help='Exact version of the freshly assembled distributions') + parser.add_argument('--maven', default='mvn', help='Maven executable') + args = parser.parse_args() + if not args.source_only and not args.revision: + parser.error('--revision is required for resolved dependency and distribution checks') + root = args.root.resolve() + try: + retired = retired_classes() + owners = check_sources(root, retired) + check_source_imports(root, owners) + if args.source_only: + print('Source-only check passed; dependencies and distributions were NOT checked.') + return 0 + resolve_dependencies(root, args.maven) + server_artifacts = server_artifact_ids(root) + for kind in REQUIRED_ARTIFACTS: + path = root / ('hugegraph-' + kind) / f'apache-hugegraph-{kind}-{args.revision}' + check_distribution(path, kind, retired, args.revision, server_artifacts) + except (CheckFailure, OSError, UnicodeError, subprocess.CalledProcessError) as error: + print(f'Shared foundation check FAILED: {error}', file=sys.stderr) + return 1 + print('Shared foundation ownership, resolved dependencies and shipped classpaths passed.') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/install-dist/scripts/shared-foundation/retired-core-classes.txt b/install-dist/scripts/shared-foundation/retired-core-classes.txt new file mode 100644 index 0000000000..2aeb3c2ed0 --- /dev/null +++ b/install-dist/scripts/shared-foundation/retired-core-classes.txt @@ -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. + + +# Shared owners removed from core. Do not reintroduce these FQCNs in core. +org.apache.hugegraph.HugeException +org.apache.hugegraph.analyzer.Analyzer +org.apache.hugegraph.analyzer.AnalyzerFactory +org.apache.hugegraph.analyzer.AnsjAnalyzer +org.apache.hugegraph.analyzer.HanLPAnalyzer +org.apache.hugegraph.analyzer.IKAnalyzer +org.apache.hugegraph.analyzer.JcsegAnalyzer +org.apache.hugegraph.analyzer.JiebaAnalyzer +org.apache.hugegraph.analyzer.MMSeg4JAnalyzer +org.apache.hugegraph.analyzer.SmartCNAnalyzer +org.apache.hugegraph.auth.AuthConstant +org.apache.hugegraph.auth.TokenGenerator +org.apache.hugegraph.backend.BackendException +org.apache.hugegraph.backend.id.EdgeId +org.apache.hugegraph.backend.id.Id +org.apache.hugegraph.backend.id.IdGenerator +org.apache.hugegraph.backend.id.IdUtil +org.apache.hugegraph.backend.id.SplicingIdGenerator +org.apache.hugegraph.backend.query.Aggregate +org.apache.hugegraph.backend.query.BatchConditionQuery +org.apache.hugegraph.backend.query.Condition +org.apache.hugegraph.backend.query.ConditionQuery +org.apache.hugegraph.backend.query.IdPrefixQuery +org.apache.hugegraph.backend.query.IdQuery +org.apache.hugegraph.backend.query.IdRangeQuery +org.apache.hugegraph.backend.query.Query +org.apache.hugegraph.backend.query.serializer.AbstractSerializerAdapter +org.apache.hugegraph.backend.query.serializer.QueryAdapter +org.apache.hugegraph.backend.query.serializer.QueryIdAdapter +org.apache.hugegraph.backend.serializer.BytesBuffer +org.apache.hugegraph.backend.store.BackendEntry$BackendColumn +org.apache.hugegraph.backend.store.Shard +org.apache.hugegraph.backend.tx.GraphIndexTransaction$MatchedIndex +org.apache.hugegraph.exception.ErrorCodeProvider +org.apache.hugegraph.exception.LimitExceedException +org.apache.hugegraph.exception.NotAllowException +org.apache.hugegraph.exception.NotFoundException +org.apache.hugegraph.exception.NotSupportException +org.apache.hugegraph.rpc.RpcServiceConfig4Client +org.apache.hugegraph.rpc.RpcServiceConfig4Server +org.apache.hugegraph.schema.EdgeLabel +org.apache.hugegraph.schema.IndexLabel +org.apache.hugegraph.schema.PropertyKey +org.apache.hugegraph.schema.SchemaElement +org.apache.hugegraph.schema.SchemaLabel +org.apache.hugegraph.schema.Userdata +org.apache.hugegraph.schema.VertexLabel +org.apache.hugegraph.schema.builder.SchemaBuilder +org.apache.hugegraph.structure.GraphType +org.apache.hugegraph.structure.HugeIndex +org.apache.hugegraph.type.HugeType +org.apache.hugegraph.type.Idfiable +org.apache.hugegraph.type.Indexable +org.apache.hugegraph.type.Nameable +org.apache.hugegraph.type.Propertiable +org.apache.hugegraph.type.Typeable +org.apache.hugegraph.type.define.Action +org.apache.hugegraph.type.define.AggregateType +org.apache.hugegraph.type.define.Cardinality +org.apache.hugegraph.type.define.CollectionType +org.apache.hugegraph.type.define.DataType +org.apache.hugegraph.type.define.Directions +org.apache.hugegraph.type.define.EdgeLabelType +org.apache.hugegraph.type.define.Frequency +org.apache.hugegraph.type.define.HugeKeys +org.apache.hugegraph.type.define.IdStrategy +org.apache.hugegraph.type.define.IndexType +org.apache.hugegraph.type.define.SchemaStatus +org.apache.hugegraph.type.define.SerialEnum +org.apache.hugegraph.type.define.WriteType +org.apache.hugegraph.util.Blob +org.apache.hugegraph.util.KryoUtil +org.apache.hugegraph.util.LZ4Util +org.apache.hugegraph.util.StringEncoding +org.apache.hugegraph.util.collection.CollectionFactory +org.apache.hugegraph.util.collection.IdSet +org.apache.hugegraph.util.collection.IntIterator +org.apache.hugegraph.util.collection.IntMap +org.apache.hugegraph.util.collection.IntSet diff --git a/install-dist/scripts/shared-foundation/test_shared_foundation.py b/install-dist/scripts/shared-foundation/test_shared_foundation.py new file mode 100644 index 0000000000..0707f22afc --- /dev/null +++ b/install-dist/scripts/shared-foundation/test_shared_foundation.py @@ -0,0 +1,638 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 io +from pathlib import Path +import tempfile +import unittest +import xml.etree.ElementTree as ET +import zipfile + +import check_shared_foundation as check + + +class FoundationChecksTest(unittest.TestCase): + + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.addCleanup(self.temporary.cleanup) + + def jar(self, artifact, classes=(), entries=None, version='1.8.0', group=check.GROUP): + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, 'w') as archive: + archive.writestr(f'META-INF/maven/{group}/{artifact}/pom.properties', + f'groupId={group}\nartifactId={artifact}\nversion={version}\n') + for name in classes: + archive.writestr(name, b'class fixture') + for name, value in (entries or {}).items(): + archive.writestr(name, value) + return buffer.getvalue() + + def distribution(self, kind='server', nested=False): + path = self.root / kind + (path / 'lib').mkdir(parents=True) + jars = {} + for artifact in check.REQUIRED_ARTIFACTS[kind]: + name = artifact.replace('-', '_') + jars[artifact] = self.jar(artifact, [f'org/apache/hugegraph/{name}.class']) + if nested: + executable = 'hg-pd-service' if kind == 'pd' else 'hg-store-node' + entries = {f'BOOT-INF/lib/{name}.jar': value for name, value in jars.items() + if name != executable} + data = self.jar(executable, [f'BOOT-INF/classes/org/apache/hugegraph/{executable}.class'], entries) + (path / 'lib' / 'service.jar').write_bytes(data) + else: + for name, value in jars.items(): + (path / 'lib' / (name + '.jar')).write_bytes(value) + if kind == 'server': + (path / 'conf').mkdir() + (path / 'conf/gremlin-server.yaml').write_text( + 'classImports: [org.apache.hugegraph.hugegraph_core]\n') + return path + + def tree(self, artifact, descendants=''): + path = self.root / 'tree.txt' + path.write_text(f'{check.GROUP}:{artifact}:jar:1.8.0\n' + descendants) + return path + + def test_shipped_classpaths_flat_and_boot(self): + for kind in ('server', 'pd', 'store'): + check.check_distribution(self.distribution(kind, nested=kind != 'server'), kind, set()) + + def test_duplicate_class_in_two_shipped_jars_fails(self): + path = self.distribution() + (path / 'lib' / 'copy.jar').write_bytes(self.jar('copy', ['org/apache/hugegraph/hugegraph_struct.class'])) + with self.assertRaisesRegex(check.CheckFailure, 'Duplicate shipped FQCN'): + check.check_distribution(path, 'server', set()) + + def test_duplicate_in_nested_boot_jar_fails(self): + duplicate = 'org/apache/hugegraph/Conflict.class' + jars = {f'BOOT-INF/lib/{name}.jar': self.jar(name, [duplicate]) for name in ('one', 'two')} + path = self.root / 'boot.jar' + path.write_bytes(self.jar('boot', entries=jars)) + with self.assertRaisesRegex(check.CheckFailure, 'Duplicate shipped FQCN'): + check.scan_jar(path, str(path), {}, set(), set(), 'pd') + + def test_multi_release_variants_in_one_jar_are_one_owner(self): + path = self.root / 'mr.jar' + path.write_bytes(self.jar('variant', ['org/apache/hugegraph/Variant.class', + 'META-INF/versions/11/org/apache/hugegraph/Variant.class'])) + owners = {} + check.scan_jar(path, str(path), owners, set(), set(), 'server') + self.assertEqual(list(owners), ['org.apache.hugegraph.Variant']) + + def test_missing_distribution_and_required_artifact_fail_closed(self): + with self.assertRaisesRegex(check.CheckFailure, 'Missing shipped'): + check.check_distribution(self.root / 'absent', 'server', set()) + path = self.distribution() + (path / 'lib' / 'hugegraph-struct.jar').unlink() + with self.assertRaisesRegex(check.CheckFailure, 'Missing expected artifacts'): + check.check_distribution(path, 'server', set()) + + def test_corrupt_jar_is_failure(self): + path = self.distribution() + (path / 'lib' / 'broken.jar').write_text('not a zip') + with self.assertRaisesRegex(check.CheckFailure, 'Invalid shipped jar'): + check.check_distribution(path, 'server', set()) + + def test_nested_core_shipped_with_pd_is_failure(self): + path = self.distribution('pd', nested=True) + (path / 'lib' / 'extra.jar').write_bytes(self.jar('extra', entries={ + 'BOOT-INF/lib/core.jar': self.jar('hugegraph-core', ['org/apache/hugegraph/Core.class']) + })) + with self.assertRaisesRegex(check.CheckFailure, 'Forbidden hugegraph-core shipped'): + check.check_distribution(path, 'pd', set()) + + def test_transitive_forbidden_dependencies_fail(self): + for module, forbidden in (('hugegraph-pd/hg-pd-service', 'hugegraph-core'), + ('hugegraph-store/hg-store-core', 'hugegraph-core'), + ('hugegraph-struct', 'hg-pd-client'), + ('hugegraph-struct', 'hugegraph-core'), + ('hugegraph-commons/hugegraph-common', 'hg-store-client')): + with self.subTest(module=module, forbidden=forbidden): + artifact = module.rsplit('/', 1)[-1] + descendants = (f'+- external:parent:jar:1.0:compile\n' + f'| \\- {check.GROUP}:{forbidden}:jar:1.8.0:compile\n') + with self.assertRaisesRegex(check.CheckFailure, 'Forbidden resolved dependency'): + check.check_tree(self.tree(artifact, descendants), module) + + def test_struct_rejects_all_pd_store_component_dependencies(self): + for name in ('hg-pd-service', 'hg-store-common', 'hugegraph-pd', 'hugegraph-store'): + for prefix in ('', '+- external:parent:jar:1.0:compile\n| '): + with self.subTest(name=name, transitive=bool(prefix)): + descendants = prefix + f'\\- {check.GROUP}:{name}:jar:1.8.0:compile\n' + with self.assertRaisesRegex(check.CheckFailure, 'Forbidden resolved dependency'): + check.check_tree(self.tree('hugegraph-struct', descendants), 'hugegraph-struct') + # The boundary concerns HugeGraph components, not similarly named external artifacts. + tree = self.tree('hugegraph-struct', '\\- external:hg-pd-service:jar:1.0:compile\n') + check.check_tree(tree, 'hugegraph-struct') + + def test_allowed_kryo_dependency_and_classifier(self): + tree = self.tree('hugegraph-struct', '\\- org.apache.tinkerpop:gremlin-shaded:jar:1.0:compile\n' + '+- external:native:jar:linux:1.0:runtime (optional)\n') + check.check_tree(tree, 'hugegraph-struct') + + def test_missing_wrong_or_malformed_tree_fails_closed(self): + with self.assertRaisesRegex(check.CheckFailure, 'Missing resolved'): + check.parse_tree(self.root / 'absent', 'hugegraph-struct') + for value in ('', 'this is a Maven error\n', f'{check.GROUP}:wrong:jar:1.8.0\n', + f'{check.GROUP}:hugegraph-struct:jar:1.8.0\n+- broken:data\n'): + path = self.root / 'bad.txt' + path.write_text(value) + with self.subTest(value=value), self.assertRaises(check.CheckFailure): + check.parse_tree(path, 'hugegraph-struct') + + def sources(self): + self.declare_server_module('hugegraph-core') + for index, module in enumerate(check.SOURCE_MODULES): + path = self.root / module / 'src/main/java/org/apache/hugegraph' / f'Unique{index}.java' + path.parent.mkdir(parents=True) + path.write_text(f'package org.apache.hugegraph;\npublic class Unique{index} {{}}\n') + + def declare_server_module(self, module): + namespace = '{http://maven.apache.org/POM/4.0.0}' + root = self.root / 'hugegraph-server' + root.mkdir(exist_ok=True) + pom = root / 'pom.xml' + if not pom.exists(): + pom.write_text('' + 'hugegraph-server') + tree = ET.parse(pom) + modules = tree.getroot().find(namespace + 'modules') + if module not in [entry.text for entry in modules]: + ET.SubElement(modules, namespace + 'module').text = module + tree.write(pom, encoding='unicode') + child = root / module / 'pom.xml' + child.parent.mkdir(parents=True, exist_ok=True) + if not child.exists(): + child.write_text('' + '' + Path(module).name + '') + return child + + def test_target_reactor_tree_must_exist_exactly_once(self): + lines = ['org.apache.hugegraph:hugegraph: pom:1.8.0'.replace(': ', ':'), + 'org.apache.hugegraph:hugegraph-struct:jar:1.8.0', + '+- external:one:jar:1.0:compile', + 'org.apache.hugegraph:hg-store-core:jar:1.8.0', + '\\- org.apache.hugegraph:hugegraph-struct:jar:1.8.0:compile'] + self.assertEqual(check.select_tree(lines, 'hugegraph-struct'), '\n'.join(lines[1:3]) + '\n') + with self.assertRaisesRegex(check.CheckFailure, 'exactly one'): + check.select_tree(lines, 'missing') + with self.assertRaisesRegex(check.CheckFailure, 'exactly one'): + check.select_tree(lines + lines[1:3], 'hugegraph-struct') + + def test_source_duplicates_and_retired_owner_are_rejected(self): + self.sources() + check.check_sources(self.root, set()) + path = self.root / check.SOURCE_MODULES[1] / 'src/main/java/org/apache/hugegraph/Unique0.java' + path.write_text('package org.apache.hugegraph;\npublic class Unique0 {}\n') + with self.assertRaisesRegex(check.CheckFailure, 'Duplicate source FQCN'): + check.check_sources(self.root, set()) + path.unlink() + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared implementation'): + check.check_sources(self.root, {'org.apache.hugegraph.Unique0'}) + + def test_manual_dev_sources_cannot_duplicate_shared_owners(self): + self.sources() + # Both the shared modules' alternate source roots and hand-maintained + # grpc helpers are production input, unlike generated grpc Java sources. + for module in (check.SOURCE_MODULES[1], 'hugegraph-store/hg-store-grpc'): + with self.subTest(module=module): + path = self.root / module / 'src/main/dev/org/apache/hugegraph/Unique0.java' + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text('package org.apache.hugegraph;\npublic class Unique0 {}\n') + with self.assertRaisesRegex(check.CheckFailure, 'Duplicate source FQCN'): + check.check_sources(self.root, set()) + path.unlink() + + def test_server_api_backend_and_dist_source_duplicates_are_rejected(self): + self.sources() + for module in ('hugegraph-api', 'hugegraph-hstore', 'hugegraph-dist'): + self.declare_server_module(module) + with self.subTest(module=module): + path = (self.root / 'hugegraph-server' / module / + 'src/main/java/org/apache/hugegraph/Unique0.java') + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text('package org.apache.hugegraph;\npublic class Unique0 {}\n') + with self.assertRaisesRegex(check.CheckFailure, 'Duplicate source FQCN'): + check.check_sources(self.root, set()) + path.unlink() + self.declare_server_module('hugegraph-test') + fixture = (self.root / 'hugegraph-server/hugegraph-test/' + 'src/main/java/org/apache/hugegraph/Unique0.java') + fixture.parent.mkdir(parents=True) + fixture.write_text('package org.apache.hugegraph;\npublic class Unique0 {}\n') + check.check_sources(self.root, set()) + + def test_retired_owner_cannot_return_through_core_dev_sources(self): + self.sources() + path = self.root / check.SOURCE_MODULES[0] / 'src/main/dev/org/apache/hugegraph/Retired.java' + path.parent.mkdir(parents=True) + path.write_text('package org.apache.hugegraph;\npublic class Retired {}\n') + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared implementation'): + check.check_sources(self.root, {'org.apache.hugegraph.Retired'}) + + def test_retired_owner_in_core_jar_is_rejected(self): + retired = check.retired_classes() + for name in ('org.apache.hugegraph.backend.id.Id', 'org.apache.hugegraph.backend.store.Shard'): + with self.subTest(name=name): + self.assertIn(name, retired) + path = self.root / 'core.jar' + entry = name.replace('.', '/') + '$Inner.class' + path.write_bytes(self.jar('hugegraph-core', [entry])) + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared class shipped'): + check.scan_jar(path, str(path), {}, set(), retired, 'server') + + def test_canonical_shard_allowed_and_old_core_owner_rejected(self): + self.sources() + retired = check.retired_classes() + relative = 'src/main/java/org/apache/hugegraph/backend/Shard.java' + canonical = self.root / 'hugegraph-struct' / relative + canonical.parent.mkdir(parents=True) + canonical.write_text('package org.apache.hugegraph.backend;\npublic class Shard {}\n') + check.check_sources(self.root, retired) + old = (self.root / check.SOURCE_MODULES[0] / + 'src/main/java/org/apache/hugegraph/backend/store/Shard.java') + old.parent.mkdir(parents=True) + old.write_text('package org.apache.hugegraph.backend.store;\npublic class Shard {}\n') + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared implementation'): + check.check_sources(self.root, retired) + + def test_retired_nested_source_owner_preserves_entry_and_iterators(self): + self.sources() + retired = check.retired_classes() + owner = 'org.apache.hugegraph.backend.store.BackendEntry$BackendColumn' + self.assertIn(owner, retired) + path = (self.root / check.SOURCE_MODULES[0] / + 'src/main/java/org/apache/hugegraph/backend/store/BackendEntry.java') + path.parent.mkdir(parents=True) + source = '''package org.apache.hugegraph.backend.store; +interface BackendEntry { + String text = "class BackendColumn {}"; + // class BackendColumn {} + Class reference = Other.BackendColumn.class; + interface BackendColumnIterator { class EmptyIterator {} } + class Other { class BackendColumn {} } + default void example() { class BackendColumn {} } +} +''' + path.write_text(source) + check.check_sources(self.root, retired) + for declaration in ('class BackendColumn {}', 'interface BackendColumn {}', + 'enum BackendColumn { VALUE }'): + with self.subTest(declaration=declaration): + path.write_text(source.replace('interface BackendEntry {', + 'interface BackendEntry {\n' + declaration)) + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared implementation'): + check.check_sources(self.root, retired) + + def test_retired_nested_jar_owner_preserves_entry_and_iterators(self): + retired = check.retired_classes() + prefix = 'org/apache/hugegraph/backend/store/BackendEntry' + path = self.root / 'core.jar' + allowed = [prefix + suffix + '.class' for suffix in + ('', '$BackendColumnIterator', '$BackendColumnIterator$EmptyIterator', + '$Other$BackendColumn')] + path.write_bytes(self.jar('hugegraph-core', allowed)) + check.scan_jar(path, str(path), {}, set(), retired, 'server') + for suffix in ('$BackendColumn', '$BackendColumn$Child'): + with self.subTest(suffix=suffix): + path.write_bytes(self.jar('hugegraph-core', allowed + [prefix + suffix + '.class'])) + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared class shipped'): + check.scan_jar(path, str(path), {}, set(), retired, 'server') + + def test_retired_index_sources_preserve_transaction_and_shared_models(self): + self.sources() + retired = check.retired_classes() + self.assertIn('org.apache.hugegraph.structure.HugeIndex', retired) + self.assertIn('org.apache.hugegraph.backend.tx.GraphIndexTransaction$MatchedIndex', retired) + for package, name in (('structure', 'Index'), ('query', 'MatchedIndex')): + path = (self.root / 'hugegraph-struct/src/main/java/org/apache/hugegraph' / + package / (name + '.java')) + path.parent.mkdir(parents=True) + path.write_text(f'package org.apache.hugegraph.{package};\npublic class {name} {{}}\n') + transaction = (self.root / check.SOURCE_MODULES[0] / + 'src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java') + transaction.parent.mkdir(parents=True) + source = '''package org.apache.hugegraph.backend.tx; +class GraphIndexTransaction { + String text = "class MatchedIndex {}"; + class MatchedIndexIterator {} + class Other { class MatchedIndex {} } +} +''' + transaction.write_text(source) + check.check_sources(self.root, retired) + huge_index = (self.root / check.SOURCE_MODULES[0] / + 'src/main/java/org/apache/hugegraph/structure/HugeIndex.java') + huge_index.parent.mkdir(parents=True) + huge_index.write_text('package org.apache.hugegraph.structure;\nclass HugeIndex {}\n') + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared implementation'): + check.check_sources(self.root, retired) + huge_index.unlink() + transaction.write_text(source.replace('class GraphIndexTransaction {', + 'class GraphIndexTransaction {\nprivate static class MatchedIndex {}')) + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared implementation'): + check.check_sources(self.root, retired) + + def test_retired_index_bytecode_including_children_is_rejected(self): + retired = check.retired_classes() + path = self.root / 'core.jar' + transaction = 'org/apache/hugegraph/backend/tx/GraphIndexTransaction' + allowed = [transaction + suffix + '.class' for suffix in + ('', '$MatchedIndexIterator', '$Other$MatchedIndex')] + path.write_bytes(self.jar('hugegraph-core', allowed)) + check.scan_jar(path, str(path), {}, set(), retired, 'server') + shared = self.root / 'struct.jar' + shared.write_bytes(self.jar('hugegraph-struct', ['org/apache/hugegraph/structure/Index.class', + 'org/apache/hugegraph/query/MatchedIndex.class'])) + check.scan_jar(shared, str(shared), {}, set(), retired, 'server') + for name in ('org/apache/hugegraph/structure/HugeIndex', + 'org/apache/hugegraph/structure/HugeIndex$IdWithExpiredTime', + transaction + '$MatchedIndex', transaction + '$MatchedIndex$Child'): + with self.subTest(name=name): + path.write_bytes(self.jar('hugegraph-core', allowed + [name + '.class'])) + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared class shipped'): + check.scan_jar(path, str(path), {}, set(), retired, 'server') + + def test_rpc_interfaces_owned_by_common_not_core_or_rpc_implementation(self): + self.sources() + retired = check.retired_classes() + for name in ('RpcServiceConfig4Client', 'RpcServiceConfig4Server'): + relative = 'src/main/java/org/apache/hugegraph/rpc/' + name + '.java' + source = f'package org.apache.hugegraph.rpc;\npublic interface {name} {{}}\n' + common = self.root / 'hugegraph-commons/hugegraph-common' / relative + common.parent.mkdir(parents=True, exist_ok=True) + common.write_text(source) + self.assertIn('org.apache.hugegraph.rpc.' + name, retired) + check.check_sources(self.root, retired) + for module, message in ((check.SOURCE_MODULES[0], 'Retired shared implementation'), + ('hugegraph-commons/hugegraph-rpc', 'Duplicate source FQCN')): + with self.subTest(name=name, module=module): + duplicate = self.root / module / relative + duplicate.parent.mkdir(parents=True, exist_ok=True) + duplicate.write_text(source) + with self.assertRaisesRegex(check.CheckFailure, message): + check.check_sources(self.root, retired) + duplicate.unlink() + + def test_all_source_gremlin_configs_resolve_current_shared_class_names(self): + current = 'org.apache.hugegraph.id.IdGenerator' + directions = 'org.apache.hugegraph.type.define.Directions' + for relative in check.SOURCE_IMPORT_CONFIGS: + path = self.root / relative + path.parent.mkdir(parents=True) + path.write_text(f'classImports: [{current}, {directions}, java.lang.Math]\n') + check.check_source_imports(self.root, {current, directions}) + path = self.root / check.SOURCE_IMPORT_CONFIGS[-1] + path.write_text('classImports: [org.apache.hugegraph.backend.id.IdGenerator]\n') + with self.assertRaisesRegex(check.CheckFailure, 'class is unavailable'): + check.check_source_imports(self.root, {current, directions}) + path.unlink() + with self.assertRaisesRegex(check.CheckFailure, 'Missing Gremlin import configuration'): + check.check_source_imports(self.root, {current, directions}) + + def test_gremlin_import_reader_flow_and_block_lists(self): + path = self.root / 'gremlin.yaml' + path.write_text('classImports: [\n "example.One", # first import\n\'example.Two\', java.lang.Math\n]\n') + self.assertEqual(check.class_imports(path), ['example.One', 'example.Two', 'java.lang.Math']) + path.write_text('plugins:\n classImports:\n - "example.One"\n - \'example.Two\'\n' + ' methodImports: [example.One#*]\n') + self.assertEqual(check.class_imports(path), ['example.One', 'example.Two']) + for value in ('classImports: *alias\n', 'classImports: [example.One\n', + 'classImports: [example.*]\n', 'classImports: []\n'): + with self.subTest(value=value): + path.write_text(value) + with self.assertRaises(check.CheckFailure): + check.class_imports(path) + + def test_packaged_gremlin_imports_resolve_shared_and_external_classes(self): + path = self.distribution() + (path / 'lib/hugegraph-struct.jar').write_bytes(self.jar('hugegraph-struct', [ + 'org/apache/hugegraph/id/IdGenerator.class', 'org/apache/hugegraph/type/define/Directions.class' + ])) + (path / 'lib/external.jar').write_bytes(self.jar('external', entries={ + 'BOOT-INF/lib/inner.jar': self.jar('external-inner', ['example/External.class']) + })) + config = path / 'conf/gremlin-server.yaml' + config.write_text('classImports: [org.apache.hugegraph.id.IdGenerator,\n' + 'org.apache.hugegraph.type.define.Directions, example.External, java.lang.Math]\n') + check.check_distribution(path, 'server', check.retired_classes()) + config.write_text('classImports: [org.apache.hugegraph.backend.id.IdGenerator]\n') + with self.assertRaisesRegex(check.CheckFailure, 'class is unavailable'): + check.check_distribution(path, 'server', check.retired_classes()) + config.unlink() + with self.assertRaisesRegex(check.CheckFailure, 'Missing Gremlin import configuration'): + check.check_distribution(path, 'server', check.retired_classes()) + + def test_forbidden_dependencies_are_matched_by_exact_group(self): + for module in ('hugegraph-struct', 'hugegraph-commons/hugegraph-common', + 'hugegraph-commons/hugegraph-rpc', 'hugegraph-pd/hg-pd-service', + 'hugegraph-store/hg-store-core'): + artifact = module.rsplit('/', 1)[-1] + with self.subTest(module=module): + tree = self.tree(artifact, '\\- external:hugegraph-core:jar:1.0:compile\n') + check.check_tree(tree, module) + tree = self.tree(artifact, f'\\- {check.GROUP}:hugegraph-core:jar:1.8.0:compile\n') + with self.assertRaisesRegex(check.CheckFailure, 'Forbidden resolved dependency'): + check.check_tree(tree, module) + + def test_struct_and_rpc_framework_dependency_boundaries(self): + module = 'hugegraph-commons/hugegraph-rpc' + self.assertIn(module, check.DEPENDENCY_MODULES) + targets = [('hugegraph-struct', 'hugegraph-rpc')] + targets.extend((module, name) for name in ('hugegraph-core', 'hugegraph-struct', 'hg-pd-client', + 'hg-store-common', 'hugegraph-pd', 'hugegraph-store')) + for caller, dependency in targets: + artifact = caller.rsplit('/', 1)[-1] + for prefix in ('', '+- external:parent:jar:1.0:compile\n| '): + with self.subTest(caller=caller, dependency=dependency, transitive=bool(prefix)): + tree = self.tree(artifact, prefix + f'\\- {check.GROUP}:{dependency}:jar:1.8.0:compile\n') + with self.assertRaisesRegex(check.CheckFailure, 'Forbidden resolved dependency'): + check.check_tree(tree, caller) + check.check_tree(self.tree('hugegraph-rpc', f'\\- {check.GROUP}:hugegraph-common:jar:1.8.0:compile\n'), module) + + def test_stale_project_versions_in_direct_and_nested_distributions_fail(self): + for kind in ('server', 'pd', 'store'): + with self.subTest(kind=kind): + path = self.distribution(kind, nested=kind != 'server') + check.check_distribution(path, kind, set(), expected_revision='1.8.0') + extra = path / 'lib/extra.jar' + stale = self.jar('stale-helper', ['org/apache/hugegraph/Stale.class'], version='1.7.0') + extra.write_bytes(stale) + with self.assertRaisesRegex(check.CheckFailure, 'Stale shipped artifact'): + check.check_distribution(path, kind, set(), expected_revision='1.8.0') + extra.write_bytes(self.jar('boot-helper', entries={'BOOT-INF/lib/stale.jar': stale})) + with self.assertRaisesRegex(check.CheckFailure, 'Stale shipped artifact'): + check.check_distribution(path, kind, set(), expected_revision='1.8.0') + extra.write_bytes(self.jar('hugegraph-core', ['example/External.class'], + group='external', version='9.0')) + check.check_distribution(path, kind, set(), expected_revision='1.8.0') + + def test_retired_sources_cannot_return_in_server_api_or_backends(self): + self.sources() + retired = check.retired_classes() + # Canonical owners preserve their original FQCNs even though those + # implementations were removed from Server; their imports/aliases stay legal. + for module, package, name in (('hugegraph-struct', 'type', 'HugeType'), + ('hugegraph-commons/hugegraph-common', 'auth', 'TokenGenerator')): + path = self.root / module / 'src/main/java/org/apache/hugegraph' / package / (name + '.java') + path.parent.mkdir(parents=True) + path.write_text(f'package org.apache.hugegraph.{package};\npublic class {name} {{}}\n') + check.check_sources(self.root, retired) + for module in ('hugegraph-api', 'hugegraph-hstore'): + self.declare_server_module(module) + path = (self.root / 'hugegraph-server' / module / + 'src/main/java/org/apache/hugegraph/backend/id/Id.java') + path.parent.mkdir(parents=True) + path.write_text('package org.apache.hugegraph.backend.id;\npublic interface Id {}\n') + with self.subTest(module=module), self.assertRaisesRegex(check.CheckFailure, + 'Retired shared implementation'): + check.check_sources(self.root, retired) + path.unlink() + nested = (self.root / 'hugegraph-server' / module / + 'src/main/java/org/apache/hugegraph/backend/store/BackendEntry.java') + nested.parent.mkdir(parents=True) + nested.write_text('package org.apache.hugegraph.backend.store;\n' + 'interface BackendEntry { class BackendColumn {} }\n') + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared implementation'): + check.check_sources(self.root, retired) + nested.write_text('package org.apache.hugegraph.backend.store;\n' + 'interface BackendEntry { String alias = "class BackendColumn {}"; }\n') + check.check_sources(self.root, retired) + nested.unlink() + self.declare_server_module('hugegraph-test') + fixture = (self.root / 'hugegraph-server/hugegraph-test/' + 'src/main/java/org/apache/hugegraph/backend/id/Id.java') + fixture.parent.mkdir(parents=True) + fixture.write_text('package org.apache.hugegraph.backend.id;\ninterface Id {}\n') + check.check_sources(self.root, retired) + + def test_retired_bytecode_cannot_return_in_any_server_artifact(self): + retired = check.retired_classes() + jar = self.root / 'owner.jar' + for artifact in check.SERVER_ARTIFACTS: + for name in ('org/apache/hugegraph/backend/id/Id.class', + 'org/apache/hugegraph/backend/store/BackendEntry$BackendColumn.class'): + with self.subTest(artifact=artifact, name=name): + jar.write_bytes(self.jar(artifact, [name])) + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared class shipped'): + check.scan_jar(jar, str(jar), {}, set(), retired, 'server') + for artifact, name in (('hugegraph-struct', 'org/apache/hugegraph/type/HugeType.class'), + ('hugegraph-common', 'org/apache/hugegraph/auth/TokenGenerator.class'), + ('hugegraph-test', 'org/apache/hugegraph/backend/id/Id.class')): + jar.write_bytes(self.jar(artifact, [name])) + check.scan_jar(jar, str(jar), {}, set(), retired, 'server') + + def test_new_server_module_artifacts_are_included_in_retirement_policy(self): + self.declare_server_module('new-backend') + ids = check.server_artifact_ids(self.root) + self.assertIn('new-backend', ids) + jar = self.root / 'new.jar' + jar.write_bytes(self.jar('new-backend', ['org/apache/hugegraph/backend/id/Id.class'])) + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared class shipped'): + check.scan_jar(jar, str(jar), {}, set(), check.retired_classes(), 'server', server_artifacts=ids) + + def test_nested_profile_module_sources_and_shipped_artifacts_are_checked(self): + self.sources() + aggregator = self.declare_server_module('backend') + aggregator.write_text('' + 'backend-parentfuture-platform' + 'new-backend') + module = aggregator.parent / 'new-backend' + module.mkdir() + (module / 'pom.xml').write_text('' + 'future-server-backend') + identities = check.server_artifact_ids(self.root) + self.assertIn('future-server-backend', identities) + retired = check.retired_classes() + for kind in ('java', 'dev'): + path = module / 'src/main' / kind / 'org/apache/hugegraph/backend/id/Id.java' + path.parent.mkdir(parents=True) + path.write_text('package org.apache.hugegraph.backend.id;\ninterface Id {}\n') + with self.subTest(kind=kind), self.assertRaisesRegex(check.CheckFailure, + 'Retired shared implementation'): + check.check_sources(self.root, retired) + path.unlink() + alias = module / 'src/main/java/org/apache/hugegraph/backend/NewBackend.java' + alias.write_text('package org.apache.hugegraph.backend;\n' + 'class NewBackend { String legacy = "org.apache.hugegraph.backend.id.Id"; }\n') + check.check_sources(self.root, retired) + jar = self.root / 'nested-backend.jar' + data = self.jar('future-server-backend', ['org/apache/hugegraph/backend/id/Id.class']) + for value in (data, self.jar('boot-wrapper', entries={'BOOT-INF/lib/backend.jar': data})): + jar.write_bytes(value) + with self.assertRaisesRegex(check.CheckFailure, 'Retired shared class shipped'): + check.scan_jar(jar, str(jar), {}, set(), retired, 'server', server_artifacts=identities) + + def test_declared_reactor_failures_are_closed(self): + self.sources() + aggregator = self.declare_server_module('backend') + cases = (('missing-child', 'Missing declared Server POM'), + ('../', 'Cyclic Server module declaration'), + ('../../outside-server', 'escapes its reactor'), + ('${unknown.module}', 'nonliteral Server module declaration'), + ('', 'Empty or nonliteral Server module declaration')) + for value, message in cases: + with self.subTest(module=value): + aggregator.write_text('backend-parent' + '' + value + '') + with self.assertRaisesRegex(check.CheckFailure, message): + check.server_module_inventory(self.root) + aggregator.write_text('backend-parent') + with self.assertRaisesRegex(check.CheckFailure, 'Invalid Server module POM'): + check.server_module_inventory(self.root) + aggregator.unlink() + with self.assertRaisesRegex(check.CheckFailure, 'Missing declared Server POM'): + check.server_module_inventory(self.root) + + def test_reactor_inventory_ignores_unlisted_copied_and_target_modules(self): + self.sources() + for relative in ('target/copied-backend', 'apache-hugegraph-server-1.8.0'): + directory = self.root / 'hugegraph-server' / relative + source = directory / 'src/main/dev/org/apache/hugegraph/backend/id/Id.java' + source.parent.mkdir(parents=True) + source.write_text('package org.apache.hugegraph.backend.id;\ninterface Id {}\n') + (directory / 'pom.xml').write_text('unlisted-copy') + check.check_sources(self.root, check.retired_classes()) + self.assertNotIn('unlisted-copy', check.server_artifact_ids(self.root)) + + def test_profile_redeclarations_deduplicate_and_test_subtree_is_excluded(self): + self.sources() + test = self.declare_server_module('hugegraph-test') + test.write_text('hugegraph-test' + 'fixtures') + child = test.parent / 'fixtures' + child.mkdir() + (child / 'pom.xml').write_text('test-fixtures') + path = child / 'src/main/java/org/apache/hugegraph/backend/id/Id.java' + path.parent.mkdir(parents=True) + path.write_text('package org.apache.hugegraph.backend.id;\ninterface Id {}\n') + root_pom = self.root / 'hugegraph-server/pom.xml' + project = ET.parse(root_pom) + namespace = '{http://maven.apache.org/POM/4.0.0}' + profiles = ET.SubElement(project.getroot(), namespace + 'profiles') + profile = ET.SubElement(profiles, namespace + 'profile') + modules = ET.SubElement(profile, namespace + 'modules') + ET.SubElement(modules, namespace + 'module').text = './hugegraph-core' + project.write(root_pom, encoding='unicode') + check.check_sources(self.root, check.retired_classes()) + identities = check.server_artifact_ids(self.root) + self.assertEqual(identities, {'hugegraph-server', 'hugegraph-core'}) + + +if __name__ == '__main__': + unittest.main() diff --git a/pom.xml b/pom.xml index 65dee5a154..6e6134b9a0 100644 --- a/pom.xml +++ b/pom.xml @@ -86,18 +86,39 @@ + 4.1.5 5.6.0 - 1.7.0 + 1.8.0 + 4.0.25 + 2.15.2 + 4.1.137.Final + 1.55.3 + 3.22.3 + 2.2 + 2.5.15 + 5.3.27 1.18.30 - - 1.3.11 - 1.3.13 + + 1.3.14 hugegraph - 11 - 11 + 17 + + [17,18) + 3.15.0 + 3.5.6 + + false + 0.8.15 + 3.6.2 + UTF-8 bash + 1.5.0 + 8.10.2 @@ -112,6 +133,71 @@ + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + com.google.protobuf + protobuf-bom + ${protobuf.runtime.version} + pom + import + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + org.apache.groovy + groovy-bom + ${groovy.version} + pom + import + + + io.netty + netty-bom + ${netty.version} + pom + import + + + org.springframework + spring-framework-bom + ${spring-framework.version} + pom + import + + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot.version} + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + org.apache.ivy + ivy + 2.6.0 + org.projectlombok lombok @@ -196,6 +282,7 @@ **/pd/grpc/**/*.java **/store/grpc/**/*.java **/target/** + **/apache-hugegraph-*/lib/arthas/logback.xml dist/**/* .github/**/* @@ -228,35 +315,13 @@ org.apache.maven.plugins maven-enforcer-plugin - - - enforce-version - - enforce - - - false - - - - - - [11,) - - - [3.5.0,) - - - - - + ${maven.enforcer.plugin.version} maven-compiler-plugin - 3.1 + ${maven.compiler.plugin.version} - ${maven.compiler.source} - ${maven.compiler.target} + ${maven.compiler.release} 500 @@ -265,6 +330,21 @@ + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.plugin.version} + + + ${surefire.failIfNoSpecifiedTests} + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.maven.plugin.version} + maven-remote-resources-plugin 3.3.0 @@ -277,6 +357,56 @@ + + + org.codehaus.mojo + flatten-maven-plugin + 1.3.0 + + true + resolveCiFriendliesOnly + + + + flatten + process-resources + + flatten + + + + flatten.clean + clean + + clean + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-version + + enforce + + + + + + + ${java.supported.version.range} + + + [3.6.3,) + + + + + + org.apache.rat apache-rat-plugin