diff --git a/.build/build-rat.xml b/.build/build-rat.xml index 244daa00f2..1d3f1e6c1c 100644 --- a/.build/build-rat.xml +++ b/.build/build-rat.xml @@ -78,7 +78,7 @@ - + diff --git a/.build/run-ci b/.build/run-ci index 34073e0b63..7dbf733c27 100755 --- a/.build/run-ci +++ b/.build/run-ci @@ -25,7 +25,7 @@ Python dependencies are found in .build/run-ci.d/requirements.txt Custom environment variables can be set in .build/.run-ci.env lint with: - `pylint --disable=C0301,W0511,C0103,W0702,C0415,C0116,C0115,R0914,W0603,R0915,R0913,R0911 run-ci` + `pylint --disable=C0301,W0511,C0103,W0702,C0415,C0116,C0115,R0914,W0603,R0915,R0913,R0917,R0911,W0212,W0621 run-ci` test with: `python .build/run-ci.d/run-ci-test.py` @@ -38,6 +38,7 @@ import gzip import itertools import os import shutil +import socket import subprocess import sys import tarfile @@ -50,9 +51,10 @@ from urllib.request import urlretrieve from typing import Optional, Tuple # External Libraries (`pip install -r .build/run-ci.d/requirements.txt`) +import requests +import yaml from bs4 import BeautifulSoup from kubernetes import client, config, stream -import requests try: import jenkins @@ -72,9 +74,9 @@ def base_job_name(args) -> str: """ if not hasattr(base_job_name, "_cached_result"): raw_url = args.repository.replace("https://github.com/", "https://raw.githubusercontent.com/").removesuffix(".git") + f"/{args.branch}/build.xml" - if 200 != requests.head(raw_url).status_code: + if 200 != requests.head(raw_url, timeout=30).status_code: raise ValueError(f"GitHub unavailable, or this branch has not been pushed yet: {args.repository} @ {args.branch} (or remote tracking not setup up: `git config --get branch.{args.branch}.remote` and `git config --get branch.{args.branch}.merge`)") - response = requests.get(raw_url) + response = requests.get(raw_url, timeout=30) response.raise_for_status() for line in response.text.splitlines(): if 'property' in line and 'name="base.version"' in line: @@ -99,7 +101,7 @@ def is_local_git_dirty(args) -> bool: # use base_job_name to verify the remote branch exists base_job_name(args) # check if the working directory is clean - clean = subprocess.run(["git", "-C", str(CASSANDRA_DIR), "diff-index", "--quiet", "HEAD", "--"]).returncode + clean = subprocess.run(["git", "-C", str(CASSANDRA_DIR), "diff-index", "--quiet", "HEAD", "--"], check=False).returncode # check if there are unpushed committed changes unpushed_commits = bool(subprocess.run(["git", "-C", str(CASSANDRA_DIR), "log", "@{u}..HEAD", "--name-only"], capture_output=True, text=True, check=False).stdout.strip()) @@ -192,6 +194,7 @@ def argument_parser() -> argparse.ArgumentParser: parser.add_argument("-k", "--dtest-branch", default=DEFAULT_DTEST_REPO_BRANCH, help="DTest repository branch.") parser.add_argument("-s", "--setup", action="store_true", help="Set up Jenkins before the build.") parser.add_argument("--only-setup", action="store_true", help="Only install Jenkins into the k8s cluster.") + parser.add_argument("-f", "--values-override", help="Path to an additional helm values file, applied over .jenkins/k8s/jenkins-deployment.yaml. Required when the target cluster carries site customisations, see .jenkins/k8s/README.md") parser.add_argument("--tear-down", action="store_true", help="Tear down Jenkins after the build.") parser.add_argument("--only-tear-down", action="store_true", help="Only tear down Jenkins.") parser.add_argument("--only-node-cleaner", action="store_true", help="Only run the node cleaner. The node cleaner scans the k8s nodes, eagerly terminating those unused.") @@ -212,6 +215,8 @@ def parse_arguments() -> argparse.Namespace: assert not (args.setup and args.only_setup), "Both --setup or --only-setup cannot be specified." assert not (args.tear_down and args.only_tear_down), "Both --tear-down or --only-tear-down cannot be specified." assert not ("custom" == args.profile and not args.profile_custom_regexp), "Custom profile requires --profile-custom-regexp." + assert not (args.values_override and not (args.setup or args.only_setup)), "--values-override requires --setup or --only-setup." + assert not (args.values_override and not Path(args.values_override).is_file()), f"No such values override file: {args.values_override}" if not args.url and os.environ.get("JENKINS_URL"): args.url = os.environ.get("JENKINS_URL") @@ -253,19 +258,113 @@ def run_kubectl_command(kubeconfig: Optional[str], kubecontext: Optional[str], k cmd += command return subprocess.run(cmd, capture_output=True, text=True, check=True).stdout.strip() -def install_jenkins(kubeconfig: Optional[str], kubecontext: Optional[str], kube_ns: str): - """Installs Jenkins Operator using Helm in the specified K8s namespace.""" - print("Adding Helm repository for Jenkins Operator...") - subprocess.run(["helm", "repo", "add", "jenkins", "https://charts.jenkins.io"], check=True) - subprocess.run(["helm", "repo", "update"], check=True) - +def run_helm_command(kubeconfig: Optional[str], kubecontext: Optional[str], kube_ns: str, command: list, + capture_output: bool = True, check: bool = True) -> subprocess.CompletedProcess: + """Runs a helm command with the specified kubeconfig, context and namespace.""" cmd = ["helm"] if kubeconfig: cmd += ["--kubeconfig", kubeconfig] if kubecontext: cmd += ["--kube-context", kubecontext] - cmd += ["--namespace", kube_ns, "upgrade", "--install", "-f", DEPLOY_YAML, "cassius", "jenkins/jenkins", "--wait"] - result = subprocess.run(cmd, capture_output=True, check=True) + cmd += ["--namespace", kube_ns] + cmd += command + return subprocess.run(cmd, capture_output=capture_output, text=True, check=check) + +def install_jenkins(kubeconfig: Optional[str], kubecontext: Optional[str], kube_ns: str, + values_override: Optional[str] = None): + """Installs Jenkins Operator using Helm in the specified K8s namespace.""" + + def confirm_helm_updates(): + """Prompts before an upgrade drops any values the deployed jenkins currently has.""" + + def helm_values() -> dict: + """ + The values a deployed jenkins was last installed with, or an empty dict when there is no release. + These are the user-supplied values, whatever files they came from, so they include any customisations + made to the site outside of `.jenkins/k8s/jenkins-deployment.yaml`. + """ + result = run_helm_command(kubeconfig, kubecontext, kube_ns, + ["get", "values", "cassius", "-o", "yaml"], check=False) + if result.returncode != 0: + debug(f"No existing cassius release found in namespace {kube_ns}: {result.stderr.strip()}") + return {} + return yaml.safe_load(result.stdout) or {} + + def merge_values(base: dict, override: dict) -> dict: + """Merges two helm values files the way helm does: maps key by key, everything else replaced.""" + merged = dict(base) + for key, value in override.items(): + if isinstance(value, dict) and isinstance(merged.get(key), dict): + merged[key] = merge_values(merged[key], value) + else: + merged[key] = value + return merged + + def detect_lost_values(live: dict, proposed: dict) -> dict: + """ + Values the deployed jenkins has that an upgrade would drop, as {dotted.key.path: live value}. + + A key held live but absent from what is about to be applied is either a customisation made to this site, + or a key that `.jenkins/k8s/jenkins-deployment.yaml` has removed since the site was last deployed. + Note also what this cannot see: a key that exists in both but was given a different value locally, such + as an edited `agent.podTemplates` entry, is silently overwritten. + Read the diff of `helm template` before deploying an unfamiliar site. + """ + def leaf_values(values, path: str = "") -> dict: + """Flattens a values map to {dotted.key.path: value}, lists are leaves (helm replaces them).""" + if not isinstance(values, dict): + return {path: values} + leaves = {} + for key, value in values.items(): + leaves.update(leaf_values(value, f"{path}.{key}" if path else str(key))) + return leaves + + live_leaves, proposed_leaves = leaf_values(live), leaf_values(proposed) + lost = {path: value for path, value in live_leaves.items() if path not in proposed_leaves} + # lists are replaced wholesale, so also report items dropped from a list that is otherwise still there + for path, value in live_leaves.items(): + if isinstance(value, list) and isinstance(proposed_leaves.get(path), list): + dropped = [item for item in value if item not in proposed_leaves[path]] + if dropped: + lost[f"{path}[]"] = dropped + return lost + + with open(DEPLOY_YAML, encoding="utf-8") as deploy_yaml: + proposed = yaml.safe_load(deploy_yaml) or {} + if values_override: + with open(values_override, encoding="utf-8") as override_yaml: + proposed = merge_values(proposed, yaml.safe_load(override_yaml) or {}) + + lost = detect_lost_values(helm_values(), proposed) + if not lost: + return + + print(f"\nWARNING: {len(lost)} value(s) the deployed jenkins has are absent from what is about to be applied.") + print("Each is either a customisation of this site, or a key removed from jenkins-deployment.yaml since" + " the site was last deployed. Upgrading drops them:\n") + for path in sorted(lost): + value = str(lost[path]).replace("\n", " ") + print(f" {path}: {value[:100] + '…' if len(value) > 100 else value}") + print(f"\nTo keep any of them, add them to a values override file (see .jenkins/k8s/README.md) and pass" + f" `--values-override`{' (the file passed does not contain them)' if values_override else ''}.") + + if not sys.stdin.isatty(): + print("Refusing to drop them when running non-interactively.") + sys.exit(1) + if input("\nDrop these values and continue? [y/N] ").strip().lower() not in ("y", "yes"): + print("Aborted, nothing was deployed.") + sys.exit(1) + + confirm_helm_updates() + + print("Adding Helm repository for Jenkins Operator...") + subprocess.run(["helm", "repo", "add", "jenkins", "https://charts.jenkins.io"], check=True) + subprocess.run(["helm", "repo", "update"], check=True) + + # site customisations are applied last, helm merges each -f over the previous + values_files = ["-f", DEPLOY_YAML] + (["-f", values_override] if values_override else []) + result = run_helm_command(kubeconfig, kubecontext, kube_ns, + ["upgrade", "--install"] + values_files + ["cassius", "jenkins/jenkins", "--wait"]) run_kubectl_command(kubeconfig, kubecontext, kube_ns, ["exec", DEFAULT_POD_NAME, "--", @@ -277,6 +376,19 @@ def install_jenkins(kubeconfig: Optional[str], kubecontext: Optional[str], kube_ sys.exit(1) +def wait_for_jenkins_http(ip: str): + host, port = (ip.rsplit(":", 1)[0], int(ip.rsplit(":", 1)[1])) if ":" in ip else (ip, 80) + spin_while(f"Waiting for Jenkins HTTP at {host}:{port}… ", lambda: _tcp_connect_ok(host, port)) + + +def _tcp_connect_ok(host: str, port: int) -> bool: + try: + with socket.create_connection((host, port), timeout=2): + return True + except OSError: + return False + + def get_jenkins(k8s_client: client.CoreV1Api, args, kube_ns: str) -> Tuple[str, jenkins.Jenkins]: """Authenticates to Jenkins and returns the Jenkins ip and server objects.""" @@ -782,13 +894,10 @@ def cleanup_and_maybe_teardown(kubeconfig: Optional[str], kubecontext: Optional[ IS_RUNNING = False if tear_down: print("Cleaning up Jenkins and all resources.") - cmd = ["helm"] - if kubeconfig: - cmd += ["--kubeconfig", kubeconfig] - if kubecontext: - cmd += ["--kube-context", kubecontext] - cmd += ["--namespace", kube_ns, "uninstall", "cassius"] - subprocess.run(cmd, check=True) + run_helm_command(kubeconfig, kubecontext, kube_ns, ["uninstall", "cassius"], capture_output=False) + # the pvc is annotated `helm.sh/resource-policy: keep`, see .jenkins/k8s/jenkins-deployment.yaml + print(f"Jenkins uninstalled. The jenkins-home volume was kept, delete it with:\n" + f" kubectl --namespace {kube_ns} delete pvc cassius-jenkins") @contextmanager @@ -826,10 +935,11 @@ def main(): if args.setup or args.only_setup: init_k8s_namespace(k8s_client, DEFAULT_KUBE_NS) with helm_installation_lock(Path("/tmp/.cassandra-run-ci.lock")): - install_jenkins(args.kubeconfig, args.kubecontext, DEFAULT_KUBE_NS) + install_jenkins(args.kubeconfig, args.kubecontext, DEFAULT_KUBE_NS, args.values_override) (ip, server) = get_jenkins(k8s_client, args, DEFAULT_KUBE_NS) if args.setup or args.only_setup: + wait_for_jenkins_http(ip) ensure_cassandra_job_parameters_visible(server) if args.only_setup: return diff --git a/.build/run-ci.d/README.md b/.build/run-ci.d/README.md index b8f65b6cbc..76780aa084 100644 --- a/.build/run-ci.d/README.md +++ b/.build/run-ci.d/README.md @@ -3,7 +3,7 @@ ``` ➤ .build/run-ci --help usage: run-ci [-h] [-c KUBECONFIG] [-x KUBECONTEXT] [-i URL] [-u USER] [-r REPOSITORY] [-b BRANCH] [-p {packaging,skinny,pre-commit,pre-commit w/ upgrades,post-commit,custom}] [-e PROFILE_CUSTOM_REGEXP] [-j JDK] [-d DTEST_REPOSITORY] [-k DTEST_BRANCH] - [-s] [--only-setup] [--tear-down] [--only-tear-down] [--only-node-cleaner] [-o DOWNLOAD_RESULTS] + [-s] [--only-setup] [-v VALUES_OVERRIDE] [--tear-down] [--only-tear-down] [--only-node-cleaner] [-o DOWNLOAD_RESULTS] Run CI pipeline for Cassandra on K8s using Jenkins. @@ -30,6 +30,8 @@ options: DTest repository branch. -s, --setup Set up Jenkins before the build. --only-setup Only install Jenkins into the k8s cluster. + -v VALUES_OVERRIDE, --values-override VALUES_OVERRIDE + Path to an additional helm values file, applied over .jenkins/k8s/jenkins-deployment.yaml. Required when the target cluster carries site customisations, see .jenkins/k8s/README.md --tear-down Tear down Jenkins after the build. --only-tear-down Only tear down Jenkins. --only-node-cleaner Only run the node cleaner. The node cleaner scans the k8s nodes, eagerly terminating those unused. @@ -63,7 +65,15 @@ Setup/Update Jenkins Helm into your current kubeconfig .build/run-ci --only-setup ``` -Uninstall Jenkins from your current kubeconfig +Setup/Update Jenkins Helm into a cluster that carries site customisations, e.g. pre-ci.cassandra.apache.org +``` +.build/run-ci --only-setup --values-override ~/.cassandra-ci/pre-ci-overrides.yaml +``` + +Before any setup, the values already deployed are compared against those about to be applied. Any value the deployed jenkins holds that the new files lack is listed, and confirmation is asked for before it is dropped; running non-interactively aborts instead. See `.jenkins/k8s/README.md` for what this can and cannot catch. + +Uninstall Jenkins from your current kubeconfig. ``` .build/run-ci --only-tear-down -``` \ No newline at end of file +``` +The jenkins-home volume is kept; delete it separately with `kubectl delete pvc cassius-jenkins` \ No newline at end of file diff --git a/.build/run-ci.d/requirements.txt b/.build/run-ci.d/requirements.txt index 420232f16f..ff11accd3a 100644 --- a/.build/run-ci.d/requirements.txt +++ b/.build/run-ci.d/requirements.txt @@ -18,6 +18,7 @@ bs4 dotenv kubernetes python-jenkins +pyyaml requests # optional for different clouds diff --git a/.build/run-ci.d/run-ci-test.py b/.build/run-ci.d/run-ci-test.py index b6c73ebd2a..c878383229 100644 --- a/.build/run-ci.d/run-ci-test.py +++ b/.build/run-ci.d/run-ci-test.py @@ -27,12 +27,14 @@ import argparse from pathlib import Path +import tempfile import unittest from unittest.mock import patch, MagicMock # Import the functions from the script from run_ci import ( + DEPLOY_YAML, debug, install_jenkins, get_jenkins, @@ -55,13 +57,84 @@ class TestCIPipeline(unittest.TestCase): debug("Test message") mock_print.assert_called_with("Test message") + # the pre-flight check is nested inside install_jenkins, so it is exercised through it: the mocked + # `helm get values` stdout stands in for the values the site already has deployed + LIVE_STORAGE_CLASS = "persistence:\n storageClass: gp2\n" + @patch('run_ci.subprocess.run') def test_install_jenkins(self, mock_run): - mock_run.return_value = MagicMock(returncode=0) + # empty stdout, i.e. nothing deployed yet, so the pre-flight check has nothing to warn about + mock_run.return_value = MagicMock(returncode=0, stdout="") install_jenkins("test-namespace", Path("/fake/cassandra/dir"), "default") mock_run.assert_any_call(["helm", "repo", "add", "jenkins", "https://charts.jenkins.io"], check=True) mock_run.assert_any_call(["helm", "repo", "update"], check=True) + @patch('run_ci.subprocess.run') + def test_install_jenkins_values_override(self, mock_run): + mock_run.return_value = MagicMock(returncode=0, stdout="") + with tempfile.NamedTemporaryFile("w", suffix=".yaml") as override: + override.write(self.LIVE_STORAGE_CLASS) + override.flush() + install_jenkins(None, None, "default", override.name) + upgrade_cmd = [c for c in [call.args[0] for call in mock_run.call_args_list] if "upgrade" in c][0] + # the site's overrides must come after, and never replace, the repo's deployment yaml + self.assertEqual(["-f", DEPLOY_YAML, "-f", override.name], upgrade_cmd[5:9]) + + @patch('run_ci.sys.stdin.isatty') + @patch('run_ci.print') + @patch('run_ci.subprocess.run') + def test_install_jenkins_aborts_non_interactively(self, mock_run, mock_print, mock_isatty): + mock_isatty.return_value = False + mock_run.return_value = MagicMock(returncode=0, stdout=self.LIVE_STORAGE_CLASS) + with self.assertRaises(SystemExit): + install_jenkins(None, None, "default") + self.assertEqual([], [c for c in [call.args[0] for call in mock_run.call_args_list] if "upgrade" in c]) + # and continues when the customisation is passed back in as an override. This also proves the merge is + # per key: were the override to replace the whole persistence map, its other keys would now be reported lost + with tempfile.NamedTemporaryFile("w", suffix=".yaml") as override: + override.write(self.LIVE_STORAGE_CLASS) + override.flush() + install_jenkins(None, None, "default", override.name) + self.assertEqual(1, len([c for c in [call.args[0] for call in mock_run.call_args_list] if "upgrade" in c])) + + @patch('run_ci.input') + @patch('run_ci.sys.stdin.isatty') + @patch('run_ci.print') + @patch('run_ci.subprocess.run') + def test_install_jenkins_prompts(self, mock_run, mock_print, mock_isatty, mock_input): + mock_isatty.return_value = True + mock_run.return_value = MagicMock(returncode=0, stdout=self.LIVE_STORAGE_CLASS) + mock_input.return_value = "n" + with self.assertRaises(SystemExit): + install_jenkins(None, None, "default") + mock_input.return_value = "y" + install_jenkins(None, None, "default") + + @patch('run_ci.sys.stdin.isatty') + @patch('run_ci.print') + @patch('run_ci.subprocess.run') + def test_install_jenkins_reports_only_detectable_losses(self, mock_run, mock_print, mock_isatty): + mock_isatty.return_value = False + # a plugin only this site installs is reported, as is a key the site alone holds; a key held in both but + # locally edited (persistence.size) cannot be seen, and must not be claimed + mock_run.return_value = MagicMock(returncode=0, stdout="persistence:\n size: 1Ti\n" + "controller:\n installPlugins:\n - site-only-plugin\n") + with self.assertRaises(SystemExit): + install_jenkins(None, None, "default") + printed = " ".join(str(call.args[0]) for call in mock_print.call_args_list if call.args) + self.assertIn("controller.installPlugins[]", printed) + self.assertIn("site-only-plugin", printed) + self.assertNotIn("persistence.size", printed) + + @patch('run_ci.print') + @patch('run_ci.subprocess.run') + def test_install_jenkins_when_nothing_deployed(self, mock_run, mock_print): + # `helm get values` fails when there is no release, and nothing is then warned about + mock_run.side_effect = lambda cmd, **kwargs: MagicMock(returncode=1 if "get" in cmd else 0, + stdout="", stderr="release: not found") + install_jenkins(None, None, "default") + self.assertEqual([], [call.args[0] for call in mock_print.call_args_list if "WARNING" in str(call.args)]) + @patch('run_ci.subprocess.run') @patch('run_ci.jenkins.Jenkins') def test_get_jenkins(self, mock_jenkins, mock_run): @@ -91,13 +164,14 @@ class TestCIPipeline(unittest.TestCase): @patch('run_ci.stream.stream') def test_delete_remote_junit_files(self, mock_stream): mock_k8s_client = MagicMock() - delete_remote_junit_files(mock_k8s_client, "test-pod", "test-namespace", 456) + delete_remote_junit_files(mock_k8s_client, "test-pod", "test-namespace", "test-job", 456) mock_stream.assert_called() @patch('run_ci.subprocess.run') def test_cleanup_and_maybe_teardown(self, mock_run): cleanup_and_maybe_teardown(None, None, "test-namespace", True) - mock_run.assert_called_with(["helm", "--namespace", "test-namespace", "uninstall", "cassius"], check=True) + mock_run.assert_called_with(["helm", "--namespace", "test-namespace", "uninstall", "cassius"], + capture_output=False, text=True, check=True) @patch('run_ci.fcntl.flock') def test_helm_installation_lock(self, mock_flock): diff --git a/.github/workflows/jenkins-check.yaml b/.github/workflows/jenkins-check.yaml new file mode 100644 index 0000000000..586325d0a7 --- /dev/null +++ b/.github/workflows/jenkins-check.yaml @@ -0,0 +1,58 @@ +name: CI Check + +# Checks the CI tooling itself: the run-ci script and everything under .jenkins/ +# Nothing here touches a k8s cluster; the helm and groovy steps only render and parse. + +on: + push: + branches: + - '**' + paths: + - '.build/run-ci' + - '.build/run-ci.d/**' + - '.jenkins/**' + - '.github/workflows/jenkins-check.yaml' + pull_request: + paths: + - '.build/run-ci' + - '.build/run-ci.d/**' + - '.jenkins/**' + - '.github/workflows/jenkins-check.yaml' + +jobs: + run-ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Python 3 + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Install dependencies + run: pip install pylint -r .build/run-ci.d/requirements.txt + - name: Lint run-ci + run: pylint --disable=C0301,W0511,C0103,W0702,C0415,C0116,C0115,R0914,W0603,R0915,R0913,R0917,R0911,W0212,W0621 .build/run-ci + - name: Run run-ci unit tests + run: python .build/run-ci.d/run-ci-test.py + - name: Check run-ci argument parsing + run: python .build/run-ci --help + + jenkins-validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Python 3 + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Install dependencies + run: pip install pyyaml + - name: Helm + uses: azure/setup-helm@v4 + # validate.sh runs groovy from docker when it is not on the PATH + - name: Validate .jenkins/ + run: .jenkins/k8s/jenkins-test.sh diff --git a/.jenkins/Jenkinsfile b/.jenkins/Jenkinsfile index 7f80fd98d3..c9ea3bf633 100644 --- a/.jenkins/Jenkinsfile +++ b/.jenkins/Jenkinsfile @@ -355,7 +355,7 @@ def build(command, cell) { ws("workspace/${JOB_NAME}/${BUILD_NUMBER}/${cell.step}/${cell.arch}/jdk-${cell.jdk}") { try { fetchSource(cell.step, cell.arch, cell.jdk) - sh """ + sh label: "checking Jenkinsfile validity", script: """ test -f .jenkins/Jenkinsfile || { echo "Invalid git fork/branch"; exit 1; } grep -q "Jenkins CI declaration" .jenkins/Jenkinsfile || { echo "Only Cassandra 5.0+ supported"; exit 1; } """ @@ -373,7 +373,7 @@ def build(command, cell) { } if (0 != status) { error("Stage ${cell.step}${cell_suffix} failed with exit status ${status}") } if ("jar" == cell.step) { - stash name: "${cell.arch}_${cell.jdk}" + _stash(cell) } } catch (exc) { if ("org.jenkinsci.plugins.workflow.steps.FlowInterruptedException" == exc.getClass().getName()) { @@ -438,8 +438,8 @@ def test(command, cell) { def descriptions = [] for (def cause in exc.getCauses()) { echo "CauseOfInterruption: ${cause.getClass().getName()} - ${cause.getShortDescription()}" - if (cause.getClass().getName().contains('CauseOfInterruption$UserInterruption')) { - throw exc // user explicitly aborted — do not retry + if (cause.getClass().getName().contains('CauseOfInterruption$UserInterruption') || cause.getClass().getName().contains('ParallelStep$FailFastCause')) { + throw exc // user abort or fail-fast — do not retry } descriptions.add(cause.getShortDescription()) } @@ -453,24 +453,12 @@ def test(command, cell) { } } dir("build") { - sh """ - mkdir -p test/output/${cell.step} - find test/output -type f -name "TEST*.xml" -execdir mkdir -p jdk_${cell.jdk}/${cell.arch} ';' -execdir mv {} jdk_${cell.jdk}/${cell.arch}/{} ';' - find test/output -name cqlshlib.xml -execdir mv cqlshlib.xml ${cell.step}/cqlshlib${cell_suffix}.xml ';' - find test/output -name nosetests.xml -execdir mv nosetests.xml ${cell.step}/nosetests${cell_suffix}.xml ';' - """ + organiseTestResultFiles(cell, cell_suffix) if (!cell.step.startsWith("microbench")) { junit testResults: "test/**/TEST-*.xml,test/**/cqlshlib*.xml,test/**/nosetests*.xml", testDataPublishers: [[$class: 'StabilityTestDataPublisher']] } - // check if we had Linux OOM killer active within the test container which could kill forked JUnit JVM processes - sh """ - echo "docker memory/oomkiller debug:" - cat /sys/fs/cgroup/docker/memory.events || true - """ - sh """ - find test/output -type f -name "*.xml" -print0 | xargs -0 -r -n1 -P"\$(nproc)" xz -f - echo "test result files compressed"; find test/output -type f -name "*.xml.xz" | wc -l - """ + debugOomKiller() + compressTestResultFiles() archiveArtifacts artifacts: "test/logs/**,test/**/TEST-*.xml.xz,test/**/cqlshlib*.xml.xz,test/**/nosetests*.xml.xz,test/**/jmh-result.json", fingerprint: true copyToNightlies("${logfile},test/logs/**,test/**/jmh-result.json", "${cell.step}/${cell.arch}/jdk${cell.jdk}/python${cell.python}/cython_${cell.cython}/" + "split_${cell.split}_${splits}".replace("/", "_")) } @@ -517,7 +505,7 @@ def fetchDockerImages(dockerfiles) { // prefetch, from apache jfrog, reduces risking dockerhub pull rate limits // also prefetch alpine:latest as its used as a utility in the scripts def dockerfilesVar = dockerfiles.join(' ') - sh """#!/bin/bash + sh label: "fetching docker images...", script: """#!/bin/bash for dockerfile in ${dockerfilesVar} ; do image_tag="\$(md5sum .build/docker/\${dockerfile}.docker | cut -d' ' -f1)" image_name="apache/cassandra-\${dockerfile}:\${image_tag}" @@ -527,6 +515,14 @@ def fetchDockerImages(dockerfiles) { done docker pull -q apache.jfrog.io/cassan-docker/alpine:3.19.1 & wait + # debug how much space the images have taken. df reports the node's filesystem: an emptyDir has no + # size of its own, so this sees node pressure coming, never the docker-storage sizeLimit + { set +x; } 2>/dev/null + free_gib=\$(df --output=avail -B 1073741824 / | tail -1 | tr -d ' ') + echo "docker images total: \$(docker system df --format '{{.Size}}' | head -1) (node free: \${free_gib}Gi)" + if [ "\${free_gib}" -le 10 ] ; then + echo "WARNING: only \${free_gib}Gi free on the node after pulling images — review the agents' ephemeral-storage budget in .jenkins/k8s/jenkins-deployment.yaml" + fi """ } @@ -564,15 +560,15 @@ def copyToNightlies(sourceFiles, remoteDirectory='') { def cleanAgent(job_name) { // get any public IP which is more helpful correlating back to the cloud instance sh script: 'hostname; curl -sm 10 ifconfig.me', returnStatus: true + def agentScriptsUrl = "https://raw.githubusercontent.com/apache/cassandra-builds/trunk/jenkins-dsl/agent_scripts/" if (isCanonical()) { - def agentScriptsUrl = "https://raw.githubusercontent.com/apache/cassandra-builds/trunk/jenkins-dsl/agent_scripts/" cleanAgentDocker(job_name, agentScriptsUrl) - logAgentInfo(job_name, agentScriptsUrl) } + logAgentInfo(job_name, agentScriptsUrl) cleanWs() if (isCanonical()) { // in the workspace prune any abandoned or uncleaned builds (CASSANDRA-20436) - sh """#!/bin/bash + sh label: "prune abandoned and uncleaned workspace builds files...", script: """#!/bin/bash set +e find /home/jenkins/jenkins-*/workspace/ -mindepth 2 -maxdepth 2 -type d -regextype posix-extended -regex '.*/[0-9]+' -mtime +31 -print -exec rm -rf {} + """ @@ -582,8 +578,7 @@ def cleanAgent(job_name) { def cleanAgentDocker(job_name, agentScriptsUrl) { // we don't expect any build to have been running for longer than maxBuildHours def maxBuildHours = 12 - echo "Pruning docker for '${job_name}' on ${NODE_NAME}…" ; - sh """#!/bin/bash + sh label: "Pruning docker for '${job_name}' on ${NODE_NAME}...", script: """#!/bin/bash set +e wget -q ${agentScriptsUrl}/docker_image_pruner.py wget -q ${agentScriptsUrl}/docker_agent_cleaner.sh @@ -592,12 +587,58 @@ def cleanAgentDocker(job_name, agentScriptsUrl) { } def logAgentInfo(job_name, agentScriptsUrl) { - sh """#!/bin/bash - set +e -o pipefail - wget -q ${agentScriptsUrl}/agent_report.sh - bash -x agent_report.sh | tee -a \$(date +"%Y%m%d%H%M")-disk-usage-stats.txt - """ - copyToNightlies("*-disk-usage-stats.txt", "cassandra/ci-cassandra.apache.org/agents/${NODE_NAME}/disk-usage/") + // post-run remaining build/ and docker usage. used to validate the agents' ephemeral-storage budget + sh label: "log build usage...", script: """ + { set +x; } 2>/dev/null + du -sh ${WORKSPACE}/build/m2 ${WORKSPACE}/build/tmp ${WORKSPACE}/build/test ${WORKSPACE}/build 2>/dev/null || true + df -h / || true + """ + if (isCanonical()) { + sh label: "running agent_report.sh for disk usage stats (and more)...", script: """#!/bin/bash + set +e -o pipefail + wget -q ${agentScriptsUrl}/agent_report.sh + bash -x agent_report.sh | tee -a \$(date +"%Y%m%d%H%M")-disk-usage-stats.txt + """ + copyToNightlies("*-disk-usage-stats.txt", "cassandra/ci-cassandra.apache.org/agents/${NODE_NAME}/disk-usage/") + } +} + +def _stash(cell) { + sh label: "check stash size...", script: """ + { set +x; } 2>/dev/null + free_gib=\$(df --output=avail -B 1073741824 / | tail -1 | tr -d ' ') + stash_gb=\$(du -sb ${WORKSPACE} | awk '{printf "%.1f", \$1/1024/1024/1024}') + echo "stash size: \${stash_gb}G (node free: \${free_gib}Gi)" + if [ "\${free_gib}" -le 10 ] ; then + echo "WARNING: only \${free_gib}Gi free on the node after building stash (\${stash_gb}G) — review jnlp's resourceLimitEphemeralStorage in .jenkins/k8s/jenkins-deployment.yaml" + fi + """ + stash name: "${cell.arch}_${cell.jdk}" +} + +def organiseTestResultFiles(cell, cell_suffix) { + sh label: "organise test result files...", script: """ + mkdir -p test/output/${cell.step} + find test/output -type f -name "TEST*.xml" -execdir mkdir -p jdk_${cell.jdk}/${cell.arch} ';' -execdir mv {} jdk_${cell.jdk}/${cell.arch}/{} ';' + find test/output -name cqlshlib.xml -execdir mv cqlshlib.xml ${cell.step}/cqlshlib${cell_suffix}.xml ';' + find test/output -name nosetests.xml -execdir mv nosetests.xml ${cell.step}/nosetests${cell_suffix}.xml ';' + """ +} + +def debugOomKiller() { + // check if we had Linux OOM killer active within the test container which could kill forked JUnit JVM processes + sh label: "checking for oom kills...", script: """ + # docker memory/oomkiller debug: + cat /sys/fs/cgroup/docker/memory.events || true + """ +} + +def compressTestResultFiles() { + sh label: "compress test result files...", script: """ + { set +x; } 2>/dev/null + find test/output -type f -name "*.xml" -print0 | xargs -0 -r -n1 -P"\$(nproc)" xz -f + echo "\$(find test/output -type f -name "*.xml.xz" | wc -l) test result files compressed" + """ } ///////////////////////////////////////// @@ -614,7 +655,7 @@ def generateTestReports() { def script_vars = "#!/bin/bash -x \n " if (isCanonical()) { // copyArtifacts takes >4hrs, hack with manual download - sh """${script_vars} + sh label: "manual download (instead of copyArtifacts)...", script: """${script_vars} ( mkdir -p build/test wget -q ${BUILD_URL}/artifact/test/output/*zip*/output.zip unzip -x -d build/test -q output.zip ) ${teeSuffix} @@ -627,7 +668,7 @@ def generateTestReports() { // merge splits for each target's test report, other axes are kept separate // TODO parallelised for loop // TODO results_details.tar.xz needs to include all logs for failed tests - sh """${script_vars} ( + sh label: "merging splits test reports...", script: """${script_vars} ( echo "test result files to decompress"; find build/test/output -type f -name "*.xml.xz" | wc -l find build/test/output -type f -name "*.xml.xz" -print0 | xargs -0 -r -n1 -P"\$(nproc)" xz -f --decompress diff --git a/.jenkins/k8s/README.md b/.jenkins/k8s/README.md index 8441306513..14ac7175fd 100644 --- a/.jenkins/k8s/README.md +++ b/.jenkins/k8s/README.md @@ -24,14 +24,14 @@ ZONE="us-central1-c" gcloud container clusters create ${CLUSTER_NAME} --machine-type e2-standard-8 --disk-type=pd-ssd --num-nodes 1 --node-labels=cassandra.jenkins.controller=true --autoscaling-profile optimize-utilization --zone ${ZONE} # small resource nodes -gcloud container node-pools create agents-small --cluster ${CLUSTER_NAME} --machine-type e2-highcpu-8 --disk-type=pd-ssd --enable-autoscaling --spot --num-nodes=0 --min-nodes=0 --max-nodes=50 --node-labels=cassandra.jenkins.agent=true,cassandra.jenkins.agent.small=true --zone ${ZONE} +gcloud container node-pools create agents-small --cluster ${CLUSTER_NAME} --machine-type e2-highcpu-8 --disk-type=pd-ssd --disk-size=107 --enable-autoscaling --spot --num-nodes=0 --min-nodes=0 --max-nodes=50 --node-labels=cassandra.jenkins.agent=true,cassandra.jenkins.agent.small=true --zone ${ZONE} # medium resource nodes # preference (by cost): n2-highcpu-8, c3-highcpu-8, n4-highcpu-8, n1-highcpu-16 -gcloud container node-pools create agents-medium --cluster ${CLUSTER_NAME} --machine-type n2-highcpu-8 --disk-type=pd-ssd --enable-autoscaling --spot --num-nodes=0 --min-nodes=0 --max-nodes=100 --node-labels=cassandra.jenkins.agent=true,cassandra.jenkins.agent.medium=true --zone ${ZONE} +gcloud container node-pools create agents-medium --cluster ${CLUSTER_NAME} --machine-type n2-highcpu-8 --disk-type=pd-ssd --disk-size=107 --enable-autoscaling --spot --num-nodes=0 --min-nodes=0 --max-nodes=100 --node-labels=cassandra.jenkins.agent=true,cassandra.jenkins.agent.medium=true --zone ${ZONE} # large resource nodes -gcloud container node-pools create agents-large --cluster ${CLUSTER_NAME} --machine-type n2-standard-8 --disk-type=pd-ssd --enable-autoscaling --spot --num-nodes=0 --min-nodes=0 --max-nodes=160 --node-labels=cassandra.jenkins.agent=true,cassandra.jenkins.agent.large=true --zone ${ZONE} +gcloud container node-pools create agents-large --cluster ${CLUSTER_NAME} --machine-type n2-standard-8 --disk-type=pd-ssd --disk-size=107 --enable-autoscaling --spot --num-nodes=0 --min-nodes=0 --max-nodes=160 --node-labels=cassandra.jenkins.agent=true,cassandra.jenkins.agent.large=true --zone ${ZONE} # For each sized resource nodes, pick any machine type that fits, those listed above should work and be the most cost-effective, but this can change region to region # See https://github.com/apache/cassandra/blob/cassandra-6.0/.jenkins/Jenkinsfile#L35-L38 @@ -64,17 +64,81 @@ kubectl exec -it svc/cassius-jenkins -c jenkins -- /bin/cat /run/secrets/additio This leaves the controller running, a single e2-standard-8 instance. All other node-pools downscale to zero. -### Local-only Access +## Upgrading an existing instance (e.g. pre-ci.cassandra.apache.org) + +A long-lived site like pre-ci.cassandra.apache.org may carry customisations: hostname, cloud load-balancer, storage class; that are deliberately absent from `jenkins-deployment.yaml`. + +Running `helm upgrade -f jenkins-deployment.yaml` or `.build/run-ci --only-setup` will drop those customisations. + +Instead keep the customisations in separate overrides file, using it like +``` +.build/run-ci --only-setup --values-override +``` + or as a second `-f` argument to `helm upgrade`. + + +##### To collect overridden values + +To get and diff currently deployed values against the version of `jenkins-deployment.yaml` that is deployed. +``` +RELEASE=cassius +NS=default +kubectl config current-context # confirm correct context +DEPLOYED_COMMIT=cassandra-5.0 # the last jenkins-deployment.yaml deployed git commit sha + +helm get values ${RELEASE} -n ${NS} -o yaml > /tmp/cassandra-ci-live-values.yaml + +git show ${DEPLOYED_COMMIT}:.jenkins/k8s/jenkins-deployment.yaml > /tmp/deployed.yaml + +for f in /tmp/deployed.yaml /tmp/cassandra-ci-live-values.yaml ; do + python3 -c 'import sys,yaml;print(yaml.safe_dump(yaml.safe_load(open(sys.argv[1])),sort_keys=True,width=10000))' ${f} > ${f}.sorted +done + +diff -u /tmp/deployed.yaml.sorted /tmp/cassandra-ci-live-values.yaml.sorted +``` + +Copy the genuine customisations you need to keep into `~/.cassandra-ci/-overrides.yaml`, keeping the full key path. + +For example: +``` +controller: + ingress: + hostName: pre-ci.cassandra.apache.org # note the capital N, `hostname` is silently ignored + serviceAnnotations: # AWS load-balancer-controller: static EIP, public subnet + service.beta.kubernetes.io/aws-load-balancer-name: pre-ci-apache-cassandra + ... +persistence: + storageClass: gp2 +``` + +Beware how Helm merges: maps are merged key by key, but lists and strings are *replaced* wholesale. Each `agent.podTemplates.*` entry is one multi-line string, so overriding a pod template masks every repo-side change to that template. Prefer keeping pod-template customisations out of the overrides file; where that is unavoidable, re-apply the repo's changes to the overridden copy by hand at each upgrade. + + +### Rolling back a bad Helm upgrade + +``` +helm history ${RELEASE} -n ${NS} +helm rollback ${RELEASE} -n ${NS} --wait --timeout 15m +``` + +`helm rollback` restores the previous chart *and* values, so the site's customisations come back with it. If the release history itself is unusable, the `/tmp/cassandra-ci-live-values.yaml` from above can be used: +``` +helm upgrade ${RELEASE} jenkins/jenkins --version ${CHART_VERSION} -n ${NS} \ + -f /tmp/cassandra-ci-live-values.yaml --wait --timeout 15m +``` + + +### Configuring Local-only Access If you want only local private access to Jenkins, do the following. -Comment these lines before running `heml upgrade …` +Comment these lines before running `helm upgrade …` ``` # serviceType: LoadBalancer # ingress: # enabled: "true" ``` -Run the heml upgrade and get the password as usual +Run the helm upgrade and get the password as usual ``` helm upgrade --install -f values.yaml cassius jenkins/jenkins --wait diff --git a/.jenkins/k8s/jenkins-deployment.yaml b/.jenkins/k8s/jenkins-deployment.yaml index 98290e6011..d305af8509 100644 --- a/.jenkins/k8s/jenkins-deployment.yaml +++ b/.jenkins/k8s/jenkins-deployment.yaml @@ -22,7 +22,11 @@ persistence: enabled: true size: "500Gi" - # aws needs gp2, gke can be left commented + # keep the claim (jobs, credentials, build history) when the release is uninstalled. + # deleting it then takes a deliberate `kubectl delete pvc cassius-jenkins` + annotations: + helm.sh/resource-policy: keep + # aws needs gp2, gke can be left commented (add it to your override yaml) #storageClass: "gp2" controller: # To get URL run `kubectl describe svc cassius-jenkins | grep 'LoadBalancer Ingress'` @@ -31,7 +35,7 @@ controller: targetPort: 8080 ingress: enabled: "true" - # uncomment and set if you have a "ci-cassandra" dns entry for the jenkins controller + # if you have a "ci-cassandra" dns entry for the jenkins controller add the following value to your override yaml #hostName: ci-cassandra. customJenkinsLabels: - controller @@ -69,13 +73,14 @@ controller: - "staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods inspect java.lang.Object" - "staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods max java.util.Collection" - "staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods putAt java.util.List java.util.List java.lang.Object" + - "field hudson.plugins.git.GitSCMBackwardCompatibility branch" - "method org.jenkinsci.plugins.workflow.steps.FlowInterruptedException getCauses" JCasC: configScripts: welcome-message: | jenkins: systemMessage: Welcome to Apache Cassandra - # we still need separate jobs because Jenkinsfile can differ (and are read before parameters are applied). + # Separate jobs are needed because Jenkinsfiles differ, and are read before parameters are applied. # if a dev branch alters the Jenkinsfile, it will not be picked up by the job – you need to edit the job configuration # see the CAUTION warning in .jenkins/Jenkinsfile # TODO: add new version each release branching @@ -154,6 +159,23 @@ agent: node-selector: cassandra.jenkins.agent: true waitForPodSec: "180" + # + # Each template below is an opaque string to the helm chart, parsed only by the kubernetes plugin, + # so the chart (nor .jenkins/k8s/jenkins-test.sh) can validate it. + # + # Two traps then to pay attention to: + # - a volume declared under `volumes:` is generated as `volume-N`, and the generated copy wins any + # merge with a raw `yaml:` entry of the same name, so a field set only there is silently dropped. + # Volumes wanting a field the plugin lacks, a sizeLimit for instance, are declared in `yaml:` alone. + # - ephemeral-storage is charged to the pod: every emptyDir, the workspace included, along with the + # containers' writable layers and logs, all against the containers' limits summed. Declare no + # request and the pod is BestEffort for storage, which the scheduler ignores and the node evicts + # first, reporting only `request is 0`. Each template below budgets 80Gi (of the ~89Gi that a + # 100GiB node allocates) leaving the rest to the node's own image cache and daemonsets. + # + # After any deploy, validate changes like: + # kubectl get pod -l jenkins/cassius-jenkins-agent -o json | jq '.items[0].spec | {volumes, containers: [.containers[] | {name, resources, volumeMounts}]}' + # podTemplates: agent-dind-small: | - name: agent-dind-small @@ -201,6 +223,9 @@ agent: resourceLimitCpu: 2 resourceRequestMemory: 1G resourceLimitMemory: 1G + # the workspace emptyDir + resourceRequestEphemeralStorage: 10Gi + resourceLimitEphemeralStorage: 20Gi ttyEnabled: 'true' workingDir: /home/jenkins/agent - name: dind @@ -213,7 +238,7 @@ agent: key: "DOCKER_IPTABLES_LEGACY" value: "1" image: docker:dind - args: "--default-address-pool base=192.168.96.0/20,size=24" # overwrite docker subnet in case of overlapping + args: "--default-address-pool base=192.168.96.0/20,size=24" # overwrite docker subnet in case of overlapping livenessProbe: failureThreshold: '0' initialDelaySeconds: '0' @@ -225,12 +250,13 @@ agent: resourceLimitCpu: 4 resourceRequestMemory: 1G resourceLimitMemory: 2400M + # docker's images and containers, in the docker-storage emptyDir + resourceRequestEphemeralStorage: 40Gi + resourceLimitEphemeralStorage: 60Gi ttyEnabled: 'true' workingDir: /home/jenkins/agent volumes: - - emptyDirVolume: - memory: 'false' - mountPath: /var/lib/docker + # /var/lib/docker is not here but in `yaml:` below, the only place it can carry a sizeLimit - emptyDirVolume: memory: 'false' mountPath: /certs @@ -247,6 +273,18 @@ agent: values: - "true" topologyKey: kubernetes.io/hostname + # docker's storage, named and mounted here so that the sizeLimit survives the plugin's merge. + # 60Gi bounds the images and their containers alone, of the pod's 80Gi + # fetchDockerImages in Jenkinsfile warns as the node fills as our image sizes grow. + volumes: + - name: docker-storage + emptyDir: + sizeLimit: 60Gi + containers: + - name: dind + volumeMounts: + - name: docker-storage + mountPath: /var/lib/docker agent-dind-medium: | - name: agent-dind-medium label: agent-dind cassandra-medium cassandra-amd64-medium @@ -291,6 +329,9 @@ agent: resourceLimitCpu: 3 resourceRequestMemory: 1G resourceLimitMemory: 2400M + # the workspace emptyDir + resourceRequestEphemeralStorage: 10Gi + resourceLimitEphemeralStorage: 20Gi ttyEnabled: 'true' workingDir: /home/jenkins/agent - name: dind @@ -315,12 +356,13 @@ agent: resourceLimitCpu: 4 resourceRequestMemory: 3400M resourceLimitMemory: 5G + # docker's images and containers, in the docker-storage emptyDir + resourceRequestEphemeralStorage: 40Gi + resourceLimitEphemeralStorage: 60Gi ttyEnabled: 'true' workingDir: /home/jenkins/agent volumes: - - emptyDirVolume: - memory: 'false' - mountPath: /var/lib/docker + # /var/lib/docker is not here but in `yaml:` below, the only place it can carry a sizeLimit - emptyDirVolume: memory: 'false' mountPath: /certs @@ -337,6 +379,18 @@ agent: values: - "true" topologyKey: kubernetes.io/hostname + # docker's storage, named and mounted here so that the sizeLimit survives the plugin's merge. + # 60Gi bounds the images and their containers alone, of the pod's 80Gi + # fetchDockerImages in Jenkinsfile warns as the node fills as our image sizes grow. + volumes: + - name: docker-storage + emptyDir: + sizeLimit: 60Gi + containers: + - name: dind + volumeMounts: + - name: docker-storage + mountPath: /var/lib/docker agent-dind-large: | - name: agent-dind-large label: agent-dind cassandra-large cassandra-amd64-large cassandra-amd64-large-dedicated @@ -381,6 +435,9 @@ agent: resourceLimitCpu: 3 resourceRequestMemory: 1G resourceLimitMemory: 2G + # the workspace emptyDir + resourceRequestEphemeralStorage: 10Gi + resourceLimitEphemeralStorage: 20Gi ttyEnabled: 'true' workingDir: /home/jenkins/agent - name: dind @@ -405,12 +462,13 @@ agent: resourceLimitCpu: 7 resourceRequestMemory: 16G resourceLimitMemory: 30G + # docker's images and containers, in the docker-storage emptyDir + resourceRequestEphemeralStorage: 40Gi + resourceLimitEphemeralStorage: 60Gi ttyEnabled: 'true' workingDir: /home/jenkins/agent volumes: - - emptyDirVolume: - memory: 'false' - mountPath: /var/lib/docker + # /var/lib/docker is not here but in `yaml:` below, the only place it can carry a sizeLimit - emptyDirVolume: memory: 'false' mountPath: /certs @@ -427,5 +485,17 @@ agent: values: - "true" topologyKey: kubernetes.io/hostname + # docker's storage, named and mounted here so that the sizeLimit survives the plugin's merge. + # 60Gi bounds the images and their containers alone, of the pod's 80Gi + # fetchDockerImages in Jenkinsfile warns as the node fills as our image sizes grow. + volumes: + - name: docker-storage + emptyDir: + sizeLimit: 60Gi + containers: + - name: dind + volumeMounts: + - name: docker-storage + mountPath: /var/lib/docker diff --git a/.jenkins/k8s/jenkins-test.sh b/.jenkins/k8s/jenkins-test.sh new file mode 100755 index 0000000000..eef4f47f7e --- /dev/null +++ b/.jenkins/k8s/jenkins-test.sh @@ -0,0 +1,117 @@ +#!/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. + +# +# Validates the CI declarations under .jenkins/ without deploying anything: +# - the Jenkinsfile parses as groovy +# - jenkins-deployment.yaml renders through the jenkins helm chart +# - the yaml embedded in it (agent pod templates, JCasC config scripts) parses +# +# Requires: helm, python3 with pyyaml, and either groovy or docker. +# Run from anywhere: .jenkins/k8s/jenkins-test.sh + +set -e + +CASSANDRA_DIR="$(cd "$(dirname "$0")/../.." > /dev/null && pwd)" +JENKINS_DIR="${CASSANDRA_DIR}/.jenkins" +status=0 + +command -v helm > /dev/null || { echo "helm must be installed and in the PATH"; exit 1; } +command -v python3 > /dev/null || { echo "python3 must be installed and in the PATH"; exit 1; } +python3 -c "import yaml" 2> /dev/null || { echo "python3 pyyaml must be installed: pip install pyyaml"; exit 1; } + +echo "== Jenkinsfile groovy syntax" +# Phases.CONVERSION parses and builds the AST without resolving the pipeline DSL or @NonCPS, +# neither of which exist outside a jenkins controller +syntax_check_dir="$(mktemp -d)" +syntax_check="${syntax_check_dir}/syntax-check.groovy" +# mktemp gives 0700, which the unprivileged user inside the groovy image cannot traverse +chmod 755 "${syntax_check_dir}" +cat > "${syntax_check}" << 'EOF' +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.Phases + +def cu = new CompilationUnit() +args.each { cu.addSource(new File(it)) } +cu.compile(Phases.CONVERSION) +println " ${args.join(', ')} parses" +EOF +if command -v groovy > /dev/null ; then + groovy "${syntax_check}" "${JENKINS_DIR}/Jenkinsfile" || status=1 +elif command -v docker > /dev/null ; then + # absolute paths, the image's working directory is not where the script was mounted + docker run --rm -v "${syntax_check_dir}:/check:ro" -v "${JENKINS_DIR}:/jenkins:ro" \ + groovy:4.0-jdk17 groovy /check/syntax-check.groovy /jenkins/Jenkinsfile || status=1 +else + echo " SKIPPED: neither groovy nor docker found" +fi + +echo "== jenkins-deployment.yaml renders through the helm chart" +helm repo add jenkins https://charts.jenkins.io > /dev/null +helm repo update > /dev/null +# --namespace and a release name only so the chart's templates have something to interpolate +helm template cassius jenkins/jenkins --namespace default -f "${JENKINS_DIR}/k8s/jenkins-deployment.yaml" > /dev/null \ + && echo " jenkins-deployment.yaml renders" || status=1 + +echo "== yaml embedded in jenkins-deployment.yaml" +python3 - "${JENKINS_DIR}/k8s" << 'EOF' || status=1 +import sys, yaml +from pathlib import Path + +k8s_dir = Path(sys.argv[1]) +errors = 0 + +for path in sorted(k8s_dir.glob("*.yaml")): + try: + values = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + except yaml.YAMLError as error: + print(f" INVALID {path.name}: {error}") + errors += 1 + continue + print(f" {path.name} parses") + + # the values in these two maps are themselves yaml documents, and a chart never validates them — + # a misindented pod template reaches the kubernetes plugin and silently loses its agents + for keys in (("agent", "podTemplates"), ("controller", "JCasC", "configScripts")): + embedded = values + for key in keys: + embedded = embedded.get(key, {}) if isinstance(embedded, dict) else {} + for name, document in (embedded or {}).items(): + location = f"{path.name} {'.'.join(keys)}.{name}" + try: + parsed = yaml.safe_load(document) + except yaml.YAMLError as error: + print(f" INVALID {location}: {error}") + errors += 1 + continue + print(f" {location} parses") + # each pod template may carry a raw kubernetes pod spec in a nested `yaml` key + for template in parsed if isinstance(parsed, list) else []: + if isinstance(template, dict) and "yaml" in template: + try: + yaml.safe_load(template["yaml"]) + except yaml.YAMLError as error: + print(f" INVALID {location}.yaml: {error}") + errors += 1 + else: + print(f" {location}.yaml parses") + +sys.exit(1 if errors else 0) +EOF + +[ 0 -eq ${status} ] && echo "== all .jenkins/ checks passed" || echo "== FAILED" +exit ${status} diff --git a/CHANGES.txt b/CHANGES.txt index 04e62e29b0..5b33484d30 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,9 +1,21 @@ 7.0 + * Don't increment client metrics on messaging service connection unpause (CASSANDRA-21491) + * Add nodetool getreplicas (CASSANDRA-17665) * Implementation of CEP-49: Hardware-accelerated compression (CASSANDRA-20975) * Avoid using ObjectUtils.getFirstNonNull in Schema (CASSANDRA-21394) * Allow nodetool garbagecollect to take a user defined list of SSTables (CASSANDRA-16767) * Add a guardrail for misprepared statements (CASSANDRA-21139) Merged from 6.0: + * Apply performance optimizations for rows merging logic (CASSANDRA-21524) + * Fix operationMode reporting DECOMMISSION_FAILED instead of LEAVING when resuming a failed decommission (CASSANDRA-21493) + * Avoid megamorphic calls when serializing and deserializing fixed-length values (CASSANDRA-21536) + * Avoid megamorphic calls for Cell.timestamp/ttl/path/localDeletionTimeAsUnsignedInt methods (CASSANDRA-21526) + * Reduce allocations in DefaultQueryOptions (CASSANDRA-21467) + * Allow unreserved keywords as user and identity names in USER and IDENTITY statements (CASSANDRA-21510) + * Reduce allocations in DefaultQueryOptions (CASSANDRA-21467) + * Reduce number of scheduledTasks on metric id release in ThreadLocalMetrics (CASSANDRA-21475) + * Cache various Enum.values() used in deserialization to avoid per-read array allocation (CASSANDRA-21528) + * Fix Accord transaction error message when altering a table (CASSANDRA-20580) * Depend only on platform-specific Zstd JNI native libraries (CASSANDRA-21483) * Expose immediately-executed tasks in the queries virtual table (CASSANDRA-21471) * Avoid potential deadlock between GlobalLogFollower and GossipStage (CASSANDRA-21384) @@ -21,7 +33,7 @@ Merged from 6.0: * Always send TCM commit failures as Messaging failures (CASSANDRA-21457) * Fix ReadCommand serializedSize() using incorrect epoch (CASSANDRA-21438) * Allocation improvements in ProtocolVersion, StorageProxy and MerkleTree (CASSANDRA-21199) - * Don’t leave autocompaction disabled during bootstrap and replace (CASSANDRA-21236) + * Don't leave autocompaction disabled during bootstrap and replace (CASSANDRA-21236) * Make nodetool abortbootstrap more robust (CASSANDRA-21235) * Don't clear prepared statement cache on nodetool cms initialize (CASSANDRA-21234) * Improve performance when deserializing cluster metadata (CASSANDRA-21224) @@ -64,6 +76,8 @@ Merged from 5.0: * Use estimated compressed size for tables to check if there is enough free space for a compaction (CASSANDRA-21245) * Fix failing select on system_views.settings for non-string keys (CASSANDRA-21348) Merged from 4.0: + * Bound declared value length against readable bytes in CBUtil (CASSANDRA-21521) + * Verify extension type before initializing reflectively-loaded classes (CASSANDRA-21525) * Rename conflicting nodetool import --copy-data short option from -p to -cd (CASSANDRA-20214) * Fix PasswordObfuscator failing to obfuscate certain passwords (CASSANDRA-21113) * Fix negative memtable allocator ownership when an update is shadowed by an existing row deletion (CASSANDRA-21469) @@ -86,6 +100,7 @@ Merged from 4.0: * Fix a removed TTLed row re-appearance in a materialized view after a cursor compaction (CASSANDRA-21152) * Rework ZSTD dictionary compression logic to create a trainer per training (CASSANDRA-21209) Merged from 5.0: + * SAI Component Checksum Validation Should be Segment-Aware (CASSANDRA-21516) * Ensure SAI sends range tombstones to the coordinator for queries on static columns (CASSANDRA-21332) Merged from 4.1: * Add Paxos v2 option and informatin in cassandra.yaml (CASSANDRA-21316) @@ -8655,4 +8670,3 @@ Full list of issues resolved in 0.4 is at https://issues.apache.org/jira/secure/ * Added FlushPeriodInMinutes configuration parameter to force flushing of infrequently-updated ColumnFamilies - diff --git a/README.asc b/README.asc index 3050d2c239..416402098d 100644 --- a/README.asc +++ b/README.asc @@ -31,7 +31,7 @@ Getting started --------------- This short guide will walk you through getting a basic one node cluster up -and running, and demonstrate some simple reads and writes. For a more-complete guide, please see the Apache Cassandra website's https://cassandra.apache.org/doc/trunk/cassandra/getting-started/index.html[Getting Started Guide]. +and running, and demonstrate some simple reads and writes. For a more-complete guide, please see the Apache Cassandra website's https://cassandra.apache.org/doc/latest/cassandra/getting-started/index.html[Getting Started Guide]. First, we'll unpack our archive: diff --git a/build.xml b/build.xml index 7240153109..2f5a425149 100644 --- a/build.xml +++ b/build.xml @@ -651,6 +651,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -710,7 +768,7 @@ - @@ -2173,7 +2231,7 @@ - + diff --git a/doc/modules/cassandra/pages/managing/configuration/cass_env_sh_file.adoc b/doc/modules/cassandra/pages/managing/configuration/cass_env_sh_file.adoc index 309b15b17d..b1bfb96393 100644 --- a/doc/modules/cassandra/pages/managing/configuration/cass_env_sh_file.adoc +++ b/doc/modules/cassandra/pages/managing/configuration/cass_env_sh_file.adoc @@ -31,11 +31,6 @@ In a multi-instance deployment, multiple Cassandra instances will independently assume that all CPU processors are available to it. This setting allows you to specify a smaller set of processors. -== `cassandra.boot_without_jna=true` - -If JNA fails to initialize, Cassandra fails to boot. Use this command to -boot Cassandra without JNA. - == `cassandra.config=` The directory location of the `cassandra.yaml file`. The default diff --git a/doc/modules/cassandra/pages/managing/operating/compression.adoc b/doc/modules/cassandra/pages/managing/operating/compression.adoc index 04ab4d13eb..3f9a4905b8 100644 --- a/doc/modules/cassandra/pages/managing/operating/compression.adoc +++ b/doc/modules/cassandra/pages/managing/operating/compression.adoc @@ -265,11 +265,11 @@ management, caching, and training behavior. === Dictionary Refresh Settings -* `compression_dictionary_refresh_interval` (default: `3600`): How often +* `compression_dictionary_refresh_interval` (default: `3600s`): How often (in seconds) to check for and refresh compression dictionaries cluster-wide. Newly trained dictionaries will be picked up by all nodes within this interval. -* `compression_dictionary_refresh_initial_delay` (default: `10`): Initial +* `compression_dictionary_refresh_initial_delay` (default: `10s`): Initial delay (in seconds) before the first dictionary refresh check after node startup. @@ -278,7 +278,7 @@ startup. * `compression_dictionary_cache_size` (default: `10`): Maximum number of compression dictionaries to cache per table. Higher values reduce lookup overhead but increase memory usage. -* `compression_dictionary_cache_expire` (default: `3600`): Dictionary +* `compression_dictionary_cache_expire` (default: `24h`): Dictionary cache entry TTL in seconds. Expired entries are evicted and reloaded on next access. @@ -289,10 +289,10 @@ Example configuration: [source,yaml] ---- # Dictionary refresh and caching -compression_dictionary_refresh_interval: 3600 -compression_dictionary_refresh_initial_delay: 10 +compression_dictionary_refresh_interval: 3600s +compression_dictionary_refresh_initial_delay: 10s compression_dictionary_cache_size: 10 -compression_dictionary_cache_expire: 3600 +compression_dictionary_cache_expire: 24h ---- === CQL training parameters: diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g index 8794d00d02..c459716406 100644 --- a/src/antlr/Parser.g +++ b/src/antlr/Parser.g @@ -2363,12 +2363,14 @@ vector_type returns [CQL3Type.Raw vt] username : IDENT | STRING_LITERAL + | unreserved_keyword | QUOTED_NAME { addRecognitionError("Quoted strings are are not supported for user names and USER is deprecated, please use ROLE");} ; identity : IDENT | STRING_LITERAL + | unreserved_keyword | QUOTED_NAME { addRecognitionError("Quoted strings are are not supported for identity");} ; diff --git a/src/java/org/apache/cassandra/auth/AuthConfig.java b/src/java/org/apache/cassandra/auth/AuthConfig.java index 86e1f626ff..52182afe31 100644 --- a/src/java/org/apache/cassandra/auth/AuthConfig.java +++ b/src/java/org/apache/cassandra/auth/AuthConfig.java @@ -63,7 +63,7 @@ public final class AuthConfig /* Authentication, authorization and role management backend, implementing IAuthenticator, I*Authorizer & IRoleManager */ - IAuthenticator authenticator = authInstantiate(conf.authenticator, AllowAllAuthenticator.class); + IAuthenticator authenticator = authInstantiate(conf.authenticator, IAuthenticator.class, AllowAllAuthenticator.class); // the configuration options regarding credentials caching are only guaranteed to // work with PasswordAuthenticator, so log a message if some other authenticator @@ -82,7 +82,7 @@ public final class AuthConfig // authorizer - IAuthorizer authorizer = authInstantiate(conf.authorizer, AllowAllAuthorizer.class); + IAuthorizer authorizer = authInstantiate(conf.authorizer, IAuthorizer.class, AllowAllAuthorizer.class); if (!authenticator.requireAuthentication() && authorizer.requireAuthorization()) { @@ -94,7 +94,7 @@ public final class AuthConfig // role manager - IRoleManager roleManager = authInstantiate(conf.role_manager, CassandraRoleManager.class); + IRoleManager roleManager = authInstantiate(conf.role_manager, IRoleManager.class, CassandraRoleManager.class); if (authenticator instanceof PasswordAuthenticator && !(roleManager instanceof CassandraRoleManager)) throw new ConfigurationException(authenticator.getClass().getName() + " requires " + CassandraRoleManager.class.getName(), false); @@ -104,12 +104,15 @@ public final class AuthConfig // authenticator IInternodeAuthenticator internodeAuthenticator = authInstantiate(conf.internode_authenticator, + IInternodeAuthenticator.class, AllowAllInternodeAuthenticator.class); DatabaseDescriptor.setInternodeAuthenticator(internodeAuthenticator); // network authorizer - INetworkAuthorizer networkAuthorizer = authInstantiate(conf.network_authorizer, AllowAllNetworkAuthorizer.class); + INetworkAuthorizer networkAuthorizer = authInstantiate(conf.network_authorizer, + INetworkAuthorizer.class, + AllowAllNetworkAuthorizer.class); if (networkAuthorizer.requireAuthorization() && !authenticator.requireAuthentication()) { @@ -120,7 +123,9 @@ public final class AuthConfig // cidr authorizer - ICIDRAuthorizer cidrAuthorizer = authInstantiate(conf.cidr_authorizer, AllowAllCIDRAuthorizer.class); + ICIDRAuthorizer cidrAuthorizer = authInstantiate(conf.cidr_authorizer, + ICIDRAuthorizer.class, + AllowAllCIDRAuthorizer.class); if (cidrAuthorizer.requireAuthorization() && !authenticator.requireAuthentication()) { @@ -140,11 +145,11 @@ public final class AuthConfig DatabaseDescriptor.getInternodeAuthenticator().validateConfiguration(); } - private static T authInstantiate(ParameterizedClass authCls, Class defaultCls) { + private static T authInstantiate(ParameterizedClass authCls, Class expectedType, Class defaultCls) { if (authCls != null && authCls.class_name != null) { String authPackage = AuthConfig.class.getPackage().getName(); - return ParameterizedClass.newInstance(authCls, List.of("", authPackage)); + return ParameterizedClass.newInstance(authCls, List.of("", authPackage), expectedType); } // for now, this has to stay and can not be replaced by ParameterizedClass.newInstance as above diff --git a/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java b/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java index 76aacc5b5a..2d897c2eee 100644 --- a/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java +++ b/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java @@ -101,7 +101,8 @@ public class MutualTlsAuthenticator implements IAuthenticator throw new ConfigurationException(message); } certificateValidator = ParameterizedClass.newInstance(new ParameterizedClass(certificateValidatorClassName), - Arrays.asList("", AuthConfig.class.getPackage().getName())); + Arrays.asList("", AuthConfig.class.getPackage().getName()), + MutualTlsCertificateValidator.class); Config config = DatabaseDescriptor.getRawConfig(); certificateValidityPeriodValidator = new MutualTlsCertificateValidityPeriodValidator(config.client_encryption_options.max_certificate_validity_period); diff --git a/src/java/org/apache/cassandra/auth/MutualTlsInternodeAuthenticator.java b/src/java/org/apache/cassandra/auth/MutualTlsInternodeAuthenticator.java index 1cac64dba0..3fc9553fd9 100644 --- a/src/java/org/apache/cassandra/auth/MutualTlsInternodeAuthenticator.java +++ b/src/java/org/apache/cassandra/auth/MutualTlsInternodeAuthenticator.java @@ -104,7 +104,8 @@ public class MutualTlsInternodeAuthenticator implements IInternodeAuthenticator } certificateValidator = ParameterizedClass.newInstance(new ParameterizedClass(certificateValidatorClassName), - Arrays.asList("", AuthConfig.class.getPackage().getName())); + Arrays.asList("", AuthConfig.class.getPackage().getName()), + MutualTlsCertificateValidator.class); Config config = DatabaseDescriptor.getRawConfig(); if (parameters.containsKey(TRUSTED_PEER_IDENTITIES)) diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java index c0fb8bc200..5bc6de7ac1 100644 --- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java +++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java @@ -85,6 +85,7 @@ import org.apache.cassandra.config.Config.DiskAccessMode; import org.apache.cassandra.config.Config.PaxosOnLinearizabilityViolation; import org.apache.cassandra.config.Config.PaxosStatePurging; import org.apache.cassandra.config.DurationSpec.IntMillisecondsBound; +import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.commitlog.AbstractCommitLogSegmentManager; import org.apache.cassandra.db.commitlog.CommitLog; @@ -273,6 +274,8 @@ public class DatabaseDescriptor public static volatile boolean allowUnlimitedConcurrentValidations = ALLOW_UNLIMITED_CONCURRENT_VALIDATIONS.getBoolean(); + private static volatile QueryOptions.DefaultReadThresholds defaultReadThresholds; + /** * RetryStrategy which provides exponential backoff with full jitter, for use by both CMS and non-CMS members * when submitting a Commit request. The range and increments of the backoff times are defined by @@ -332,6 +335,7 @@ public class DatabaseDescriptor private static void clear() { sstableFormats = null; + defaultReadThresholds = null; clearMBean("org.apache.cassandra.db:type=DynamicEndpointSnitch"); clearMBean("org.apache.cassandra.db:type=EndpointSnitchInfo"); clearMBean("org.apache.cassandra.db:type=LocationInfo"); @@ -515,7 +519,7 @@ public class DatabaseDescriptor String loaderClass = CONFIG_LOADER.getString(); ConfigurationLoader loader = loaderClass == null ? new YamlConfigurationLoader() - : FBUtilities.construct(loaderClass, "configuration loading"); + : FBUtilities.construct(loaderClass, "configuration loading", ConfigurationLoader.class); Config config = loader.loadConfig(); if (!hasLoggedConfig) @@ -1655,7 +1659,8 @@ public class DatabaseDescriptor } try { - Class seedProviderClass = Class.forName(conf.seed_provider.class_name); + Class seedProviderClass = + FBUtilities.classForNameWithoutInitialization(conf.seed_provider.class_name, "seed provider", SeedProvider.class); seedProvider = (SeedProvider) seedProviderClass.getConstructor(Map.class).newInstance(conf.seed_provider.parameters); } // there are about 5 checked exceptions that could be thrown here. @@ -2032,7 +2037,7 @@ public class DatabaseDescriptor { if (!snitchClassName.contains(".")) snitchClassName = "org.apache.cassandra.locator." + snitchClassName; - IEndpointSnitch snitch = FBUtilities.construct(snitchClassName, "snitch"); + IEndpointSnitch snitch = FBUtilities.construct(snitchClassName, "snitch", IEndpointSnitch.class); return snitch; } @@ -2040,7 +2045,7 @@ public class DatabaseDescriptor { if (!className.contains(".")) className = "org.apache.cassandra.locator." + className; - NodeProximity sorter = FBUtilities.construct(className, "node proximity measurement"); + NodeProximity sorter = FBUtilities.construct(className, "node proximity measurement", NodeProximity.class); return sorter; } @@ -2048,7 +2053,7 @@ public class DatabaseDescriptor { if (!className.contains(".")) className = "org.apache.cassandra.locator." + className; - InitialLocationProvider provider = FBUtilities.construct(className, "initial location provider"); + InitialLocationProvider provider = FBUtilities.construct(className, "initial location provider", InitialLocationProvider.class); return provider; } @@ -2056,7 +2061,7 @@ public class DatabaseDescriptor { if (!className.contains(".")) className = "org.apache.cassandra.locator." + className; - NodeAddressConfig config = FBUtilities.construct(className, "node address config"); + NodeAddressConfig config = FBUtilities.construct(className, "node address config", NodeAddressConfig.class); return config; } @@ -2064,7 +2069,7 @@ public class DatabaseDescriptor { if (!detectorClassName.contains(".")) detectorClassName = "org.apache.cassandra.gms." + detectorClassName; - IFailureDetector detector = FBUtilities.construct(detectorClassName, "failure detector"); + IFailureDetector detector = FBUtilities.construct(detectorClassName, "failure detector", IFailureDetector.class); return detector; } @@ -5570,6 +5575,14 @@ public class DatabaseDescriptor return conf.invalid_legacy_protocol_magic_no_spam_enabled; } + public static QueryOptions.DefaultReadThresholds getDefaultReadThresholds() + { + if (defaultReadThresholds == null) + defaultReadThresholds = new QueryOptions.DefaultReadThresholds(getCoordinatorReadSizeWarnThreshold(), + getCoordinatorReadSizeFailThreshold()); + return defaultReadThresholds; + } + public static boolean getReadThresholdsEnabled() { return conf.read_thresholds_enabled; @@ -5594,6 +5607,7 @@ public class DatabaseDescriptor { logger.info("updating coordinator_read_size_warn_threshold to {}", value); conf.coordinator_read_size_warn_threshold = value; + defaultReadThresholds = null; } @Nullable @@ -5606,6 +5620,7 @@ public class DatabaseDescriptor { logger.info("updating coordinator_read_size_fail_threshold to {}", value); conf.coordinator_read_size_fail_threshold = value; + defaultReadThresholds = null; } @Nullable diff --git a/src/java/org/apache/cassandra/config/ParameterizedClass.java b/src/java/org/apache/cassandra/config/ParameterizedClass.java index d772629f19..803fcb1fac 100644 --- a/src/java/org/apache/cassandra/config/ParameterizedClass.java +++ b/src/java/org/apache/cassandra/config/ParameterizedClass.java @@ -64,7 +64,17 @@ public class ParameterizedClass p.containsKey(PARAMETERS) ? (Map)((List)p.get(PARAMETERS)).get(0) : null); } + /** + * Prefer {@link #newInstance(ParameterizedClass, List, Class)}: passing an {@code expectedType} verifies the + * resolved class is the intended extension type before it is instantiated. This overload performs no type + * check (it still loads without initialization, so a wrong class name cannot run its static initializer here). + */ static public K newInstance(ParameterizedClass parameterizedClass, List searchPackages) + { + return newInstance(parameterizedClass, searchPackages, null); + } + + static public K newInstance(ParameterizedClass parameterizedClass, List searchPackages, Class expectedType) { Class providerClass = null; if (searchPackages == null || searchPackages.isEmpty()) @@ -76,9 +86,12 @@ public class ParameterizedClass if (!searchPackage.isEmpty() && !searchPackage.endsWith(".")) searchPackage = searchPackage + '.'; String name = searchPackage + parameterizedClass.class_name; - providerClass = Class.forName(name); + // Load without initialization so a wrong class name does not run its static initializer here. The + // type is verified below (once the search has resolved a class) and the class is only initialized + // later, when it is constructed. + providerClass = Class.forName(name, false, ParameterizedClass.class.getClassLoader()); } - catch (ClassNotFoundException e) + catch (ClassNotFoundException | NoClassDefFoundError e) { //no-op } @@ -91,6 +104,13 @@ public class ParameterizedClass throw new ConfigurationException(error); } + // Verify the resolved class is the expected extension type before it is initialized/instantiated. Done once, + // after the package search, so a wrong-type match under an earlier search package does not abort the search + // before a valid class under a later package is found. + if (expectedType != null && !expectedType.isAssignableFrom(providerClass)) + throw new ConfigurationException("Invalid parameterized class " + providerClass.getName() + + ": must extend or implement " + expectedType.getName()); + try { Constructor mapConstructor = filterConstructor(providerClass, c -> c.getParameterTypes().length == 1 && c.getParameterTypes()[0].equals(Map.class)); diff --git a/src/java/org/apache/cassandra/cql3/QueryOptions.java b/src/java/org/apache/cassandra/cql3/QueryOptions.java index afc5bacc38..78910e5788 100644 --- a/src/java/org/apache/cassandra/cql3/QueryOptions.java +++ b/src/java/org/apache/cassandra/cql3/QueryOptions.java @@ -322,7 +322,7 @@ public abstract class QueryOptions implements RealTimeFunctionContext // if daemon initialization hasn't happened yet (very common in tests) then ignore if (!DatabaseDescriptor.isDaemonInitialized() || !DatabaseDescriptor.getReadThresholdsEnabled()) return DisabledReadThresholds.INSTANCE; - return new DefaultReadThresholds(DatabaseDescriptor.getCoordinatorReadSizeWarnThreshold(), DatabaseDescriptor.getCoordinatorReadSizeFailThreshold()); + return DatabaseDescriptor.getDefaultReadThresholds(); } } @@ -349,7 +349,7 @@ public abstract class QueryOptions implements RealTimeFunctionContext } } - private static class DefaultReadThresholds implements ReadThresholds + public static class DefaultReadThresholds implements ReadThresholds { private final long warnThresholdBytes; private final long abortThresholdBytes; diff --git a/src/java/org/apache/cassandra/cql3/selection/Selector.java b/src/java/org/apache/cassandra/cql3/selection/Selector.java index 72f0f6608c..60f03a1050 100644 --- a/src/java/org/apache/cassandra/cql3/selection/Selector.java +++ b/src/java/org/apache/cassandra/cql3/selection/Selector.java @@ -96,12 +96,19 @@ public abstract class Selector SLICE_SELECTOR(ElementsSelector.SliceSelector.deserializer), VECTOR_SELECTOR(VectorSelector.deserializer); + private static final Kind[] VALUES = values(); + private final SelectorDeserializer deserializer; Kind(SelectorDeserializer deserializer) { this.deserializer = deserializer; } + + public static Kind fromOrdinal(int ordinal) + { + return VALUES[ordinal]; + } } /** @@ -260,7 +267,7 @@ public abstract class Selector public Selector deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException { - Kind kind = Kind.values()[in.readUnsignedByte()]; + Kind kind = Kind.fromOrdinal(in.readUnsignedByte()); return kind.deserializer.deserialize(in, version, metadata); } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java index aee1496cb5..09501c8446 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java @@ -653,7 +653,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement boolean forceMigrationChange = modeChange && explicitlySetMigrationFrom && next.transactionalMigrationFrom != newMigrateFrom; if (modeChange && next.transactionalMode.accordIsEnabled && !DatabaseDescriptor.getAccordTransactionsEnabled()) - throw ire(format("Cannot change transactional mode to %s for %s.%s with accord_transactions_enabled set to false", + throw ire(format("Cannot change transactional mode to %s for %s.%s with accord.enabled set to false", next.transactionalMode, keyspaceName, tableName)); // user is manually updating migration mode, don't interfere diff --git a/src/java/org/apache/cassandra/db/ClusteringBoundOrBoundary.java b/src/java/org/apache/cassandra/db/ClusteringBoundOrBoundary.java index e419bfbf36..1a13ba2567 100644 --- a/src/java/org/apache/cassandra/db/ClusteringBoundOrBoundary.java +++ b/src/java/org/apache/cassandra/db/ClusteringBoundOrBoundary.java @@ -116,7 +116,7 @@ public interface ClusteringBoundOrBoundary extends ClusteringPrefix public ClusteringBoundOrBoundary deserialize(DataInputPlus in, int version, List> types) throws IOException { - Kind kind = Kind.values()[in.readByte()]; + Kind kind = Kind.fromOrdinal(in.readByte()); return deserializeValues(in, kind, version, types); } diff --git a/src/java/org/apache/cassandra/db/ClusteringPrefix.java b/src/java/org/apache/cassandra/db/ClusteringPrefix.java index 44ccf7be3a..ff2b9261a2 100644 --- a/src/java/org/apache/cassandra/db/ClusteringPrefix.java +++ b/src/java/org/apache/cassandra/db/ClusteringPrefix.java @@ -84,6 +84,8 @@ public interface ClusteringPrefix extends IMeasurableMemory, Clusterable SSTABLE_UPPER_BOUND ( 4, 1, v -> ByteSource.GTGT_NEXT_COMPONENT); // @formatter:on + private static final Kind[] VALUES = values(); + private final int comparison; /** @@ -101,6 +103,11 @@ public interface ClusteringPrefix extends IMeasurableMemory, Clusterable this.asByteComparable = asByteComparable; } + public static Kind fromOrdinal(int ordinal) + { + return VALUES[ordinal]; + } + /** * Compares the 2 provided kind. *

@@ -476,7 +483,7 @@ public interface ClusteringPrefix extends IMeasurableMemory, Clusterable public void skip(DataInputPlus in, int version, List> types) throws IOException { - Kind kind = Kind.values()[in.readByte()]; + Kind kind = Kind.fromOrdinal(in.readByte()); // We shouldn't serialize static clusterings assert kind != Kind.STATIC_CLUSTERING; if (kind == Kind.CLUSTERING) @@ -487,7 +494,7 @@ public interface ClusteringPrefix extends IMeasurableMemory, Clusterable public ClusteringPrefix deserialize(DataInputPlus in, int version, List> types) throws IOException { - Kind kind = Kind.values()[in.readByte()]; + Kind kind = Kind.fromOrdinal(in.readByte()); // We shouldn't serialize static clusterings assert kind != Kind.STATIC_CLUSTERING; if (kind == Kind.CLUSTERING) @@ -657,7 +664,7 @@ public interface ClusteringPrefix extends IMeasurableMemory, Clusterable throw new IOException("Corrupt flags value for clustering prefix (isStatic flag set): " + flags); this.nextIsRow = UnfilteredSerializer.kind(flags) == Unfiltered.Kind.ROW; - this.nextKind = nextIsRow ? Kind.CLUSTERING : ClusteringPrefix.Kind.values()[in.readByte()]; + this.nextKind = nextIsRow ? Kind.CLUSTERING : Kind.fromOrdinal(in.readByte()); this.nextSize = nextIsRow ? comparator.size() : in.readUnsignedShort(); this.deserializedSize = 0; diff --git a/src/java/org/apache/cassandra/db/DeletionTime.java b/src/java/org/apache/cassandra/db/DeletionTime.java index 1d54a00fff..c220d1316b 100644 --- a/src/java/org/apache/cassandra/db/DeletionTime.java +++ b/src/java/org/apache/cassandra/db/DeletionTime.java @@ -178,7 +178,8 @@ public abstract class DeletionTime implements Comparable, IMeasura public boolean deletes(Cell cell) { - return deletes(cell.timestamp()); + // check for LIVE first to avoid a potential cell megamorphic call + return markedForDeleteAt() != MARKED_FOR_DELETE_AT_LIVE && deletes(cell.timestamp()); } public boolean deletes(long timestamp) diff --git a/src/java/org/apache/cassandra/db/ReadCommand.java b/src/java/org/apache/cassandra/db/ReadCommand.java index 53ad6b3d1b..d848e23b6b 100644 --- a/src/java/org/apache/cassandra/db/ReadCommand.java +++ b/src/java/org/apache/cassandra/db/ReadCommand.java @@ -198,6 +198,8 @@ public abstract class ReadCommand extends AbstractReadQuery SINGLE_PARTITION (SinglePartitionReadCommand.selectionDeserializer, SinglePartitionReadCommand.accordSelectionDeserializer), PARTITION_RANGE (PartitionRangeReadCommand.selectionDeserializer, ignore -> PartitionRangeReadCommand.selectionDeserializer); + private static final Kind[] VALUES = values(); + private final SelectionDeserializer selectionDeserializer; private final Function accordSelectionDeserializer; @@ -206,6 +208,11 @@ public abstract class ReadCommand extends AbstractReadQuery this.selectionDeserializer = selectionDeserializer; this.accordSelectionDeserializer = accordSelectionDeserializer; } + + public static Kind fromOrdinal(int ordinal) + { + return VALUES[ordinal]; + } } protected ReadCommand(Epoch serializedAtEpoch, @@ -1450,7 +1457,7 @@ public abstract class ReadCommand extends AbstractReadQuery public ReadCommand deserialize(DataInputPlus in, int version) throws IOException { - Kind kind = Kind.values()[in.readByte()]; + Kind kind = Kind.fromOrdinal(in.readByte()); int flags = in.readByte(); // Shouldn't happen or it's a user error (see comment above) but // better complain loudly than doing the wrong thing. @@ -1488,7 +1495,7 @@ public abstract class ReadCommand extends AbstractReadQuery public ReadCommand deserializeForAccord(Seekable key, TableMetadatas tables, DataInputPlus in, int version) throws IOException { - Kind kind = Kind.values()[in.readByte()]; + Kind kind = Kind.fromOrdinal(in.readByte()); int flags = in.readByte(); if (isDigest(flags) || isForThrift(flags) || acceptsTransient(flags)) throw new IllegalStateException("Received an Accord command with a digest/thrift/transient flag set."); diff --git a/src/java/org/apache/cassandra/db/StorageHook.java b/src/java/org/apache/cassandra/db/StorageHook.java index f5fdec6a56..56938b441f 100644 --- a/src/java/org/apache/cassandra/db/StorageHook.java +++ b/src/java/org/apache/cassandra/db/StorageHook.java @@ -55,7 +55,7 @@ public interface StorageHook String className = STORAGE_HOOK.getString(); if (className != null) { - return FBUtilities.construct(className, StorageHook.class.getSimpleName()); + return FBUtilities.construct(className, StorageHook.class.getSimpleName(), StorageHook.class); } return new StorageHook() @@ -89,4 +89,4 @@ public interface StorageHook } }; } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/db/aggregation/AggregationSpecification.java b/src/java/org/apache/cassandra/db/aggregation/AggregationSpecification.java index a4a1c57eca..2cd7d2a001 100644 --- a/src/java/org/apache/cassandra/db/aggregation/AggregationSpecification.java +++ b/src/java/org/apache/cassandra/db/aggregation/AggregationSpecification.java @@ -65,7 +65,14 @@ public abstract class AggregationSpecification */ public enum Kind { - AGGREGATE_EVERYTHING, AGGREGATE_BY_PK_PREFIX, AGGREGATE_BY_PK_PREFIX_WITH_SELECTOR + AGGREGATE_EVERYTHING, AGGREGATE_BY_PK_PREFIX, AGGREGATE_BY_PK_PREFIX_WITH_SELECTOR; + + private static final Kind[] VALUES = values(); + + public static Kind fromOrdinal(int ordinal) + { + return VALUES[ordinal]; + } } /** @@ -253,7 +260,7 @@ public abstract class AggregationSpecification public AggregationSpecification deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException { - Kind kind = Kind.values()[in.readUnsignedByte()]; + Kind kind = Kind.fromOrdinal(in.readUnsignedByte()); switch (kind) { case AGGREGATE_EVERYTHING: diff --git a/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java b/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java index d55799a484..f9fdb3f2e6 100644 --- a/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java +++ b/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java @@ -80,7 +80,7 @@ public abstract class AbstractClusteringIndexFilter implements ClusteringIndexFi public ClusteringIndexFilter deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException { - Kind kind = Kind.values()[in.readUnsignedByte()]; + Kind kind = Kind.fromOrdinal(in.readUnsignedByte()); boolean reversed = in.readBoolean(); return kind.deserializer.deserialize(in, version, metadata, reversed); diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java index 2ed144a0d1..c3286f60bb 100644 --- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java +++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java @@ -46,12 +46,19 @@ public interface ClusteringIndexFilter SLICE (ClusteringIndexSliceFilter.deserializer), NAMES (ClusteringIndexNamesFilter.deserializer); + private static final Kind[] VALUES = values(); + protected final InternalDeserializer deserializer; private Kind(InternalDeserializer deserializer) { this.deserializer = deserializer; } + + public static Kind fromOrdinal(int ordinal) + { + return VALUES[ordinal]; + } } static interface InternalDeserializer diff --git a/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java b/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java index b4f0346c76..f2baad98e2 100644 --- a/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java +++ b/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java @@ -44,7 +44,17 @@ public abstract class ColumnSubselection implements Comparable // and this is why we have some UNUSEDX for values we don't use anymore // (we could clean those on a major protocol update, but it's not worth // the trouble for now) - protected enum Kind { SIMPLE, MAP_ELEMENT, UNUSED1, CUSTOM, USER } + protected enum Kind + { + SIMPLE, MAP_ELEMENT, UNUSED1, CUSTOM, USER; + + private static final Kind[] VALUES = values(); + + static Kind fromOrdinal(int ordinal) + { + return VALUES[ordinal]; + } + } protected abstract Kind kind(); protected final ColumnMetadata column; @@ -685,7 +695,7 @@ public class RowFilter implements Iterable public Expression deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException { - Kind kind = Kind.values()[in.readByte()]; + Kind kind = Kind.fromOrdinal(in.readByte()); // custom expressions (3.0+ only) do not contain a column or operator, only a value if (kind == Kind.CUSTOM) diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java index 990a07a1ce..ebae5d5394 100644 --- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java +++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java @@ -65,7 +65,7 @@ public interface GuardrailsConfigProvider */ static GuardrailsConfigProvider build(String customImpl) { - return FBUtilities.construct(customImpl, "custom guardrails config provider"); + return FBUtilities.construct(customImpl, "custom guardrails config provider", GuardrailsConfigProvider.class); } /** diff --git a/src/java/org/apache/cassandra/db/guardrails/ValueGenerator.java b/src/java/org/apache/cassandra/db/guardrails/ValueGenerator.java index c01e18f679..0a11734034 100644 --- a/src/java/org/apache/cassandra/db/guardrails/ValueGenerator.java +++ b/src/java/org/apache/cassandra/db/guardrails/ValueGenerator.java @@ -143,8 +143,11 @@ public abstract class ValueGenerator try { + Class rawGeneratorClass = + FBUtilities.classForNameWithoutInitialization(className, "generator", ValueGenerator.class); + @SuppressWarnings("unchecked") Class> generatorClass = - FBUtilities.classForName(className, "generator"); + (Class>) rawGeneratorClass; @SuppressWarnings("unchecked") ValueGenerator generator = generatorClass.getConstructor(CustomGuardrailConfig.class) @@ -165,4 +168,4 @@ public abstract class ValueGenerator className, message), ex); } } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/db/guardrails/ValueValidator.java b/src/java/org/apache/cassandra/db/guardrails/ValueValidator.java index 3aa9533ff5..8e93e129e0 100644 --- a/src/java/org/apache/cassandra/db/guardrails/ValueValidator.java +++ b/src/java/org/apache/cassandra/db/guardrails/ValueValidator.java @@ -127,8 +127,11 @@ public abstract class ValueValidator try { + Class rawValidatorClass = + FBUtilities.classForNameWithoutInitialization(className, "validator", ValueValidator.class); + @SuppressWarnings("unchecked") Class> validatorClass = - FBUtilities.classForName(className, "validator"); + (Class>) rawValidatorClass; @SuppressWarnings("unchecked") ValueValidator validator = validatorClass.getConstructor(CustomGuardrailConfig.class) diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractTimeUUIDType.java b/src/java/org/apache/cassandra/db/marshal/AbstractTimeUUIDType.java index ffa8fbc95b..fa1cd90273 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractTimeUUIDType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractTimeUUIDType.java @@ -39,7 +39,7 @@ public abstract class AbstractTimeUUIDType extends TemporalType { AbstractTimeUUIDType() { - super(ComparisonType.CUSTOM); + super(ComparisonType.CUSTOM, 16); } // singleton @Override @@ -193,12 +193,6 @@ public abstract class AbstractTimeUUIDType extends TemporalType return super.decomposeUntyped(value); } - @Override - public int valueLengthIfFixed() - { - return 16; - } - @Override public long toTimeInMillis(ByteBuffer value) { diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java index 09545d486e..257a64f931 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java @@ -90,11 +90,18 @@ public abstract class AbstractType implements Comparator, Assignm public final ComparisonType comparisonType; public final boolean isByteOrderComparable; public final ValueComparators comparatorSet; + private final int valueLengthIfFixed; protected AbstractType(ComparisonType comparisonType) + { + this(comparisonType, VARIABLE_LENGTH); + } + + protected AbstractType(ComparisonType comparisonType, int valueLengthIfFixed) { this.comparisonType = comparisonType; this.isByteOrderComparable = comparisonType == ComparisonType.BYTE_ORDER; + this.valueLengthIfFixed = valueLengthIfFixed; reverseComparator = (o1, o2) -> AbstractType.this.compare(o2, o1); try { @@ -384,12 +391,12 @@ public abstract class AbstractType implements Comparator, Assignm /** * Similar to {@link #isValueCompatibleWith(AbstractType)}, but takes into account {@link Cell} encoding. * In particular, this method doesn't consider two types serialization compatible if one of them has fixed - * length (overrides {@link #valueLengthIfFixed()}, and the other one doesn't. + * length, and the other one doesn't. */ public boolean isSerializationCompatibleWith(AbstractType previous) { return isValueCompatibleWith(previous) - && valueLengthIfFixed() == previous.valueLengthIfFixed() + && valueLengthIfFixed == previous.valueLengthIfFixed && isMultiCell() == previous.isMultiCell(); } @@ -498,7 +505,7 @@ public abstract class AbstractType implements Comparator, Assignm */ public int valueLengthIfFixed() { - return VARIABLE_LENGTH; + return valueLengthIfFixed; } /** @@ -508,7 +515,7 @@ public abstract class AbstractType implements Comparator, Assignm */ public final boolean isValueLengthFixed() { - return valueLengthIfFixed() != VARIABLE_LENGTH; + return valueLengthIfFixed != VARIABLE_LENGTH; } /** @@ -570,7 +577,7 @@ public abstract class AbstractType implements Comparator, Assignm public void writeValue(V value, ValueAccessor accessor, DataOutputPlus out) throws IOException { assert !isNull(value, accessor) : "bytes should not be null for type " + this; - int expectedValueLength = valueLengthIfFixed(); + int expectedValueLength = valueLengthIfFixed; if (expectedValueLength >= 0) { int actualValueLength = accessor.size(value); @@ -589,7 +596,7 @@ public abstract class AbstractType implements Comparator, Assignm public void writeValue(IndexedValueHolder valueHolder, int i, ValueAccessor accessor, DataOutputPlus out) throws IOException { assert !valueHolder.isNull(i) : "bytes should not be null for type " + this; - int expectedValueLength = valueLengthIfFixed(); + int expectedValueLength = valueLengthIfFixed; if (expectedValueLength >= 0) { int actualValueLength = valueHolder.size(i); @@ -613,7 +620,7 @@ public abstract class AbstractType implements Comparator, Assignm public long writtenLength(V value, ValueAccessor accessor) { assert !accessor.isEmpty(value) : "bytes should not be empty for type " + this; - return valueLengthIfFixed() >= 0 + return valueLengthIfFixed >= 0 ? accessor.size(value) // if the size is wrong, this will be detected in writeValue : accessor.sizeWithVIntLength(value); } @@ -621,7 +628,7 @@ public abstract class AbstractType implements Comparator, Assignm public long writtenLength(IndexedValueHolder valueHolder, int i, ValueAccessor accessor) { assert !valueHolder.isNull(i) : "bytes should not be null for type " + this; - return valueLengthIfFixed() >= 0 + return valueLengthIfFixed >= 0 ? valueHolder.size(i) // if the size is wrong, this will be detected in writeValue : accessor.sizeWithVIntLength(valueHolder, i); } @@ -643,7 +650,7 @@ public abstract class AbstractType implements Comparator, Assignm public V read(ValueAccessor accessor, DataInputPlus in, int maxValueSize) throws IOException { - int length = valueLengthIfFixed(); + int length = valueLengthIfFixed; if (length >= 0) return accessor.read(in, length); @@ -664,7 +671,7 @@ public abstract class AbstractType implements Comparator, Assignm public void skipValue(DataInputPlus in) throws IOException { - int length = valueLengthIfFixed(); + int length = valueLengthIfFixed; if (length >= 0) in.skipBytesFully(length); else diff --git a/src/java/org/apache/cassandra/db/marshal/BooleanType.java b/src/java/org/apache/cassandra/db/marshal/BooleanType.java index 171b239ad2..7f6238bbd6 100644 --- a/src/java/org/apache/cassandra/db/marshal/BooleanType.java +++ b/src/java/org/apache/cassandra/db/marshal/BooleanType.java @@ -36,7 +36,7 @@ public class BooleanType extends AbstractType private static final ArgumentDeserializer ARGUMENT_DESERIALIZER = new DefaultArgumentDeserializer(instance); private static final ByteBuffer MASKED_VALUE = instance.decompose(false); - BooleanType() {super(ComparisonType.CUSTOM);} // singleton + BooleanType() {super(ComparisonType.CUSTOM, 1);} // singleton @Override public boolean allowsEmpty() @@ -127,12 +127,6 @@ public class BooleanType extends AbstractType return ARGUMENT_DESERIALIZER; } - @Override - public int valueLengthIfFixed() - { - return 1; - } - @Override public ByteBuffer getMaskedValue() { diff --git a/src/java/org/apache/cassandra/db/marshal/DateType.java b/src/java/org/apache/cassandra/db/marshal/DateType.java index 87a31ade55..6442c159a0 100644 --- a/src/java/org/apache/cassandra/db/marshal/DateType.java +++ b/src/java/org/apache/cassandra/db/marshal/DateType.java @@ -49,7 +49,7 @@ public class DateType extends AbstractType private static final ArgumentDeserializer ARGUMENT_DESERIALIZER = new DefaultArgumentDeserializer(instance); private static final ByteBuffer MASKED_VALUE = instance.decompose(new Date(0)); - DateType() {super(ComparisonType.BYTE_ORDER);} // singleton + DateType() {super(ComparisonType.BYTE_ORDER, 8);} // singleton public boolean isEmptyValueMeaningless() { @@ -143,12 +143,6 @@ public class DateType extends AbstractType return ARGUMENT_DESERIALIZER; } - @Override - public int valueLengthIfFixed() - { - return 8; - } - @Override public ByteBuffer getMaskedValue() { diff --git a/src/java/org/apache/cassandra/db/marshal/DoubleType.java b/src/java/org/apache/cassandra/db/marshal/DoubleType.java index 2d065e299d..2e423e91bf 100644 --- a/src/java/org/apache/cassandra/db/marshal/DoubleType.java +++ b/src/java/org/apache/cassandra/db/marshal/DoubleType.java @@ -40,7 +40,7 @@ public class DoubleType extends NumberType private static final ByteBuffer MASKED_VALUE = instance.decompose(0d); - DoubleType() {super(ComparisonType.CUSTOM);} // singleton + DoubleType() {super(ComparisonType.CUSTOM, 8);} // singleton @Override public boolean allowsEmpty() @@ -145,12 +145,6 @@ public class DoubleType extends NumberType }; } - @Override - public int valueLengthIfFixed() - { - return 8; - } - @Override public ByteBuffer add(Number left, Number right) { diff --git a/src/java/org/apache/cassandra/db/marshal/EmptyType.java b/src/java/org/apache/cassandra/db/marshal/EmptyType.java index 23553111f7..98c82c87a1 100644 --- a/src/java/org/apache/cassandra/db/marshal/EmptyType.java +++ b/src/java/org/apache/cassandra/db/marshal/EmptyType.java @@ -71,7 +71,7 @@ public class EmptyType extends AbstractType public static final EmptyType instance = new EmptyType(); - private EmptyType() {super(ComparisonType.CUSTOM);} // singleton + private EmptyType() {super(ComparisonType.CUSTOM, 0);} // singleton @Override public ByteSource asComparableBytes(ValueAccessor accessor, V data, ByteComparable.Version version) @@ -137,12 +137,6 @@ public class EmptyType extends AbstractType throw new UnsupportedOperationException(); } - @Override - public int valueLengthIfFixed() - { - return 0; - } - @Override public long writtenLength(V value, ValueAccessor accessor) { diff --git a/src/java/org/apache/cassandra/db/marshal/FloatType.java b/src/java/org/apache/cassandra/db/marshal/FloatType.java index 9ccb7fca18..38bc5de766 100644 --- a/src/java/org/apache/cassandra/db/marshal/FloatType.java +++ b/src/java/org/apache/cassandra/db/marshal/FloatType.java @@ -41,7 +41,7 @@ public class FloatType extends NumberType private static final ByteBuffer MASKED_VALUE = instance.decompose(0f); - FloatType() {super(ComparisonType.CUSTOM);} // singleton + FloatType() {super(ComparisonType.CUSTOM, 4);} // singleton @Override public boolean allowsEmpty() @@ -146,12 +146,6 @@ public class FloatType extends NumberType }; } - @Override - public int valueLengthIfFixed() - { - return 4; - } - @Override public ByteBuffer add(Number left, Number right) { diff --git a/src/java/org/apache/cassandra/db/marshal/Int32Type.java b/src/java/org/apache/cassandra/db/marshal/Int32Type.java index a68255edd3..fea3e1f366 100644 --- a/src/java/org/apache/cassandra/db/marshal/Int32Type.java +++ b/src/java/org/apache/cassandra/db/marshal/Int32Type.java @@ -43,7 +43,7 @@ public class Int32Type extends NumberType Int32Type() { - super(ComparisonType.CUSTOM); + super(ComparisonType.CUSTOM, 4); } // singleton @Override @@ -152,12 +152,6 @@ public class Int32Type extends NumberType }; } - @Override - public int valueLengthIfFixed() - { - return 4; - } - @Override public ByteBuffer add(Number left, Number right) { diff --git a/src/java/org/apache/cassandra/db/marshal/LexicalUUIDType.java b/src/java/org/apache/cassandra/db/marshal/LexicalUUIDType.java index fab451b291..4ee3024efb 100644 --- a/src/java/org/apache/cassandra/db/marshal/LexicalUUIDType.java +++ b/src/java/org/apache/cassandra/db/marshal/LexicalUUIDType.java @@ -44,7 +44,7 @@ public class LexicalUUIDType extends AbstractType LexicalUUIDType() { - super(ComparisonType.CUSTOM); + super(ComparisonType.CUSTOM, 16); } // singleton @Override @@ -148,12 +148,6 @@ public class LexicalUUIDType extends AbstractType return ARGUMENT_DESERIALIZER; } - @Override - public int valueLengthIfFixed() - { - return 16; - } - @Override public ByteBuffer getMaskedValue() { diff --git a/src/java/org/apache/cassandra/db/marshal/LongType.java b/src/java/org/apache/cassandra/db/marshal/LongType.java index 400b6ae836..41ae32186a 100644 --- a/src/java/org/apache/cassandra/db/marshal/LongType.java +++ b/src/java/org/apache/cassandra/db/marshal/LongType.java @@ -41,7 +41,7 @@ public class LongType extends NumberType private static final ByteBuffer MASKED_VALUE = instance.decompose(0L); - LongType() {super(ComparisonType.CUSTOM);} // singleton + LongType() {super(ComparisonType.CUSTOM, 8);} // singleton @Override public boolean allowsEmpty() @@ -170,12 +170,6 @@ public class LongType extends NumberType }; } - @Override - public int valueLengthIfFixed() - { - return 8; - } - @Override public ByteBuffer add(Number left, Number right) { diff --git a/src/java/org/apache/cassandra/db/marshal/MultiElementType.java b/src/java/org/apache/cassandra/db/marshal/MultiElementType.java index dc8f912e09..2bbede82ef 100644 --- a/src/java/org/apache/cassandra/db/marshal/MultiElementType.java +++ b/src/java/org/apache/cassandra/db/marshal/MultiElementType.java @@ -38,6 +38,11 @@ public abstract class MultiElementType extends AbstractType super(comparisonType); } + protected MultiElementType(ComparisonType comparisonType, int valueLengthIfFixed) + { + super(comparisonType, valueLengthIfFixed); + } + /** * Returns the serialized representation of the value composed of the specified elements. * @@ -133,4 +138,3 @@ public abstract class MultiElementType extends AbstractType throw new UnsupportedOperationException(this + " does not support retrieving elements by key or index"); } } - diff --git a/src/java/org/apache/cassandra/db/marshal/NumberType.java b/src/java/org/apache/cassandra/db/marshal/NumberType.java index 04c6e1dc1b..c007dac44d 100644 --- a/src/java/org/apache/cassandra/db/marshal/NumberType.java +++ b/src/java/org/apache/cassandra/db/marshal/NumberType.java @@ -34,6 +34,11 @@ public abstract class NumberType extends AbstractType super(comparisonType); } + protected NumberType(ComparisonType comparisonType, int valueLengthIfFixed) + { + super(comparisonType, valueLengthIfFixed); + } + /** * Checks if this type support floating point numbers. * @return {@code true} if this type support floating point numbers, {@code false} otherwise. diff --git a/src/java/org/apache/cassandra/db/marshal/ReversedType.java b/src/java/org/apache/cassandra/db/marshal/ReversedType.java index 5c81f0e558..85e4b44273 100644 --- a/src/java/org/apache/cassandra/db/marshal/ReversedType.java +++ b/src/java/org/apache/cassandra/db/marshal/ReversedType.java @@ -57,7 +57,7 @@ public class ReversedType extends AbstractType private ReversedType(AbstractType baseType) { - super(ComparisonType.CUSTOM); + super(ComparisonType.CUSTOM, baseType.valueLengthIfFixed()); this.baseType = baseType; } @@ -181,12 +181,6 @@ public class ReversedType extends AbstractType return getInstance(baseType.withUpdatedUserType(udt)); } - @Override - public int valueLengthIfFixed() - { - return baseType.valueLengthIfFixed(); - } - @Override public boolean isReversed() { diff --git a/src/java/org/apache/cassandra/db/marshal/TemporalType.java b/src/java/org/apache/cassandra/db/marshal/TemporalType.java index 5f5c4564d5..24f0eaa7a4 100644 --- a/src/java/org/apache/cassandra/db/marshal/TemporalType.java +++ b/src/java/org/apache/cassandra/db/marshal/TemporalType.java @@ -37,6 +37,11 @@ public abstract class TemporalType extends AbstractType super(comparisonType); } + protected TemporalType(ComparisonType comparisonType, int valueLengthIfFixed) + { + super(comparisonType, valueLengthIfFixed); + } + /** * Returns the current temporal value. * @return the current temporal value. diff --git a/src/java/org/apache/cassandra/db/marshal/TimestampType.java b/src/java/org/apache/cassandra/db/marshal/TimestampType.java index b408fdda53..68ac6c8919 100644 --- a/src/java/org/apache/cassandra/db/marshal/TimestampType.java +++ b/src/java/org/apache/cassandra/db/marshal/TimestampType.java @@ -53,7 +53,7 @@ public class TimestampType extends TemporalType private static final ByteBuffer MASKED_VALUE = instance.decompose(new Date(0)); - private TimestampType() {super(ComparisonType.CUSTOM);} // singleton + private TimestampType() {super(ComparisonType.CUSTOM, 8);} // singleton @Override public boolean allowsEmpty() @@ -166,12 +166,6 @@ public class TimestampType extends TemporalType return TimestampSerializer.instance; } - @Override - public int valueLengthIfFixed() - { - return 8; - } - @Override protected void validateDuration(Duration duration) { diff --git a/src/java/org/apache/cassandra/db/marshal/TypeParser.java b/src/java/org/apache/cassandra/db/marshal/TypeParser.java index b2dc0202bc..d3a20fc7ca 100644 --- a/src/java/org/apache/cassandra/db/marshal/TypeParser.java +++ b/src/java/org/apache/cassandra/db/marshal/TypeParser.java @@ -449,8 +449,7 @@ public class TypeParser private static AbstractType getAbstractType(String compareWith) throws ConfigurationException { - String className = compareWith.contains(".") ? compareWith : "org.apache.cassandra.db.marshal." + compareWith; - Class> typeClass = FBUtilities.>classForName(className, "abstract-type"); + Class> typeClass = getAbstractTypeClass(compareWith); try { Field field = typeClass.getDeclaredField("instance"); @@ -465,8 +464,7 @@ public class TypeParser private static AbstractType getAbstractType(String compareWith, TypeParser parser) throws SyntaxException, ConfigurationException { - String className = compareWith.contains(".") ? compareWith : "org.apache.cassandra.db.marshal." + compareWith; - Class> typeClass = FBUtilities.>classForName(className, "abstract-type"); + Class> typeClass = getAbstractTypeClass(compareWith); if (PseudoUtf8Type.class.isAssignableFrom(typeClass)) { if (StorageService.instance.isDaemonSetupCompleted()) @@ -491,6 +489,19 @@ public class TypeParser } } + private static Class> getAbstractTypeClass(String compareWith) throws ConfigurationException + { + String className = compareWith.contains(".") ? compareWith : "org.apache.cassandra.db.marshal." + compareWith; + // Defer class initialization until after confirming this is an AbstractType. The static instance field + // access or getInstance(TypeParser) invocation below performs the initialization for valid types. + @SuppressWarnings("unchecked") + Class> typeClass = + (Class>) FBUtilities.classForNameWithoutInitialization(className, + "abstract-type", + AbstractType.class); + return typeClass; + } + private static AbstractType getRawAbstractType(Class> typeClass) throws ConfigurationException { try diff --git a/src/java/org/apache/cassandra/db/marshal/UUIDType.java b/src/java/org/apache/cassandra/db/marshal/UUIDType.java index 2b526f3cef..cf36f99dee 100644 --- a/src/java/org/apache/cassandra/db/marshal/UUIDType.java +++ b/src/java/org/apache/cassandra/db/marshal/UUIDType.java @@ -56,7 +56,7 @@ public class UUIDType extends AbstractType UUIDType() { - super(ComparisonType.CUSTOM); + super(ComparisonType.CUSTOM, 16); } @Override @@ -252,12 +252,6 @@ public class UUIDType extends AbstractType return (uuid.get(6) & 0xf0) >> 4; } - @Override - public int valueLengthIfFixed() - { - return 16; - } - @Override public ByteBuffer getMaskedValue() { diff --git a/src/java/org/apache/cassandra/db/marshal/VectorType.java b/src/java/org/apache/cassandra/db/marshal/VectorType.java index 3cdf72270e..74123c126e 100644 --- a/src/java/org/apache/cassandra/db/marshal/VectorType.java +++ b/src/java/org/apache/cassandra/db/marshal/VectorType.java @@ -82,20 +82,16 @@ public final class VectorType extends MultiElementType> public final AbstractType elementType; public final int dimension; private final TypeSerializer elementSerializer; - private final int valueLengthIfFixed; private final VectorSerializer serializer; private VectorType(AbstractType elementType, int dimension) { - super(ComparisonType.CUSTOM); + super(ComparisonType.CUSTOM, valueLengthIfFixed(elementType, dimension)); if (dimension <= 0) throw new InvalidRequestException(String.format("vectors may only have positive dimensions; given %d", dimension)); this.elementType = elementType; this.dimension = dimension; this.elementSerializer = elementType.getSerializer(); - this.valueLengthIfFixed = elementType.isValueLengthFixed() ? - elementType.valueLengthIfFixed() * dimension : - super.valueLengthIfFixed(); this.serializer = elementType.isValueLengthFixed() ? new FixedLengthSerializer() : new VariableLengthSerializer(); @@ -126,10 +122,10 @@ public final class VectorType extends MultiElementType> return getSerializer().compareCustom(left, accessorL, right, accessorR); } - @Override - public int valueLengthIfFixed() + private static int valueLengthIfFixed(AbstractType elementType, int dimension) { - return valueLengthIfFixed; + int elementLength = elementType.valueLengthIfFixed(); + return elementLength >= 0 ? elementLength * dimension : elementLength; } @Override diff --git a/src/java/org/apache/cassandra/db/rows/AbstractCell.java b/src/java/org/apache/cassandra/db/rows/AbstractCell.java index c398839742..ea6110cbf6 100644 --- a/src/java/org/apache/cassandra/db/rows/AbstractCell.java +++ b/src/java/org/apache/cassandra/db/rows/AbstractCell.java @@ -61,7 +61,18 @@ public abstract class AbstractCell extends Cell public boolean isTombstone() { - return localDeletionTime() != NO_DELETION_TIME && ttl() == NO_TTL; + return isTombstone(localDeletionTime()); + } + + public long minDeletionTime() + { + long localDeletionTime = localDeletionTime(); + return isTombstone(localDeletionTime) ? Long.MIN_VALUE : localDeletionTime; + } + + private boolean isTombstone(long localDeletionTime) + { + return localDeletionTime != NO_DELETION_TIME && ttl() == NO_TTL; } public boolean isExpiring() diff --git a/src/java/org/apache/cassandra/db/rows/ArrayCell.java b/src/java/org/apache/cassandra/db/rows/ArrayCell.java index 77d44c3eb7..c74103f2e0 100644 --- a/src/java/org/apache/cassandra/db/rows/ArrayCell.java +++ b/src/java/org/apache/cassandra/db/rows/ArrayCell.java @@ -31,17 +31,12 @@ import org.apache.cassandra.utils.memory.ByteBufferCloner; import static org.apache.cassandra.utils.ByteArrayUtil.EMPTY_BYTE_ARRAY; -public class ArrayCell extends AbstractCell +public class ArrayCell extends HeapAbstractCell { private static final long EMPTY_SIZE = ObjectSizes.measure(new ArrayCell(ColumnMetadata.regularColumn("", "", "", ByteType.instance, ColumnMetadata.NO_UNIQUE_ID), 0L, 0, 0, EMPTY_BYTE_ARRAY, null)); // Careful: Adding vars here has an impact on memtable size - private final long timestamp; - private final int ttl; - private final int localDeletionTimeUnsignedInteger; - private final byte[] value; - private final CellPath path; // Please keep both int/long overloaded ctros public. Otherwise silent casts will mess timestamps when one is not // available. @@ -52,12 +47,8 @@ public class ArrayCell extends AbstractCell public ArrayCell(ColumnMetadata column, long timestamp, int ttl, int localDeletionTimeUnsignedInteger, byte[] value, CellPath path) { - super(column); - this.timestamp = timestamp; - this.ttl = ttl; - this.localDeletionTimeUnsignedInteger = localDeletionTimeUnsignedInteger; + super(column, timestamp, ttl, localDeletionTimeUnsignedInteger, path); this.value = value; - this.path = path; } public static ArrayCell live(ColumnMetadata column, long timestamp, byte[] value, CellPath path) @@ -71,16 +62,6 @@ public class ArrayCell extends AbstractCell return new ArrayCell(column, timestamp, ttl, ExpirationDateOverflowHandling.computeLocalExpirationTime(nowInSec, ttl), value, path); } - public long timestamp() - { - return timestamp; - } - - public int ttl() - { - return ttl; - } - public byte[] value() { return value; @@ -91,10 +72,6 @@ public class ArrayCell extends AbstractCell return ByteArrayAccessor.instance; } - public CellPath path() - { - return path; - } public Cell withUpdatedColumn(ColumnMetadata newColumn) { @@ -144,9 +121,4 @@ public class ArrayCell extends AbstractCell return EMPTY_SIZE + ObjectSizes.sizeOfArray(value) - value.length + (path == null ? 0 : path.unsharedHeapSizeExcludingData()); } - @Override - protected int localDeletionTimeAsUnsignedInt() - { - return localDeletionTimeUnsignedInteger; - } } diff --git a/src/java/org/apache/cassandra/db/rows/BTreeRow.java b/src/java/org/apache/cassandra/db/rows/BTreeRow.java index b8df8450c1..82cf43c6a7 100644 --- a/src/java/org/apache/cassandra/db/rows/BTreeRow.java +++ b/src/java/org/apache/cassandra/db/rows/BTreeRow.java @@ -172,7 +172,7 @@ public class BTreeRow extends AbstractRow private static long minDeletionTime(Cell cell) { - return cell.isTombstone() ? Long.MIN_VALUE : cell.localDeletionTime(); + return cell.minDeletionTime(); } private static long minDeletionTime(LivenessInfo info) @@ -439,6 +439,11 @@ public class BTreeRow extends AbstractRow return nowInSec >= minLocalDeletionTime; } + public long minLocalDeletionTime() + { + return minLocalDeletionTime; + } + public boolean hasInvalidDeletions() { if (primaryKeyLivenessInfo().isExpiring() && (primaryKeyLivenessInfo().ttl() < 0 || primaryKeyLivenessInfo().localExpirationTime() < 0)) diff --git a/src/java/org/apache/cassandra/db/rows/BufferCell.java b/src/java/org/apache/cassandra/db/rows/BufferCell.java index fa0826fa57..3f8de1d5fe 100644 --- a/src/java/org/apache/cassandra/db/rows/BufferCell.java +++ b/src/java/org/apache/cassandra/db/rows/BufferCell.java @@ -30,17 +30,12 @@ import org.apache.cassandra.utils.memory.ByteBufferCloner; import static java.lang.String.format; -public class BufferCell extends AbstractCell +public class BufferCell extends HeapAbstractCell { private static final long EMPTY_SIZE = ObjectSizes.measure(new BufferCell(ColumnMetadata.regularColumn("", "", "", ByteType.instance, ColumnMetadata.NO_UNIQUE_ID), 0L, 0, 0, ByteBufferUtil.EMPTY_BYTE_BUFFER, null)); // Careful: Adding vars here has an impact on memtable size - private final long timestamp; - private final int ttl; - private final int localDeletionTimeUnsignedInteger; - private final ByteBuffer value; - private final CellPath path; // Please keep both int/long overloaded ctros public. Otherwise silent casts will mess timestamps when one is not // available. @@ -51,14 +46,10 @@ public class BufferCell extends AbstractCell public BufferCell(ColumnMetadata column, long timestamp, int ttl, int localDeletionTimeUnsignedInteger, ByteBuffer value, CellPath path) { - super(column); + super(column, timestamp, ttl, localDeletionTimeUnsignedInteger, path); assert !column.isPrimaryKeyColumn(); assert column.isComplex() == (path != null) : format("Column %s.%s(%s: %s) isComplex: %b with cellpath: %s", column.ksName, column.cfName, column.name, column.type.toString(), column.isComplex(), path); - this.timestamp = timestamp; - this.ttl = ttl; - this.localDeletionTimeUnsignedInteger = localDeletionTimeUnsignedInteger; this.value = value; - this.path = path; } public static BufferCell live(ColumnMetadata column, long timestamp, ByteBuffer value) @@ -92,16 +83,6 @@ public class BufferCell extends AbstractCell return new BufferCell(column, timestamp, NO_TTL, nowInSec, ByteBufferUtil.EMPTY_BYTE_BUFFER, path); } - public long timestamp() - { - return timestamp; - } - - public int ttl() - { - return ttl; - } - public ByteBuffer value() { return value; @@ -112,10 +93,6 @@ public class BufferCell extends AbstractCell return ByteBufferAccessor.instance; } - public CellPath path() - { - return path; - } public Cell withUpdatedColumn(ColumnMetadata newColumn) { @@ -163,10 +140,4 @@ public class BufferCell extends AbstractCell { return EMPTY_SIZE + ObjectSizes.sizeOnHeapExcludingDataOf(value) + (path == null ? 0 : path.unsharedHeapSizeExcludingData()); } - - @Override - protected int localDeletionTimeAsUnsignedInt() - { - return localDeletionTimeUnsignedInteger; - } } diff --git a/src/java/org/apache/cassandra/db/rows/Cell.java b/src/java/org/apache/cassandra/db/rows/Cell.java index 6dea396993..5a76d1bcd4 100644 --- a/src/java/org/apache/cassandra/db/rows/Cell.java +++ b/src/java/org/apache/cassandra/db/rows/Cell.java @@ -150,6 +150,8 @@ public abstract class Cell extends ColumnData return deletionTimeUnsignedIntegerToLong(localDeletionTimeAsUnsignedInt()); } + public abstract long minDeletionTime(); + /** * Whether the cell is a tombstone or not. * diff --git a/src/java/org/apache/cassandra/db/rows/HeapAbstractCell.java b/src/java/org/apache/cassandra/db/rows/HeapAbstractCell.java new file mode 100644 index 0000000000..d44186b2e9 --- /dev/null +++ b/src/java/org/apache/cassandra/db/rows/HeapAbstractCell.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db.rows; + +import org.apache.cassandra.schema.ColumnMetadata; + +public abstract class HeapAbstractCell extends AbstractCell +{ + // Careful: Adding vars here has an impact on memtable size + protected final long timestamp; + protected final int ttl; + protected final int localDeletionTimeUnsignedInteger; + + protected final CellPath path; + + protected HeapAbstractCell(ColumnMetadata column, long timestamp, int ttl, int localDeletionTimeUnsignedInteger, CellPath path) + { + super(column); + this.timestamp = timestamp; + this.ttl = ttl; + this.localDeletionTimeUnsignedInteger = localDeletionTimeUnsignedInteger; + this.path = path; + } + + @Override + public long timestamp() + { + return timestamp; + } + + @Override + public int ttl() + { + return ttl; + } + + @Override + protected int localDeletionTimeAsUnsignedInt() + { + return localDeletionTimeUnsignedInteger; + } + + @Override + public CellPath path() + { + return path; + } +} diff --git a/src/java/org/apache/cassandra/db/rows/Row.java b/src/java/org/apache/cassandra/db/rows/Row.java index 9c281408ff..4137c42ca2 100644 --- a/src/java/org/apache/cassandra/db/rows/Row.java +++ b/src/java/org/apache/cassandra/db/rows/Row.java @@ -20,7 +20,6 @@ package org.apache.cassandra.db.rows; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -44,9 +43,10 @@ import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.paxos.Commit; import org.apache.cassandra.utils.BiLongAccumulator; import org.apache.cassandra.utils.BulkIterator; +import org.apache.cassandra.utils.ComplexCellMergeIterator; import org.apache.cassandra.utils.LongAccumulator; -import org.apache.cassandra.utils.MergeIterator; import org.apache.cassandra.utils.ObjectSizes; +import org.apache.cassandra.utils.RowMergeIterator; import org.apache.cassandra.utils.SearchIterator; import org.apache.cassandra.utils.btree.BTree; import org.apache.cassandra.utils.btree.UpdateFunction; @@ -221,6 +221,15 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory */ public boolean hasDeletion(long nowInSec); + /** + * The smallest local deletion time of all the data in this row (row deletion, primary key liveness, cells and + * complex deletions), or {@link Cell#MAX_DELETION_TIME} if the row has no deletion nor expiring data. + *

+ * Unlike {@link #hasDeletion(long)}, this value is independent of the current time. In particular, a value of + * {@link Cell#MAX_DELETION_TIME} guarantees the row carries neither tombstones nor expiring data. + */ + public long minLocalDeletionTime(); + /** * An iterator to efficiently search data for a given column. * @@ -762,6 +771,11 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory LivenessInfo rowInfo = LivenessInfo.EMPTY; Deletion rowDeletion = Deletion.LIVE; + int columnsCountEstimation = 0; + // Track the smallest local deletion time across all inputs: if none of them carries any deletion or + // expiring data (i.e. this stays at MAX_DELETION_TIME), the merged row can't either, so we can hand the + // value to BTreeRow.create() below and skip the full btree scan it would otherwise do to recompute it. + long minDeletionTime = Cell.MAX_DELETION_TIME; for (Row row : rows) { if (row == null) @@ -771,6 +785,11 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory rowInfo = row.primaryKeyLivenessInfo(); if (row.deletion().supersedes(rowDeletion)) rowDeletion = row.deletion(); + + minDeletionTime = Math.min(minDeletionTime, row.minLocalDeletionTime()); + + columnDataIterators.add(row.iterator()); + columnsCountEstimation = Math.max(columnsCountEstimation, row.columnCount()); } if (rowDeletion.isShadowedBy(rowInfo)) @@ -784,25 +803,12 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory if (activeDeletion.deletes(rowInfo)) rowInfo = LivenessInfo.EMPTY; - int columnsCountEstimation = 0; - for (Row row : rows) - { - if (row != null) - { - columnDataIterators.add(row.iterator()); - columnsCountEstimation = Math.max(columnsCountEstimation, row.columnCount()); - } - else - { - columnDataIterators.add(Collections.emptyIterator()); - } - } // try to estimate and set a potential target capacity if (dataBuffer.length < columnsCountEstimation) dataBuffer = new ColumnData[columnsCountEstimation]; columnDataReducer.setActiveDeletion(activeDeletion); - Iterator merged = MergeIterator.get(columnDataIterators, ColumnData.comparator, columnDataReducer); + Iterator merged = RowMergeIterator.get(columnDataIterators, ColumnData.comparator, columnDataReducer); while (merged.hasNext()) { ColumnData data = merged.next(); @@ -819,8 +825,12 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory try (BulkIterator it = BulkIterator.of(dataBuffer)) { - return BTreeRow.create(clustering, rowInfo, rowDeletion, - BTree.build(it, dataBufferSize, UpdateFunction.noOp())); + Object[] tree = BTree.build(it, dataBufferSize, UpdateFunction.noOp()); + // If none of the merged rows had any deletion or expiring data, neither does the result, so we can + // pass the already-known min local deletion time and avoid rescanning the whole btree to recompute it. + return minDeletionTime == Cell.MAX_DELETION_TIME + ? BTreeRow.create(clustering, rowInfo, rowDeletion, tree, Cell.MAX_DELETION_TIME) + : BTreeRow.create(clustering, rowInfo, rowDeletion, tree); } } @@ -841,10 +851,11 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory return rows; } - private static class ColumnDataReducer extends MergeIterator.Reducer + private static class ColumnDataReducer extends RowMergeIterator.Reducer { private ColumnMetadata column; - private final List versions; + private final ColumnData[] versions; + private int versionsSize; private DeletionTime activeDeletion; @@ -854,7 +865,7 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory public ColumnDataReducer(int size, boolean hasComplex) { - this.versions = new ArrayList<>(size); + this.versions = new ColumnData[size]; this.complexBuilder = hasComplex ? ComplexColumnData.builder() : null; this.complexCells = hasComplex ? new ArrayList<>(size) : null; this.cellReducer = new CellReducer(); @@ -870,20 +881,24 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory if (useColumnMetadata(data.column())) column = data.column(); - versions.add(data); + versions[versionsSize++] = data; } /** - * Determines it the {@code ColumnMetadata} is the one that should be used. - * @param dataColumn the {@code ColumnMetadata} to use. - * @return {@code true} if the {@code ColumnMetadata} is the one that should be used, {@code false} otherwise. + * Determines whether {@code dataColumn} should replace the currently selected column metadata, + * i.e. whether no column has been selected yet or {@code dataColumn} is a newer version. + * @param dataColumn the candidate {@code ColumnMetadata} to evaluate. + * @return {@code true} if {@code dataColumn} should be used, {@code false} otherwise. */ private boolean useColumnMetadata(ColumnMetadata dataColumn) { - if (column == null) + ColumnMetadata currentColumn = column; + if (currentColumn == null) return true; + if (currentColumn == dataColumn) + return false; - return ColumnMetadataVersionComparator.INSTANCE.compare(column, dataColumn) < 0; + return ColumnMetadataVersionComparator.INSTANCE.compare(currentColumn, dataColumn) < 0; } protected ColumnData getReduced() @@ -891,9 +906,9 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory if (column.isSimple()) { Cell merged = null; - for (int i=0, isize=versions.size(); i cell = (Cell) versions.get(i); + Cell cell = (Cell) versions[i]; if (!activeDeletion.deletes(cell)) merged = merged == null ? cell : Cells.reconcile(merged, cell); } @@ -904,9 +919,9 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory complexBuilder.newColumn(column); complexCells.clear(); DeletionTime complexDeletion = DeletionTime.LIVE; - for (int i=0, isize=versions.size(); i, IMeasurableMemory cellReducer.setActiveDeletion(activeDeletion); } - Iterator> cells = MergeIterator.get(complexCells, Cell.comparator, cellReducer); + Iterator> cells = ComplexCellMergeIterator.get(complexCells, Cell.comparator, cellReducer); while (cells.hasNext()) { Cell merged = cells.next(); @@ -937,11 +952,12 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory protected void onKeyChange() { column = null; - versions.clear(); + Arrays.fill(versions, 0, versionsSize, null); + versionsSize = 0; } } - private static class CellReducer extends MergeIterator.Reducer, Cell> + private static class CellReducer extends ComplexCellMergeIterator.Reducer, Cell> { private DeletionTime activeDeletion; private Cell merged; diff --git a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterators.java b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterators.java index bc4bfd15ba..a306841892 100644 --- a/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterators.java +++ b/src/java/org/apache/cassandra/db/rows/UnfilteredRowIterators.java @@ -38,7 +38,7 @@ import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.IMergeIterator; -import org.apache.cassandra.utils.MergeIterator; +import org.apache.cassandra.utils.UnfilteredMergeIterator; /** * Static methods to work with atom iterators. @@ -415,7 +415,7 @@ public abstract class UnfilteredRowIterators reversed, EncodingStats.merge(iterators, UnfilteredRowIterator::stats)); - this.mergeIterator = MergeIterator.get(iterators, + this.mergeIterator = UnfilteredMergeIterator.get(iterators, reversed ? metadata.comparator.reversed() : metadata.comparator, new MergeReducer(iterators.size(), reversed, listener)); this.listener = listener; @@ -540,7 +540,7 @@ public abstract class UnfilteredRowIterators listener.close(); } - private class MergeReducer extends MergeIterator.Reducer + private class MergeReducer extends UnfilteredMergeIterator.Reducer { private final MergeListener listener; diff --git a/src/java/org/apache/cassandra/diag/DiagnosticEventPersistence.java b/src/java/org/apache/cassandra/diag/DiagnosticEventPersistence.java index 81820382f3..b1e8df8783 100644 --- a/src/java/org/apache/cassandra/diag/DiagnosticEventPersistence.java +++ b/src/java/org/apache/cassandra/diag/DiagnosticEventPersistence.java @@ -128,6 +128,7 @@ public final class DiagnosticEventPersistence LastEventIdBroadcaster.instance().setLastEventId(event.getClass().getName(), store.getLastEventId()); } + @SuppressWarnings("unchecked") private Class getEventClass(String eventClazz) throws ClassNotFoundException, InvalidClassException { // get class by eventClazz argument name @@ -135,12 +136,12 @@ public final class DiagnosticEventPersistence if (!eventClazz.startsWith("org.apache.cassandra.")) throw new RuntimeException("Not a Cassandra event class: " + eventClazz); - Class clazz = (Class) Class.forName(eventClazz); + Class clazz = Class.forName(eventClazz, false, DiagnosticEventPersistence.class.getClassLoader()); if (!(DiagnosticEvent.class.isAssignableFrom(clazz))) throw new InvalidClassException("Event class must be of type DiagnosticEvent"); - return clazz; + return (Class) clazz.asSubclass(DiagnosticEvent.class); } private DiagnosticEventStore getStore(Class cls) diff --git a/src/java/org/apache/cassandra/index/SecondaryIndexManager.java b/src/java/org/apache/cassandra/index/SecondaryIndexManager.java index 2846a8ede5..aeb4556ff4 100644 --- a/src/java/org/apache/cassandra/index/SecondaryIndexManager.java +++ b/src/java/org/apache/cassandra/index/SecondaryIndexManager.java @@ -921,6 +921,11 @@ public class SecondaryIndexManager implements IndexRegistry, INotificationConsum return indexes.get(indexName); } + static Class loadIndexClass(String className) + { + return FBUtilities.classForNameWithoutInitialization(className, "Index", Index.class); + } + private Index createInstance(IndexMetadata indexDef) { Index newIndex; @@ -933,7 +938,7 @@ public class SecondaryIndexManager implements IndexRegistry, INotificationConsum try { - Class indexClass = FBUtilities.classForName(className, "Index"); + Class indexClass = loadIndexClass(className); Constructor ctor = indexClass.getConstructor(ColumnFamilyStore.class, IndexMetadata.class); newIndex = ctor.newInstance(baseCfs, indexDef); } diff --git a/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java b/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java index 0c93f3c1fe..ed7dcb488c 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java +++ b/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java @@ -20,6 +20,8 @@ package org.apache.cassandra.index.sai.disk.io; import java.io.IOException; +import javax.annotation.concurrent.NotThreadSafe; + import org.apache.lucene.store.DataInput; import org.apache.lucene.store.IndexInput; @@ -30,10 +32,13 @@ import org.apache.cassandra.io.util.RandomAccessReader; * This is a wrapper over a Cassandra {@link RandomAccessReader} that provides an {@link IndexInput} * interface for Lucene classes that need {@link IndexInput}. This is an optimisation because the * Lucene {@link DataInput} reads bytes one at a time whereas the {@link RandomAccessReader} is - * optimised to read multibyte objects faster. + * optimized to read multibyte objects faster. */ +@NotThreadSafe public class IndexInputReader extends IndexInput { + public static final Runnable NO_OP_ON_CLOSE = () -> {}; + /** * the byte order of `input`'s native readX operations doesn't matter, * because we only use `readFully` and `readByte` methods. IndexInput calls these @@ -42,27 +47,47 @@ public class IndexInputReader extends IndexInput private final RandomAccessReader input; private final Runnable doOnClose; - private IndexInputReader(RandomAccessReader input, Runnable doOnClose) + /** Absolute offset in the underlying file that this input's position 0 refers to. */ + private final long offset; + + /** Bounded length of this input, in bytes. */ + private final long length; + + private IndexInputReader(RandomAccessReader input, Runnable doOnClose, long offset, long length) { super(input.getPath()); this.input = input; this.doOnClose = doOnClose; + this.offset = offset; + this.length = length; } public static IndexInputReader create(RandomAccessReader input) { - return new IndexInputReader(input, () -> {}); + // Top-level inputs own the underlying reader; folding its close into doOnClose lets us + // avoid a separate ownership flag on the class. + return new IndexInputReader(input, input::close, 0L, input.length()); } public static IndexInputReader create(RandomAccessReader input, Runnable doOnClose) { - return new IndexInputReader(input, doOnClose); + Runnable close = () -> { + try + { + input.close(); + } + finally + { + doOnClose.run(); + } + }; + return new IndexInputReader(input, close, 0L, input.length()); } public static IndexInputReader create(FileHandle handle) { RandomAccessReader reader = handle.createReader(); - return new IndexInputReader(reader, () -> {}); + return new IndexInputReader(reader, reader::close, 0L, reader.length()); } @Override @@ -80,37 +105,42 @@ public class IndexInputReader extends IndexInput @Override public void close() { - try - { - input.close(); - } - finally - { - doOnClose.run(); - } + doOnClose.run(); } @Override public long getFilePointer() { - return input.getFilePointer(); + return input.getFilePointer() - offset; } @Override public void seek(long position) { - input.seek(position); + if (position > length) + throw new IllegalArgumentException("Cannot seek to position " + position + " past length of " + length); + + input.seek(offset + position); } @Override public long length() { - return input.length(); + return length; } @Override public IndexInput slice(String sliceDescription, long offset, long length) { - throw new UnsupportedOperationException("Slice operations are not supported"); + if (offset < 0 || length < 0 || offset + length > this.length) + throw new IllegalArgumentException("Invalid slice: offset=" + offset + ", length=" + length + ", parent length=" + this.length + " for " + sliceDescription); + + // Slices share the underlying reader with their parent; the no-op close keeps the parent's lifecycle intact. + IndexInputReader slice = new IndexInputReader(input, NO_OP_ON_CLOSE, this.offset + offset, length); + + // Seek to the beginning of the slice... + slice.seek(0); + + return slice; } } diff --git a/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java b/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java index ba8b13ca4c..5068e039f4 100644 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java @@ -21,11 +21,14 @@ package org.apache.cassandra.index.sai.disk.v1; import java.io.IOException; import java.io.UncheckedIOException; import java.util.EnumSet; +import java.util.List; import java.util.Set; import com.codahale.metrics.Gauge; import com.google.common.annotations.VisibleForTesting; +import org.apache.lucene.codecs.CodecUtil; +import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.store.IndexInput; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +47,7 @@ import org.apache.cassandra.index.sai.disk.format.IndexComponent; import org.apache.cassandra.index.sai.disk.format.IndexDescriptor; import org.apache.cassandra.index.sai.disk.format.OnDiskFormat; import org.apache.cassandra.index.sai.disk.v1.segment.SegmentBuilder; +import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata; import org.apache.cassandra.index.sai.metrics.AbstractMetrics; import org.apache.cassandra.index.sai.utils.IndexIdentifier; import org.apache.cassandra.index.sai.utils.IndexTermType; @@ -96,6 +100,18 @@ public class V1OnDiskFormat implements OnDiskFormat IndexComponent.TERMS_DATA, IndexComponent.POSTING_LISTS); + /** + * Per-column components whose files are written in append mode with one SAI codec footer + * per segment (see {@link org.apache.cassandra.index.sai.disk.v1.bbtree.NumericIndexWriter}, + * {@link org.apache.cassandra.index.sai.disk.v1.trie.TrieTermsDictionaryWriter}, + * {@link org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter}, and + * {@link org.apache.cassandra.index.sai.disk.v1.vector.OnHeapGraph}). + */ + private static final Set SEGMENTED_COMPONENTS = EnumSet.of(IndexComponent.BALANCED_TREE, + IndexComponent.POSTING_LISTS, + IndexComponent.TERMS_DATA, + IndexComponent.COMPRESSED_VECTORS); + /** * Global limit on heap consumed by all index segment building that occurs outside the context of Memtable flush. *

@@ -219,15 +235,90 @@ public class V1OnDiskFormat implements OnDiskFormat } } + if (isEmptyIndex) + return; + + // Safely read the segment metadata so we can validate per-segment checksums below... + List segments = null; + if (checksum) + { + validateIndexComponent(indexDescriptor, indexIdentifier, IndexComponent.META, true); + try + { + segments = SegmentMetadata.load(MetadataSource.loadColumnMetadata(indexDescriptor, indexIdentifier), indexDescriptor.primaryKeyFactory); + } + catch (IOException e) + { + rethrowIOException(e); + } + } + for (IndexComponent indexComponent : perColumnIndexComponents(indexTermType)) { - if (!isEmptyIndex && isNotBuildCompletionMarker(indexComponent)) + if (isNotBuildCompletionMarker(indexComponent)) { - validateIndexComponent(indexDescriptor, indexIdentifier, indexComponent, checksum); + // META was validated up-front in CHECKSUM mode; don't validate it twice. + if (checksum && indexComponent == IndexComponent.META) + continue; + + if (checksum && SEGMENTED_COMPONENTS.contains(indexComponent)) + { + assert segments != null : "No segment metadata available!"; + validateSegmentedIndexComponent(indexDescriptor, indexIdentifier, indexComponent, segments, indexTermType.isVector()); + } + else + validateIndexComponent(indexDescriptor, indexIdentifier, indexComponent, checksum); } } } + private static void validateSegmentedIndexComponent(IndexDescriptor indexDescriptor, + IndexIdentifier indexIdentifier, + IndexComponent indexComponent, + List segments, + boolean payloadOnlyMetadata) + { + try (IndexInput input = indexDescriptor.openPerIndexInput(indexComponent, indexIdentifier)) + { + long fileLength = input.length(); + long frameStart = 0; + + for (SegmentMetadata segment : segments) + { + SegmentMetadata.ComponentMetadata cm = segment.componentMetadatas.get(indexComponent); + + // Non-vector writers record offsets as the codec-framed segment starts (before + // the header) and length as the full framed length (through the footer). The vector + // writer (OnHeapGraph#writeData) instead records the offset as the payload start (after + // the header) and length as just the payload length, because vector readers seek + // directly at the payload. Segments are written contiguously in append mode, so we can + // recover the vector-path frame extent by walking segment ends and adding the trailing + // 16-byte codec footer. + long frameEnd = payloadOnlyMetadata ? cm.offset + cm.length + CodecUtil.footerLength() : cm.offset + cm.length; + + if (frameEnd > fileLength || frameEnd < frameStart) + throw new CorruptIndexException(String.format("Segment frame [%d, %d) is inconsistent with component file length %d", + frameStart, frameEnd, fileLength), + indexComponent.name + '@' + frameStart); + + IndexInput slice = input.slice(indexComponent.name + '@' + frameStart, frameStart, frameEnd - frameStart); + SAICodecUtils.validateChecksum(slice); + frameStart = frameEnd; + } + + if (frameStart != fileLength) + throw new CorruptIndexException(String.format("Component file length %d does not match combined frame length of all segments %d", + fileLength, frameStart), + indexComponent.name); + } + catch (Exception e) + { + logger.warn(indexDescriptor.logMessage("Segmented checksum validation failed for index component {} on SSTable {}"), + indexComponent, indexDescriptor.sstableDescriptor); + rethrowIOException(e); + } + } + private static void validateIndexComponent(IndexDescriptor indexDescriptor, IndexIdentifier indexContext, IndexComponent indexComponent, @@ -245,9 +336,7 @@ public class V1OnDiskFormat implements OnDiskFormat catch (Exception e) { logger.warn(indexDescriptor.logMessage("{} failed for index component {} on SSTable {}"), - checksum ? "Checksum validation" : "Validation", - indexComponent, - indexDescriptor.sstableDescriptor); + checksum ? "Checksum validation" : "Validation", indexComponent, indexDescriptor.sstableDescriptor); rethrowIOException(e); } } diff --git a/src/java/org/apache/cassandra/index/sai/utils/CellWithSource.java b/src/java/org/apache/cassandra/index/sai/utils/CellWithSource.java index ce697f361a..b0ee8284b4 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/CellWithSource.java +++ b/src/java/org/apache/cassandra/index/sai/utils/CellWithSource.java @@ -103,6 +103,12 @@ public class CellWithSource extends Cell return cell.localDeletionTime(); } + @Override + public long minDeletionTime() + { + return cell.minDeletionTime(); + } + @Override public boolean isTombstone() { diff --git a/src/java/org/apache/cassandra/index/sai/utils/RowWithSource.java b/src/java/org/apache/cassandra/index/sai/utils/RowWithSource.java index 33f5f07deb..6b13a92236 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/RowWithSource.java +++ b/src/java/org/apache/cassandra/index/sai/utils/RowWithSource.java @@ -213,6 +213,12 @@ public class RowWithSource implements Row return row.hasDeletion(nowInSec); } + @Override + public long minLocalDeletionTime() + { + return row.minLocalDeletionTime(); + } + @Override public SearchIterator searchIterator() { diff --git a/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java b/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java index c827b2b820..339a2cae5d 100644 --- a/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java +++ b/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java @@ -37,6 +37,7 @@ import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.Mode; import org.apache.cassandra.index.sasi.plan.Expression.Op; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.IndexMetadata; +import org.apache.cassandra.utils.FBUtilities; public class IndexMode { @@ -60,10 +61,10 @@ public class IndexMode public final Mode mode; public final boolean isAnalyzed, isLiteral; - public final Class analyzerClass; + public final Class analyzerClass; public final long maxCompactionFlushMemoryInBytes; - private IndexMode(Mode mode, boolean isLiteral, boolean isAnalyzed, Class analyzerClass, long maxMemBytes) + private IndexMode(Mode mode, boolean isLiteral, boolean isAnalyzed, Class analyzerClass, long maxMemBytes) { this.mode = mode; this.isLiteral = isLiteral; @@ -81,7 +82,7 @@ public class IndexMode if (isAnalyzed) { if (analyzerClass != null) - analyzer = (AbstractAnalyzer) analyzerClass.newInstance(); + analyzer = analyzerClass.newInstance(); else if (TOKENIZABLE_TYPES.contains(validator)) analyzer = new StandardAnalyzer(); } @@ -99,21 +100,14 @@ public class IndexMode // validate that a valid analyzer class was provided if specified if (indexOptions.containsKey(INDEX_ANALYZER_CLASS_OPTION)) { - Class analyzerClass; - try - { - analyzerClass = Class.forName(indexOptions.get(INDEX_ANALYZER_CLASS_OPTION)); - } - catch (ClassNotFoundException e) - { - throw new ConfigurationException(String.format("Invalid analyzer class option specified [%s]", - indexOptions.get(INDEX_ANALYZER_CLASS_OPTION))); - } + Class analyzerClass = FBUtilities.classForNameWithoutInitialization(indexOptions.get(INDEX_ANALYZER_CLASS_OPTION), + "analyzer", + AbstractAnalyzer.class); AbstractAnalyzer analyzer; try { - analyzer = (AbstractAnalyzer) analyzerClass.newInstance(); + analyzer = analyzerClass.newInstance(); analyzer.validate(indexOptions, cd); } catch (InstantiationException | IllegalAccessException e) @@ -148,25 +142,30 @@ public class IndexMode } boolean isAnalyzed = false; - Class analyzerClass = null; - try + Class analyzerClass = null; + if (indexOptions.get(INDEX_ANALYZER_CLASS_OPTION) != null) { - if (indexOptions.get(INDEX_ANALYZER_CLASS_OPTION) != null) + try { - analyzerClass = Class.forName(indexOptions.get(INDEX_ANALYZER_CLASS_OPTION)); + analyzerClass = FBUtilities.classForNameWithoutInitialization(indexOptions.get(INDEX_ANALYZER_CLASS_OPTION), + "analyzer", + AbstractAnalyzer.class); isAnalyzed = indexOptions.get(INDEX_ANALYZED_OPTION) == null - ? true : Boolean.parseBoolean(indexOptions.get(INDEX_ANALYZED_OPTION)); + ? true : Boolean.parseBoolean(indexOptions.get(INDEX_ANALYZED_OPTION)); } - else if (indexOptions.get(INDEX_ANALYZED_OPTION) != null) + catch (ConfigurationException e) { - isAnalyzed = Boolean.parseBoolean(indexOptions.get(INDEX_ANALYZED_OPTION)); + if (!(e.getCause() instanceof ClassNotFoundException)) + throw e; + + // Should not happen as we already validated we could instantiate an instance in validateAnalyzer(). + logger.error("Failed to find specified analyzer class [{}]. Falling back to default analyzer", + indexOptions.get(INDEX_ANALYZER_CLASS_OPTION)); } } - catch (ClassNotFoundException e) + else if (indexOptions.get(INDEX_ANALYZED_OPTION) != null) { - // should not happen as we already validated we could instantiate an instance in validateAnalyzer() - logger.error("Failed to find specified analyzer class [{}]. Falling back to default analyzer", - indexOptions.get(INDEX_ANALYZER_CLASS_OPTION)); + isAnalyzed = Boolean.parseBoolean(indexOptions.get(INDEX_ANALYZED_OPTION)); } boolean isLiteral = false; diff --git a/src/java/org/apache/cassandra/io/sstable/ClusteringDescriptor.java b/src/java/org/apache/cassandra/io/sstable/ClusteringDescriptor.java index 27bd3cea89..45015df281 100644 --- a/src/java/org/apache/cassandra/io/sstable/ClusteringDescriptor.java +++ b/src/java/org/apache/cassandra/io/sstable/ClusteringDescriptor.java @@ -61,7 +61,7 @@ public class ClusteringDescriptor extends ResizableByteBuffer protected void loadClustering(RandomAccessReader dataReader, byte clusteringKind, int clusteringColumnsBound) throws IOException { - set(ClusteringPrefix.Kind.values()[clusteringKind], clusteringKind, clusteringColumnsBound); + set(ClusteringPrefix.Kind.fromOrdinal(clusteringKind), clusteringKind, clusteringColumnsBound); if (clusteringKind != STATIC_CLUSTERING_KIND) readUnfilteredClustering(dataReader, clusteringTypes, this.clusteringColumnsBound, this); else @@ -103,7 +103,7 @@ public class ClusteringDescriptor extends ResizableByteBuffer } private void set(byte clusteringKindEncoded, int clusteringColumnsBound) { - set(ClusteringPrefix.Kind.values()[clusteringKindEncoded], clusteringKindEncoded, clusteringColumnsBound); + set(ClusteringPrefix.Kind.fromOrdinal(clusteringKindEncoded), clusteringKindEncoded, clusteringColumnsBound); } private void set(ClusteringPrefix.Kind clusteringKind, byte clusteringKindEncoded, int clusteringColumnsBound) diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableCursorWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableCursorWriter.java index 102e109ab3..bc0b830f0f 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableCursorWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableCursorWriter.java @@ -513,7 +513,7 @@ public class SSTableCursorWriter implements AutoCloseable public void writeRangeTombstone(UnfilteredDescriptor rangeTombstone, boolean updateClusteringMetadata) throws IOException { int tombstoneKind = rangeTombstone.clusteringKindEncoded(); - ClusteringPrefix.Kind kind = ClusteringPrefix.Kind.values()[tombstoneKind]; + ClusteringPrefix.Kind kind = ClusteringPrefix.Kind.fromOrdinal(tombstoneKind); long unfilteredStartPosition = getPosition(); /** See: {@link org.apache.cassandra.db.rows.UnfilteredSerializer#serialize */ dataWriter.writeByte((byte)IS_MARKER); diff --git a/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java b/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java index 4dd68b3dfd..4be3f0e605 100644 --- a/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java +++ b/src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java @@ -340,11 +340,11 @@ public abstract class AbstractReplicationStrategy if ("org.apache.cassandra.locator.OldNetworkTopologyStrategy".equals(className)) // see CASSANDRA-16301 throw new ConfigurationException("The support for the OldNetworkTopologyStrategy has been removed in C* version 4.0. The keyspace strategy should be switch to NetworkTopologyStrategy"); - Class strategyClass = FBUtilities.classForName(className, "replication strategy"); - if (!AbstractReplicationStrategy.class.isAssignableFrom(strategyClass)) - { - throw new ConfigurationException(String.format("Specified replication strategy class (%s) is not derived from AbstractReplicationStrategy", className)); - } + @SuppressWarnings("unchecked") + Class strategyClass = + (Class) FBUtilities.classForNameWithoutInitialization(className, + "replication strategy", + AbstractReplicationStrategy.class); return strategyClass; } diff --git a/src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java b/src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java index feb96c02bd..d7b429ada6 100644 --- a/src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java +++ b/src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java @@ -26,6 +26,8 @@ import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; @@ -41,6 +43,7 @@ import org.apache.cassandra.concurrent.ScheduledExecutors; import org.apache.cassandra.concurrent.Shutdownable; import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.concurrent.FastThreadLocalThread; import static com.google.common.collect.ImmutableList.of; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; @@ -64,10 +67,8 @@ public class ThreadLocalMetrics static final AtomicInteger idGenerator = new AtomicInteger(); - private static final Object freeMetricIdSetGuard = new Object(); - @VisibleForTesting - static final BitSet freeMetricIdSet = new BitSet(); + static final FreeMetricIdSetTracker freeMetricIdSetTracker = new FreeMetricIdSetTracker(); private static final List allThreadLocalMetrics = new CopyOnWriteArrayList<>(); @@ -101,7 +102,13 @@ public class ThreadLocalMetrics { ThreadLocalMetrics result = new ThreadLocalMetrics(); allThreadLocalMetrics.add(result); - destroyWhenUnreachable(Thread.currentThread(), result::release); + + Thread thread = Thread.currentThread(); + // use phantom references ony if needed + // CassandraThread is FastThreadLocalThread too + if (!(thread instanceof FastThreadLocalThread)) + destroyWhenUnreachable(thread, result::release); + return result; } @@ -317,13 +324,7 @@ public class ThreadLocalMetrics static int allocateMetricId() { - int metricId; - synchronized (freeMetricIdSetGuard) - { - metricId = freeMetricIdSet.nextSetBit(0); - if (metricId >= 0) - freeMetricIdSet.clear(metricId); - } + int metricId = freeMetricIdSetTracker.getFreeMetricId(); if (metricId < 0) metricId = idGenerator.getAndIncrement(); @@ -374,26 +375,92 @@ public class ThreadLocalMetrics lock.unlock(); } - // there's no an obvious happens-before relation between currentCounterValues[metricId] = 0 write we just did - // and an initial read of the entry by a thread which updates the reused metric - // as a workaround we introduce a delay in recyling to provide the write visibility in practice - // even if it is not formally guaranteed by the JMM - ScheduledExecutors.scheduledTasks.schedule(() -> { - synchronized (freeMetricIdSetGuard) + freeMetricIdSetTracker.markAsFree(metricId); + } + + @VisibleForTesting + static class FreeMetricIdSetTracker + { + private final BitSet freeMetricIdSet = new BitSet(); + + private final BitSet tickDelayedToFreeMetricIdSet = new BitSet(); + private final BitSet tockDelayedToFreeMetricIdSet = new BitSet(); + + private BitSet delayedToFreeMetricIdSet = tickDelayedToFreeMetricIdSet; + + private ScheduledFuture cleanupTask; + + @VisibleForTesting + synchronized void triggerRecycling() + { + cleanupTask = null; + BitSet toProcess = otherSet(delayedToFreeMetricIdSet); + freeMetricIdSet.or(toProcess); + toProcess.clear(); + if (!delayedToFreeMetricIdSet.isEmpty()) + scheduleCleanupTask(); + delayedToFreeMetricIdSet = toProcess; + } + + private BitSet otherSet(BitSet set) + { + return set == tickDelayedToFreeMetricIdSet ? tockDelayedToFreeMetricIdSet : tickDelayedToFreeMetricIdSet; + } + + public synchronized int getFreeMetricId() + { + int metricId = freeMetricIdSet.nextSetBit(0); + if (metricId >= 0) + freeMetricIdSet.clear(metricId); + return metricId; + } + + public synchronized void markAsFree(int metricId) + { + // there's no an obvious happens-before relation between currentCounterValues[metricId] = 0 write we just did + // and an initial read of the entry by a thread which updates the reused metric + // as a workaround we introduce a delay in recyling to provide the write visibility in practice + // even if it is not formally guaranteed by the JMM + delayedToFreeMetricIdSet.set(metricId); + scheduleCleanupTask(); + } + + // must be called while holding this monitor (from a synchronized method) + @VisibleForTesting + protected void scheduleCleanupTask() + { + try { - freeMetricIdSet.set(metricId); + if (cleanupTask == null) + cleanupTask = ScheduledExecutors.scheduledTasks.schedule(this::triggerRecycling, 5, TimeUnit.SECONDS); } - }, 5, TimeUnit.SECONDS); + catch (RejectedExecutionException e) + { + // ignore theoretically possible rejections during a shutdown + } + } + + public synchronized int getFreeMetricSetCardinality() + { + return freeMetricIdSet.cardinality(); + } + + @Override + public synchronized String toString() + { + return "FreeMetricIdSetTracker{" + + "freeMetricIdSet=" + freeMetricIdSet + + ", tickDelayedToFreeMetricIdSet=" + tickDelayedToFreeMetricIdSet + + ", tockDelayedToFreeMetricIdSet=" + tockDelayedToFreeMetricIdSet + + ", delayedToFreeMetricIdSet=" + (delayedToFreeMetricIdSet == tickDelayedToFreeMetricIdSet ? "tick" : "tock") + + '}'; + } } @VisibleForTesting static int getAllocatedMetricsCount() { - int freeCount; - synchronized (freeMetricIdSetGuard) - { - freeCount = freeMetricIdSet.cardinality(); - } + int freeCount = freeMetricIdSetTracker.getFreeMetricSetCardinality(); return idGenerator.get() - freeCount; } diff --git a/src/java/org/apache/cassandra/net/AbstractMessageHandler.java b/src/java/org/apache/cassandra/net/AbstractMessageHandler.java index 0ef03990fd..abbd5c77b9 100644 --- a/src/java/org/apache/cassandra/net/AbstractMessageHandler.java +++ b/src/java/org/apache/cassandra/net/AbstractMessageHandler.java @@ -31,7 +31,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.concurrent.ManyToOneConcurrentLinkedQueue; -import org.apache.cassandra.metrics.ClientMetrics; import org.apache.cassandra.net.FrameDecoder.CorruptFrame; import org.apache.cassandra.net.FrameDecoder.Frame; import org.apache.cassandra.net.FrameDecoder.FrameProcessor; @@ -314,7 +313,7 @@ public abstract class AbstractMessageHandler extends ChannelInboundHandlerAdapte decoder.reactivate(); if (decoder.isActive()) - ClientMetrics.instance.unpauseConnection(); + onConnectionUnpaused(); } } catch (Throwable t) @@ -323,6 +322,10 @@ public abstract class AbstractMessageHandler extends ChannelInboundHandlerAdapte } } + protected void onConnectionUnpaused() + { + } + protected abstract void fatalExceptionCaught(Throwable t); // return true if the handler should be reactivated - if no new hurdles were encountered, diff --git a/src/java/org/apache/cassandra/repair/autorepair/AutoRepairConfig.java b/src/java/org/apache/cassandra/repair/autorepair/AutoRepairConfig.java index dd5c0d6ed9..2c3ba9dc20 100644 --- a/src/java/org/apache/cassandra/repair/autorepair/AutoRepairConfig.java +++ b/src/java/org/apache/cassandra/repair/autorepair/AutoRepairConfig.java @@ -395,7 +395,10 @@ public class AutoRepairConfig implements Serializable className = parameterizedClass.class_name.contains(".") ? parameterizedClass.class_name : "org.apache.cassandra.repair.autorepair." + parameterizedClass.class_name; - tokenRangeSplitterClass = FBUtilities.classForName(className, "token_range_splitter"); + tokenRangeSplitterClass = + FBUtilities.classForNameWithoutInitialization(className, + "token_range_splitter", + IAutoRepairTokenRangeSplitter.class); } else { diff --git a/src/java/org/apache/cassandra/schema/CompactionParams.java b/src/java/org/apache/cassandra/schema/CompactionParams.java index 9217a36376..1cfdd31458 100644 --- a/src/java/org/apache/cassandra/schema/CompactionParams.java +++ b/src/java/org/apache/cassandra/schema/CompactionParams.java @@ -311,15 +311,7 @@ public final class CompactionParams String className = name.contains(".") ? name : "org.apache.cassandra.db.compaction." + name; - Class strategyClass = FBUtilities.classForName(className, "compaction strategy"); - - if (!AbstractCompactionStrategy.class.isAssignableFrom(strategyClass)) - { - throw new ConfigurationException(format("Compaction strategy class %s is not derived from AbstractReplicationStrategy", - className)); - } - - return strategyClass; + return FBUtilities.classForNameWithoutInitialization(className, "compaction strategy", AbstractCompactionStrategy.class); } /* diff --git a/src/java/org/apache/cassandra/schema/CompressionParams.java b/src/java/org/apache/cassandra/schema/CompressionParams.java index a438e34eb1..078f58d8f7 100644 --- a/src/java/org/apache/cassandra/schema/CompressionParams.java +++ b/src/java/org/apache/cassandra/schema/CompressionParams.java @@ -46,6 +46,7 @@ import org.apache.cassandra.io.compress.ZstdDictionaryCompressor; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.utils.FBUtilities; import static java.lang.String.format; @@ -302,7 +303,7 @@ public final class CompressionParams return maxCompressedLength; } - private static Class parseCompressorClass(String className) throws ConfigurationException + private static Class parseCompressorClass(String className) throws ConfigurationException { if (className == null || className.isEmpty()) return null; @@ -310,15 +311,17 @@ public final class CompressionParams className = className.contains(".") ? className : "org.apache.cassandra.io.compress." + className; try { - return Class.forName(className); + return FBUtilities.classForNameWithoutInitialization(className, "compression", ICompressor.class); } - catch (Exception e) + catch (ConfigurationException e) { - throw new ConfigurationException("Could not create Compression for type " + className, e); + if (e.getCause() instanceof ClassNotFoundException || e.getCause() instanceof NoClassDefFoundError) + throw new ConfigurationException("Could not create Compression for type " + className, e); + throw e; } } - private static ICompressor createCompressor(Class compressorClass, Map compressionOptions) throws ConfigurationException + private static ICompressor createCompressor(Class compressorClass, Map compressionOptions) throws ConfigurationException { if (compressorClass == null) { diff --git a/src/java/org/apache/cassandra/schema/IndexMetadata.java b/src/java/org/apache/cassandra/schema/IndexMetadata.java index 15a0670e3a..b471260ccc 100644 --- a/src/java/org/apache/cassandra/schema/IndexMetadata.java +++ b/src/java/org/apache/cassandra/schema/IndexMetadata.java @@ -148,9 +148,7 @@ public final class IndexMetadata // Get the fully qualified class name: String className = getIndexClassName(); - Class indexerClass = FBUtilities.classForName(className, "custom indexer"); - if (!Index.class.isAssignableFrom(indexerClass)) - throw new ConfigurationException(String.format("Specified Indexer class (%s) does not implement the Indexer interface", className)); + Class indexerClass = FBUtilities.classForNameWithoutInitialization(className, "custom indexer", Index.class); validateCustomIndexOptions(table, indexerClass, options); } } diff --git a/src/java/org/apache/cassandra/schema/MemtableParams.java b/src/java/org/apache/cassandra/schema/MemtableParams.java index 7d88f6518e..c2111b9b0b 100644 --- a/src/java/org/apache/cassandra/schema/MemtableParams.java +++ b/src/java/org/apache/cassandra/schema/MemtableParams.java @@ -228,19 +228,25 @@ public final class MemtableParams try { Memtable.Factory factory; - Class clazz = Class.forName(className); + Class clazz = Class.forName(className, false, MemtableParams.class.getClassLoader()); final Map parametersCopy = options.parameters != null ? new HashMap<>(options.parameters) : new HashMap<>(); try { Method factoryMethod = clazz.getDeclaredMethod("factory", Map.class); + if (!Memtable.Factory.class.isAssignableFrom(factoryMethod.getReturnType())) + throw new ClassCastException("Memtable factory method on " + className + + " must return " + Memtable.Factory.class.getName()); factory = (Memtable.Factory) factoryMethod.invoke(null, parametersCopy); } catch (NoSuchMethodException e) { // continue with FACTORY field Field factoryField = clazz.getDeclaredField("FACTORY"); + if (!Memtable.Factory.class.isAssignableFrom(factoryField.getType())) + throw new ClassCastException("Memtable FACTORY field on " + className + + " must be of type " + Memtable.Factory.class.getName()); factory = (Memtable.Factory) factoryField.get(null); } if (!parametersCopy.isEmpty()) diff --git a/src/java/org/apache/cassandra/schema/ReplicationParams.java b/src/java/org/apache/cassandra/schema/ReplicationParams.java index c1e2643897..63cad56057 100644 --- a/src/java/org/apache/cassandra/schema/ReplicationParams.java +++ b/src/java/org/apache/cassandra/schema/ReplicationParams.java @@ -286,7 +286,7 @@ public final class ReplicationParams Map options = new HashMap<>(size); for (int i = 0; i < size; i++) options.put(in.readUTF(), in.readUTF()); - return new ReplicationParams(FBUtilities.classForName(klassName, "ReplicationStrategy"), options); + return new ReplicationParams(FBUtilities.classForNameWithoutInitialization(klassName, "ReplicationStrategy", AbstractReplicationStrategy.class), options); } public long serializedSize(ReplicationParams t, Version version) @@ -322,7 +322,7 @@ public final class ReplicationParams Map options = new HashMap<>(size); for (int i=0; i keyProviderClass = (Class)Class.forName(options.key_provider.class_name); - Constructor ctor = keyProviderClass.getConstructor(TransparentDataEncryptionOptions.class); - keyProvider = (KeyProvider)ctor.newInstance(options); + Class keyProviderClass = + FBUtilities.classForNameWithoutInitialization(options.key_provider.class_name, "key provider", KeyProvider.class); + Constructor ctor = keyProviderClass.getConstructor(TransparentDataEncryptionOptions.class); + keyProvider = ctor.newInstance(options); } catch (Exception e) { diff --git a/src/java/org/apache/cassandra/service/CacheService.java b/src/java/org/apache/cassandra/service/CacheService.java index 8240c2880f..c755c06347 100644 --- a/src/java/org/apache/cassandra/service/CacheService.java +++ b/src/java/org/apache/cassandra/service/CacheService.java @@ -147,9 +147,11 @@ public class CacheService implements CacheServiceMBean ? DatabaseDescriptor.getRowCacheClassName() : "org.apache.cassandra.cache.NopCacheProvider"; try { - Class> cacheProviderClass = - (Class>) Class.forName(cacheProviderClassName); - cacheProvider = cacheProviderClass.newInstance(); + Class cacheProviderClass = + FBUtilities.classForNameWithoutInitialization(cacheProviderClassName, "row cache provider", CacheProvider.class); + @SuppressWarnings("unchecked") + CacheProvider typedCacheProvider = cacheProviderClass.newInstance(); + cacheProvider = typedCacheProvider; } catch (Exception e) { diff --git a/src/java/org/apache/cassandra/service/ClientState.java b/src/java/org/apache/cassandra/service/ClientState.java index c119d5f4c3..a17688d116 100644 --- a/src/java/org/apache/cassandra/service/ClientState.java +++ b/src/java/org/apache/cassandra/service/ClientState.java @@ -124,7 +124,7 @@ public class ClientState { try { - handler = FBUtilities.construct(customHandlerClass, "QueryHandler"); + handler = FBUtilities.construct(customHandlerClass, "QueryHandler", QueryHandler.class); logger.info("Using {} as a query handler for native protocol queries (as requested by the {} system property)", customHandlerClass, CUSTOM_QUERY_HANDLER_CLASS.getKey()); } diff --git a/src/java/org/apache/cassandra/service/DiskErrorsHandlerService.java b/src/java/org/apache/cassandra/service/DiskErrorsHandlerService.java index 036559a738..7556e368d4 100644 --- a/src/java/org/apache/cassandra/service/DiskErrorsHandlerService.java +++ b/src/java/org/apache/cassandra/service/DiskErrorsHandlerService.java @@ -78,7 +78,7 @@ public class DiskErrorsHandlerService String fsErrorHandlerClass = CassandraRelevantProperties.CUSTOM_DISK_ERROR_HANDLER.getString(); DiskErrorsHandler fsErrorHandler = fsErrorHandlerClass == null ? new DefaultDiskErrorsHandler() - : FBUtilities.construct(fsErrorHandlerClass, "disk error handler"); + : FBUtilities.construct(fsErrorHandlerClass, "disk error handler", DiskErrorsHandler.class); DiskErrorsHandlerService.set(fsErrorHandler); } } diff --git a/src/java/org/apache/cassandra/service/StorageService.java b/src/java/org/apache/cassandra/service/StorageService.java index 397e50fa0d..b4c8343558 100644 --- a/src/java/org/apache/cassandra/service/StorageService.java +++ b/src/java/org/apache/cassandra/service/StorageService.java @@ -3403,6 +3403,11 @@ public class StorageService extends NotificationBroadcasterSupport implements IE */ @Deprecated(since = "4.0") public List getNaturalEndpoints(String keyspaceName, String cf, String key) + { + return getNaturalReplicas(keyspaceName, cf, key); + } + + public List getNaturalReplicas(String keyspaceName, String cf, String key) { EndpointsForToken replicas = getNaturalReplicasForToken(keyspaceName, cf, key); List inetList = new ArrayList<>(replicas.size()); @@ -3410,11 +3415,16 @@ public class StorageService extends NotificationBroadcasterSupport implements IE return inetList; } - public List getNaturalEndpointsWithPort(String keyspaceName, String cf, String key) + public List getNaturalReplicasWithPort(String keyspaceName, String cf, String key) { return Replicas.stringify(getNaturalReplicasForToken(keyspaceName, cf, key), true); } + public List getNaturalEndpointsWithPort(String keyspaceName, String cf, String key) + { + return getNaturalReplicasWithPort(keyspaceName, cf, key); + } + /** @deprecated See CASSANDRA-7544 */ @Deprecated(since = "4.0") public List getNaturalEndpoints(String keyspaceName, ByteBuffer key) diff --git a/src/java/org/apache/cassandra/service/StorageServiceMBean.java b/src/java/org/apache/cassandra/service/StorageServiceMBean.java index e6f7a18c8e..abc8689031 100644 --- a/src/java/org/apache/cassandra/service/StorageServiceMBean.java +++ b/src/java/org/apache/cassandra/service/StorageServiceMBean.java @@ -262,12 +262,28 @@ public interface StorageServiceMBean extends NotificationEmitter * @param key - key for which we need to find the endpoint return value - * the endpoint responsible for this key * @deprecated See CASSANDRA-7544 + * @link getNaturalReplicas + * @link getNaturalReplicasWithPort */ @Deprecated(since = "4.0") public List getNaturalEndpoints(String keyspaceName, String cf, String key); - public List getNaturalEndpointsWithPort(String keyspaceName, String cf, String key); + /** @deprecated See CASSANDRA-17665 */ + @Deprecated(since = "7.0") public List getNaturalEndpointsWithPort(String keyspaceName, String cf, String key); /** @deprecated See CASSANDRA-7544 */ @Deprecated(since = "4.0") public List getNaturalEndpoints(String keyspaceName, ByteBuffer key); - public List getNaturalEndpointsWithPort(String keysapceName, ByteBuffer key); + /** @deprecated See CASSANDRA-17665 */ + @Deprecated(since = "7.0") public List getNaturalEndpointsWithPort(String keysapceName, ByteBuffer key); + + /** + * This method returns the N replicas that are responsible for storing the + * specified key i.e for replication. + * + * @param keyspaceName keyspace name + * @param cf Column family name + * @param key - key for which we need to find the replica return value - + * the replica responsible for this key + */ + public List getNaturalReplicas(String keyspaceName, String cf, String key); + public List getNaturalReplicasWithPort(String keyspaceName, String cf, String key); /** * @deprecated use {@link #takeSnapshot(String tag, Map options, String... entities)} instead. See CASSANDRA-10907 diff --git a/src/java/org/apache/cassandra/service/accord/AccordService.java b/src/java/org/apache/cassandra/service/accord/AccordService.java index 340760370c..111e207474 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordService.java +++ b/src/java/org/apache/cassandra/service/accord/AccordService.java @@ -458,7 +458,9 @@ public class AccordService implements IAccordService, Shutdownable { Invariants.require(localId != null, "static localId must be set before instantiating AccordService"); logger.info("Starting accord with nodeId {}", localId); - AccordAgent agent = FBUtilities.construct(CassandraRelevantProperties.ACCORD_AGENT_CLASS.getString(AccordAgent.class.getName()), "AccordAgent"); + AccordAgent agent = FBUtilities.construct(CassandraRelevantProperties.ACCORD_AGENT_CLASS.getString(AccordAgent.class.getName()), + "AccordAgent", + AccordAgent.class); agent.setup(localId); AccordTimeService time = new AccordTimeService(); this.scheduler = new AccordScheduler(); diff --git a/src/java/org/apache/cassandra/streaming/StreamHook.java b/src/java/org/apache/cassandra/streaming/StreamHook.java index 84db420f04..df1e8ee76e 100644 --- a/src/java/org/apache/cassandra/streaming/StreamHook.java +++ b/src/java/org/apache/cassandra/streaming/StreamHook.java @@ -37,7 +37,7 @@ public interface StreamHook String className = STREAM_HOOK.getString(); if (className != null) { - return FBUtilities.construct(className, StreamHook.class.getSimpleName()); + return FBUtilities.construct(className, StreamHook.class.getSimpleName(), StreamHook.class); } else { diff --git a/src/java/org/apache/cassandra/tcm/extensions/ExtensionKey.java b/src/java/org/apache/cassandra/tcm/extensions/ExtensionKey.java index 976e327951..2165fceddd 100644 --- a/src/java/org/apache/cassandra/tcm/extensions/ExtensionKey.java +++ b/src/java/org/apache/cassandra/tcm/extensions/ExtensionKey.java @@ -41,7 +41,7 @@ public class ExtensionKey> extends MetadataKey public K newValue() { - return valueType.cast(FBUtilities.construct(valueType.getName(), "extension value")); + return FBUtilities.construct(valueType.getName(), "extension value", valueType); } public static final class Serializer implements MetadataSerializer> @@ -58,7 +58,9 @@ public class ExtensionKey> extends MetadataKey { String id = in.readUTF(); String valType = in.readUTF(); - return new ExtensionKey(id, FBUtilities.classForName(valType, "value type")); + Class valueType = + FBUtilities.classForNameWithoutInitialization(valType, "value type", ExtensionValue.class); + return new ExtensionKey(id, valueType); } @Override @@ -68,4 +70,3 @@ public class ExtensionKey> extends MetadataKey } } } - diff --git a/src/java/org/apache/cassandra/tcm/sequences/SingleNodeSequences.java b/src/java/org/apache/cassandra/tcm/sequences/SingleNodeSequences.java index 313700aaaf..82a1059b6e 100644 --- a/src/java/org/apache/cassandra/tcm/sequences/SingleNodeSequences.java +++ b/src/java/org/apache/cassandra/tcm/sequences/SingleNodeSequences.java @@ -92,6 +92,7 @@ public interface SingleNodeSequences else if (InProgressSequences.isLeave(inProgress)) { logger.info("Resuming decommission @ {} (current epoch = {}): {}", inProgress.latestModification, metadata.epoch, inProgress.status()); + StorageService.instance.clearTransientMode(); } else { diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java index 353324e0ea..c60e0427e5 100644 --- a/src/java/org/apache/cassandra/tools/NodeProbe.java +++ b/src/java/org/apache/cassandra/tools/NodeProbe.java @@ -1200,14 +1200,14 @@ public class NodeProbe implements AutoCloseable ssProxy.setHintedHandoffThrottleInKB(throttleInKB); } - public List getEndpointsWithPort(String keyspace, String cf, String key) + public List getReplicasWithPort(String keyspace, String cf, String key) { - return ssProxy.getNaturalEndpointsWithPort(keyspace, cf, key); + return ssProxy.getNaturalReplicasWithPort(keyspace, cf, key); } - public List getEndpoints(String keyspace, String cf, String key) + public List getReplicas(String keyspace, String cf, String key) { - return ssProxy.getNaturalEndpoints(keyspace, cf, key); + return ssProxy.getNaturalReplicas(keyspace, cf, key); } public List getSSTables(String keyspace, String cf, String key, boolean hexFormat) diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetEndpoints.java b/src/java/org/apache/cassandra/tools/nodetool/GetEndpoints.java index 3f30680951..6f0b6ea972 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/GetEndpoints.java +++ b/src/java/org/apache/cassandra/tools/nodetool/GetEndpoints.java @@ -17,62 +17,10 @@ */ package org.apache.cassandra.tools.nodetool; -import java.net.InetAddress; -import java.util.ArrayList; -import java.util.List; - -import org.apache.cassandra.tools.NodeProbe; -import org.apache.cassandra.tools.nodetool.layout.CassandraUsage; - import picocli.CommandLine.Command; -import picocli.CommandLine.Mixin; -import picocli.CommandLine.Parameters; -import static com.google.common.base.Preconditions.checkArgument; -import static org.apache.cassandra.tools.nodetool.CommandUtils.concatArgs; - -@Command(name = "getendpoints", description = "Print the end points that owns the key") -public class GetEndpoints extends AbstractCommand +@Deprecated(since = "7.0") // this is alias to getreplicas +@Command(name = "getendpoints", description = "Print the end points that owns the key, deprecated, use getreplicas instead") +public class GetEndpoints extends GetReplicas { - @CassandraUsage(usage = " ", description = "The keyspace, the table, and the partition key for which we need to find the endpoint") - private List args = new ArrayList<>(); - - @Parameters(index = "0", arity = "0..1", description = "The keyspace for which we need to find the endpoint") - private String keyspace; - - @Parameters(index = "1", arity = "0..1", description = "The table for which we need to find the endpoint") - private String table; - - @Parameters(index = "2", arity = "0..1", description = "The partition key for which we need to find the endpoint") - private String key; - - @Mixin - private PrintPortMixin printPortMixin = new PrintPortMixin(); - - @Override - public void execute(NodeProbe probe) - { - args = concatArgs(keyspace, table, key); - - checkArgument(args.size() == 3, "getendpoints requires keyspace, table and partition key arguments"); - String ks = args.get(0); - String table = args.get(1); - String key = args.get(2); - - if (printPortMixin.printPort) - { - for (String endpoint : probe.getEndpointsWithPort(ks, table, key)) - { - probe.output().out.println(endpoint); - } - } - else - { - List endpoints = probe.getEndpoints(ks, table, key); - for (InetAddress endpoint : endpoints) - { - probe.output().out.println(endpoint.getHostAddress()); - } - } - } } diff --git a/src/java/org/apache/cassandra/tools/nodetool/GetReplicas.java b/src/java/org/apache/cassandra/tools/nodetool/GetReplicas.java new file mode 100644 index 0000000000..5c91d20a7d --- /dev/null +++ b/src/java/org/apache/cassandra/tools/nodetool/GetReplicas.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.cassandra.tools.nodetool; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; + +import org.apache.cassandra.tools.NodeProbe; +import org.apache.cassandra.tools.nodetool.layout.CassandraUsage; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Parameters; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.cassandra.tools.nodetool.CommandUtils.concatArgs; + +@Command(name = "getreplicas", description = "Print the replicas that own the key") +public class GetReplicas extends AbstractCommand +{ + @CassandraUsage(usage = "
", description = "The keyspace, the table, and the partition key for which we need to find the replica (e.g., pk1:pk2:pk3 for compound keys)") + private List args = new ArrayList<>(); + + @Parameters(index = "0", arity = "0..1", description = "The keyspace for which we need to find the replica") + private String keyspace; + + @Parameters(index = "1", arity = "0..1", description = "The table for which we need to find the replica") + private String table; + + @Parameters(index = "2", arity = "0..1", description = "The partition key for which we need to find the replica (e.g., pk1:pk2:pk3 for compound keys)") + private String key; + + @Mixin + private PrintPortMixin printPortMixin = new PrintPortMixin(); + + @Override + public void execute(NodeProbe probe) + { + args = concatArgs(keyspace, table, key); + + checkArgument(args.size() == 3, "requires keyspace, table and partition key arguments"); + String ks = args.get(0); + String table = args.get(1); + String key = args.get(2); + + if (printPortMixin.printPort) + { + for (String replica : probe.getReplicasWithPort(ks, table, key)) + { + probe.output().out.println(replica); + } + } + else + { + List replicas = probe.getReplicas(ks, table, key); + for (InetAddress replica : replicas) + { + probe.output().out.println(replica.getHostAddress()); + } + } + } +} diff --git a/src/java/org/apache/cassandra/tools/nodetool/InvalidatePermissionsCache.java b/src/java/org/apache/cassandra/tools/nodetool/InvalidatePermissionsCache.java index e1fcc6b26a..7f1372b33d 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/InvalidatePermissionsCache.java +++ b/src/java/org/apache/cassandra/tools/nodetool/InvalidatePermissionsCache.java @@ -38,7 +38,7 @@ import static com.google.common.base.Preconditions.checkArgument; @Command(name = "invalidatepermissionscache", description = "Invalidate the permissions cache") public class InvalidatePermissionsCache extends AbstractCommand { - @Parameters(paramLabel = "role", description = "A role for which permissions to specified resources need to be invalidated", arity = "0..1", index = "0") + @Parameters(paramLabel = "role_name", description = "A role for which permissions to specified resources need to be invalidated", arity = "0..1", index = "0") private String roleName; // Data Resources diff --git a/src/java/org/apache/cassandra/tools/nodetool/NodetoolCommand.java b/src/java/org/apache/cassandra/tools/nodetool/NodetoolCommand.java index af1e835d91..fd2ccde672 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/NodetoolCommand.java +++ b/src/java/org/apache/cassandra/tools/nodetool/NodetoolCommand.java @@ -120,6 +120,7 @@ import static org.apache.cassandra.tools.nodetool.Help.printTopCommandUsage; GetInterDCStreamThroughput.class, GetLoggingLevels.class, GetMaxHintWindow.class, + GetReplicas.class, GetSSTables.class, GetSeeds.class, GetSnapshotThrottle.class, diff --git a/src/java/org/apache/cassandra/tools/nodetool/Sjk.java b/src/java/org/apache/cassandra/tools/nodetool/Sjk.java index c117ed638b..f9d19eabd2 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/Sjk.java +++ b/src/java/org/apache/cassandra/tools/nodetool/Sjk.java @@ -393,6 +393,7 @@ public class Sjk extends AbstractCommand List> result = new ArrayList<>(); try { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); String path = packageName.replace('.', '/'); for (String f : findFiles(path)) { @@ -400,7 +401,7 @@ public class Sjk extends AbstractCommand { f = f.substring(0, f.length() - ".class".length()); f = f.replace('/', '.'); - result.add(Class.forName(f)); + result.add(Class.forName(f, false, cl)); } } return result; diff --git a/src/java/org/apache/cassandra/tracing/Tracing.java b/src/java/org/apache/cassandra/tracing/Tracing.java index 4ef2e500cb..8d621cb668 100644 --- a/src/java/org/apache/cassandra/tracing/Tracing.java +++ b/src/java/org/apache/cassandra/tracing/Tracing.java @@ -117,7 +117,7 @@ public abstract class Tracing extends ExecutorLocals.Impl { try { - tracing = FBUtilities.construct(customTracingClass, "Tracing"); + tracing = FBUtilities.construct(customTracingClass, "Tracing", Tracing.class); logger.info("Using the {} class to trace queries (as requested by the {} system property)", customTracingClass, CUSTOM_TRACING_CLASS.getKey()); } diff --git a/src/java/org/apache/cassandra/transport/CBUtil.java b/src/java/org/apache/cassandra/transport/CBUtil.java index a456b6e2a3..260350cce3 100644 --- a/src/java/org/apache/cassandra/transport/CBUtil.java +++ b/src/java/org/apache/cassandra/transport/CBUtil.java @@ -472,6 +472,9 @@ public abstract class CBUtil int length = cb.readInt(); if (length < 0) return null; + if (length > cb.readableBytes()) + throw new ProtocolException(String.format("Cannot read value of length %d, only %d bytes remaining in the message", + length, cb.readableBytes())); ByteBuffer buffer = cb.nioBuffer(cb.readerIndex(), length); cb.skipBytes(length); @@ -738,6 +741,9 @@ public abstract class CBUtil private static byte[] readRawBytes(ByteBuf cb, int length) { + if (length > cb.readableBytes()) + throw new ProtocolException(String.format("Cannot read value of length %d, only %d bytes remaining in the message", + length, cb.readableBytes())); byte[] bytes = new byte[length]; cb.readBytes(bytes); return bytes; diff --git a/src/java/org/apache/cassandra/transport/CQLMessageHandler.java b/src/java/org/apache/cassandra/transport/CQLMessageHandler.java index 2486497c2f..13cb7463dd 100644 --- a/src/java/org/apache/cassandra/transport/CQLMessageHandler.java +++ b/src/java/org/apache/cassandra/transport/CQLMessageHandler.java @@ -106,7 +106,7 @@ public class CQLMessageHandler extends AbstractMessageHandler interface MessageConsumer { - void dispatch(Channel channel, M message, Dispatcher.FlushItemConverter toFlushItem, Overload backpressure); +

void dispatch(Channel channel, M message, Dispatcher.FlushItemConverter

toFlushItem, P param, Overload backpressure); boolean hasQueueCapacity(); } @@ -159,6 +159,12 @@ public class CQLMessageHandler extends AbstractMessageHandler return super.process(frame); } + @Override + protected void onConnectionUnpaused() + { + ClientMetrics.instance.unpauseConnection(); + } + /** * Checks limits on bytes in flight and the request rate limiter (if enabled), then takes one of three actions: * @@ -389,7 +395,7 @@ public class CQLMessageHandler extends AbstractMessageHandler try { message = messageDecoder.decode(channel, request); - dispatcher.dispatch(channel, message, this::toFlushItem, backpressure); + dispatcher.dispatch(channel, message, CQLMessageHandler::toFlushItem, this, backpressure); // sucessfully delivered a CQL message to the execution // stage, so reset the counter of consecutive errors @@ -485,7 +491,8 @@ public class CQLMessageHandler extends AbstractMessageHandler // The Dispatcher will call this to obtain the FlushItem to enqueue with its Flusher once // a dispatched request has been processed. - Envelope responseFrame = response.encode(request.getSource().header.version); + Envelope.Header header = request.getSource().header; + Envelope responseFrame = response.encode(header.version, header.streamId); int responseSize = envelopeSize(responseFrame.header); ClientMessageSizeMetrics.bytesSent.inc(responseSize); ClientMessageSizeMetrics.bytesSentPerResponse.update(responseSize); @@ -500,9 +507,9 @@ public class CQLMessageHandler extends AbstractMessageHandler @Override public void cleanup(Flusher.FlushItem flushItem) { - release(flushItem.request.header); - flushItem.request.release(); - flushItem.response.release(); + release(flushItem.requestEnvelope.header); + flushItem.requestEnvelope.release(); + flushItem.responseEnvelope.release(); } private void release(Envelope.Header header) @@ -524,8 +531,9 @@ public class CQLMessageHandler extends AbstractMessageHandler if (!extracted.isSuccess()) { // Hard fail on any decoding error as we can't trust the subsequent frames of - // the large message - handleError(ProtocolException.toFatalException(extracted.error())); + // the large message. The stream id is a best-effort value read before extraction + // failed, so route it back where possible rather than defaulting. + handleError(ProtocolException.toFatalException(extracted.error()), extracted.streamId()); return false; } @@ -542,7 +550,7 @@ public class CQLMessageHandler extends AbstractMessageHandler // not make sense to continue processing subsequent frames handleError(ProtocolException.toFatalException(new OversizedAuthMessageException( MULTI_FRAME_AUTH_ERROR_MESSAGE_PREFIX + - "type = " + header.type + ", size = " + header.bodySizeInBytes))); + "type = " + header.type + ", size = " + header.bodySizeInBytes)), header.streamId); ClientMetrics.instance.markRequestDiscarded(); return false; } diff --git a/src/java/org/apache/cassandra/transport/Dispatcher.java b/src/java/org/apache/cassandra/transport/Dispatcher.java index 3f6468c218..2e1b36a4f9 100644 --- a/src/java/org/apache/cassandra/transport/Dispatcher.java +++ b/src/java/org/apache/cassandra/transport/Dispatcher.java @@ -93,10 +93,9 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer { - FlushItem toFlushItem(Channel channel, Message.Request request, Message.Response response); + FlushItem toFlushItem(P param, Channel channel, Message.Request request, Message.Response response); } public Dispatcher(boolean useLegacyFlusher) @@ -105,18 +104,17 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer void dispatch(Channel channel, Message.Request request, FlushItemConverter

forFlusher, P param, Overload backpressure) { if (!request.connection().getTracker().isRunning()) { // We can not respond with a custom, transport, or server exceptions since, given current implementation of clients, // they will defunct the connection. Without a protocol version bump that introduces an "I am going away message", // we have to stick to an existing error code. - Message.Response response = ErrorMessage.fromException(new OverloadedException("Server is shutting down")); - response.setStreamId(request.getStreamId()); + Message.Response response = ErrorMessage.fromTransportException(new OverloadedException("Server is shutting down")); response.setWarnings(ClientWarn.instance.getWarnings()); response.attach(request.connection); - FlushItem toFlush = forFlusher.toFlushItem(channel, request, response); + FlushItem toFlush = forFlusher.toFlushItem(param, channel, request, response); flush(toFlush); return; } @@ -128,7 +126,7 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer(channel, request, forFlusher, param, backpressure)); ClientMetrics.instance.markRequestDispatched(); } @@ -293,20 +291,22 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer implements DebuggableTask.RunnableDebuggableTask { private final Channel channel; private final Message.Request request; - private final FlushItemConverter forFlusher; + private final FlushItemConverter

forFlusher; + private final P flusherParam; private final Overload backpressure; private volatile long startTimeNanos; - public RequestProcessor(Channel channel, Message.Request request, FlushItemConverter forFlusher, Overload backpressure) + public RequestProcessor(Channel channel, Message.Request request, FlushItemConverter

forFlusher, P flusherParam, Overload backpressure) { this.channel = channel; this.request = request; this.forFlusher = forFlusher; + this.flusherParam = flusherParam; this.backpressure = backpressure; } @@ -314,7 +314,7 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS)) { ClientMetrics.instance.markTimedOutBeforeProcessing(); - return ErrorMessage.fromException(new OverloadedException("Query timed out before it could start")); + return ErrorMessage.fromTransportException(new OverloadedException("Query timed out before it could start")); } if (connection.getVersion().isGreaterOrEqualTo(ProtocolVersion.V4)) @@ -434,8 +434,6 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer handler = ExceptionHandlers.getUnexpectedExceptionHandler(channel, true); - ErrorMessage error = ErrorMessage.fromException(t, handler); - error.setStreamId(request.getStreamId()); - error.setWarnings(ClientWarn.instance.getWarnings()); - return error; + response = ErrorMessage.fromExceptionNoStreamId(t, handler); } finally { + if (response != null) + response.setWarnings(ClientWarn.instance.getWarnings()); CoordinatorWarnings.reset(); CoordinatorWriteWarnings.reset(); ClientWarn.instance.resetWarnings(); } + return response; } /** * Note: this method is not expected to execute on the netty event loop. */ - void processRequest(Channel channel, Message.Request request, FlushItemConverter forFlusher, Overload backpressure, RequestTime requestTime) +

void processRequest(Channel channel, Message.Request request, FlushItemConverter

forFlusher, P param, Overload backpressure, RequestTime requestTime) { Message.Response response = processRequest(channel, request, backpressure, requestTime); - FlushItem toFlush = forFlusher.toFlushItem(channel, request, response); + FlushItem toFlush = forFlusher.toFlushItem(param, channel, request, response); Message.logger.trace("Responding: {}, v={}", response, request.connection().getVersion()); flush(toFlush); } @@ -517,7 +516,7 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer flush(new FlushItem.Framed(channel, - eventMessage.encode(version), + eventMessage.encode(version, EventMessage.EVENT_MESSAGE_STREAM_ID), // -1 was set in EventMessage previously null, allocator, - f -> f.response.release())); + f -> f.responseEnvelope.release())); } } diff --git a/src/java/org/apache/cassandra/transport/Envelope.java b/src/java/org/apache/cassandra/transport/Envelope.java index e70b4fe551..b33b054819 100644 --- a/src/java/org/apache/cassandra/transport/Envelope.java +++ b/src/java/org/apache/cassandra/transport/Envelope.java @@ -146,6 +146,11 @@ public class Envelope public final Message.Type type; public final long bodySizeInBytes; + public static Header dummy(int streamId, Message.Type type) + { + return new Header(ProtocolVersion.CURRENT, 0, streamId, type, 0); + } + private Header(ProtocolVersion version, int flags, int streamId, Message.Type type, long bodySizeInBytes) { this.version = version; @@ -258,7 +263,7 @@ public class Envelope // This throws a protocol exception if the version number is unsupported, // the opcode is unknown or invalid flags are set for the version version = ProtocolVersion.decode(versionNum, DatabaseDescriptor.getNativeTransportAllowOlderProtocols()); - validateFlags(version, flags); + validateFlags(version, flags, streamId); type = Message.Type.fromOpcode(opcode, direction); return new HeaderExtractionResult.Success(new Header(version, flags, streamId, type, bodyLength)); } @@ -272,6 +277,10 @@ public class Envelope // cause the channel to be closed. return new HeaderExtractionResult.Error(e, streamId, bodyLength); } + catch (ErrorMessage.WrappedException e) + { + return new HeaderExtractionResult.Error((ProtocolException) e.getCause(), e.getStreamId(), bodyLength); + } } public static abstract class HeaderExtractionResult @@ -370,7 +379,8 @@ public class Envelope Message.Direction direction = Message.Direction.extractFromVersion(firstByte); int versionNum = firstByte & PROTOCOL_VERSION_MASK; - ProtocolVersion version; + ProtocolVersion version = null; + ProtocolException protocolException = null; try { @@ -378,19 +388,43 @@ public class Envelope } catch (ProtocolException e) { - // Skip the remaining useless bytes. Otherwise the channel closing logic may try to decode again. - buffer.skipBytes(readableBytes); - throw e; + // defer throw to attempt to extract the stream id + protocolException = e; } // Wait until we have the complete header if (readableBytes < Header.LENGTH) + { + if (protocolException != null) + { + // Skip the remaining useless bytes. Otherwise the channel closing logic may try to decode again. + buffer.skipBytes(readableBytes); + // The header is incomplete, so there is no stream id to recover. Wrap with the unset + // sentinel; the channel-level exception handler sees it has no routable stream id and + // closes the connection rather than emit an unroutable error frame. + throw protocolException; + } return null; + } int flags = buffer.getByte(idx++); - validateFlags(version, flags); - int streamId = buffer.getShort(idx); + + if (protocolException != null) + { + // Protocol versions 1 and 2 use a shorter header with a single-byte stream id. Reading a + // 16-bit stream id from such a header splices the stream id byte together with the opcode + // byte and recovers a bogus id, routing the error to a stream the client never used + // (CASSANDRA-21508). A v1/v2 client that downgrades would then never see the error and time + // out, so recover the stream id using the attempted version's header layout. + int recoveredStreamId = versionNum < ProtocolVersion.V3.asInt() ? buffer.getByte(idx) : streamId; + // Skip the remaining useless bytes. Otherwise the channel closing logic may try to decode again. + buffer.skipBytes(readableBytes); + throw ErrorMessage.wrap(protocolException, recoveredStreamId); + } + + validateFlags(version, flags, streamId); + idx += 2; // This throws a protocol exceptions if the opcode is unknown @@ -436,11 +470,12 @@ public class Envelope return new Envelope(new Header(version, flags, streamId, type, bodyLength), body); } - private void validateFlags(ProtocolVersion version, int flags) + private void validateFlags(ProtocolVersion version, int flags, int streamId) { if (version.isBeta() && !Header.Flag.contains(flags, Header.Flag.USE_BETA)) - throw new ProtocolException(String.format("Beta version of the protocol used (%s), but USE_BETA flag is unset", version), - version); + throw ErrorMessage.wrap(new ProtocolException(String.format("Beta version of the protocol used (%s), but USE_BETA flag is unset", version), + version), + streamId); } @Override diff --git a/src/java/org/apache/cassandra/transport/ExceptionHandlers.java b/src/java/org/apache/cassandra/transport/ExceptionHandlers.java index 12bfe4190d..2a97704db8 100644 --- a/src/java/org/apache/cassandra/transport/ExceptionHandlers.java +++ b/src/java/org/apache/cassandra/transport/ExceptionHandlers.java @@ -38,6 +38,7 @@ import org.apache.cassandra.exceptions.OversizedCQLMessageException; import org.apache.cassandra.metrics.ClientMetrics; import org.apache.cassandra.net.FrameEncoder; import org.apache.cassandra.transport.messages.ErrorMessage; +import org.apache.cassandra.transport.messages.ErrorMessage.WithStreamId; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.Throwables; @@ -76,23 +77,53 @@ public class ExceptionHandlers if (ctx.channel().isOpen()) { Predicate handler = getUnexpectedExceptionHandler(ctx.channel(), false); - ErrorMessage errorMessage = ErrorMessage.fromException(cause, handler); - Envelope response = errorMessage.encode(version); - FrameEncoder.Payload payload = allocator.allocate(true, CQLMessageHandler.envelopeSize(response.header)); + // No request in scope at the channel level; a WrappedException cause carries the frame's + // stream id and overrides this fallback. + WithStreamId withStreamId = ErrorMessage.fromException(cause, handler); + ErrorMessage errorMessage = withStreamId.message; try { - response.encodeInto(payload.buffer); - response.release(); - payload.finish(); - ChannelPromise promise = ctx.newPromise(); - // On protocol exception, close the channel as soon as the message has been sent - if (isFatal(cause)) - promise.addListener(future -> ctx.close()); - ctx.writeAndFlush(payload, promise); + int streamId = withStreamId.streamId; + boolean isFatal = isFatal(cause); + if (streamId == Message.UNSET_STREAM_ID) + { + // No stream id could be recovered, so we have no request to route a response to. + // Close the connection rather than emit an unroutable frame (CASSANDRA-21508). + isFatal = true; + streamId = 0; + } + + Envelope response = errorMessage.encode(version, streamId); + FrameEncoder.Payload payload = allocator.allocate(true, CQLMessageHandler.envelopeSize(response.header)); + try + { + response.encodeInto(payload.buffer); + response.release(); + payload.finish(); + ChannelPromise promise = ctx.newPromise(); + // On a fatal error, close the channel only once the error frame has been written, + // so the client receives the diagnostic before the connection is torn down. Closing + // synchronously here can abort the in-flight flush and drop the frame when the socket + // can't drain it immediately (TCP backpressure, TLS buffering, or a large frame). + // Matches PreV5Handlers.ExceptionHandler and InitialConnectionHandler. + // + // Trade-off of deferring the close (CASSANDRA-21508): + // - There is a slim chance we send two frames with the same streamId. Responses + // already queued on this connection will have a chance to flush before the close + // fires. For the majority of cases, each frame will carry its own unique stream + // id, so nothing is misrouted. These are valid responses to requests that were + // correctly-decoded earlier. + if (isFatal) + promise.addListener(future -> ctx.close()); + ctx.writeAndFlush(payload, promise); + } + finally + { + payload.release(); + } } finally { - payload.release(); JVMStabilityInspector.inspectThrowable(cause); } } diff --git a/src/java/org/apache/cassandra/transport/Flusher.java b/src/java/org/apache/cassandra/transport/Flusher.java index d0b76e0a99..674af5250e 100644 --- a/src/java/org/apache/cassandra/transport/Flusher.java +++ b/src/java/org/apache/cassandra/transport/Flusher.java @@ -36,7 +36,6 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.net.FrameEncoder; import org.apache.cassandra.net.FrameEncoderCrc; import org.apache.cassandra.net.FrameEncoderLZ4; -import org.apache.cassandra.transport.Message.Response; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.memory.BufferPool; @@ -57,22 +56,23 @@ abstract class Flusher implements Runnable interface OnFlushCleanup { void cleanup(FlushItem item); } - static class FlushItem + + public static class FlushItem { enum Kind {FRAMED, UNFRAMED} final Kind kind; final Channel channel; - final T response; - final Envelope request; + final T responseEnvelope; + final Envelope requestEnvelope; final OnFlushCleanup tidy; - FlushItem(Kind kind, Channel channel, T response, Envelope request, OnFlushCleanup tidy) + FlushItem(Kind kind, Channel channel, T responseEnvelope, Envelope requestEnvelope, OnFlushCleanup tidy) { this.kind = kind; this.channel = channel; - this.request = request; - this.response = response; + this.requestEnvelope = requestEnvelope; + this.responseEnvelope = responseEnvelope; this.tidy = tidy; } @@ -85,21 +85,21 @@ abstract class Flusher implements Runnable { final FrameEncoder.PayloadAllocator allocator; Framed(Channel channel, - Envelope response, - Envelope request, + Envelope responseEnvelope, + Envelope requestEnvelope, FrameEncoder.PayloadAllocator allocator, OnFlushCleanup tidy) { - super(Kind.FRAMED, channel, response, request, tidy); + super(Kind.FRAMED, channel, responseEnvelope, requestEnvelope, tidy); this.allocator = allocator; } } - static class Unframed extends FlushItem + static class Unframed extends FlushItem { - Unframed(Channel channel, Response response, Envelope request, OnFlushCleanup tidy) + Unframed(Channel channel, Envelope responseEnvelope, Envelope requestEnvelope, OnFlushCleanup tidy) { - super(Kind.UNFRAMED, channel, response, request, tidy); + super(Kind.UNFRAMED, channel, responseEnvelope, requestEnvelope, tidy); } } } @@ -151,13 +151,13 @@ abstract class Flusher implements Runnable private void processUnframedResponse(FlushItem.Unframed flush) { - flush.channel.write(flush.response, flush.channel.voidPromise()); + flush.channel.write(flush.responseEnvelope, flush.channel.voidPromise()); channels.add(flush.channel); } private void processFramedResponse(FlushItem.Framed flush) { - Envelope outbound = flush.response; + Envelope outbound = flush.responseEnvelope; if (envelopeSize(outbound.header) >= MAX_FRAMED_PAYLOAD_SIZE) { flushLargeMessage(flush.channel, outbound, flush.allocator); @@ -171,7 +171,7 @@ abstract class Flusher implements Runnable payloads.put(flushBuffer.channel, flushBuffer); } - flushBuffer.add(flush.response); + flushBuffer.add(flush.responseEnvelope); } } diff --git a/src/java/org/apache/cassandra/transport/InitialConnectionHandler.java b/src/java/org/apache/cassandra/transport/InitialConnectionHandler.java index 556a560793..28299363fb 100644 --- a/src/java/org/apache/cassandra/transport/InitialConnectionHandler.java +++ b/src/java/org/apache/cassandra/transport/InitialConnectionHandler.java @@ -36,6 +36,7 @@ import org.apache.cassandra.transport.messages.StartupMessage; import org.apache.cassandra.transport.messages.SupportedMessage; import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.VoidChannelPromise; @@ -91,8 +92,7 @@ public class InitialConnectionHandler extends ByteToMessageDecoder supportedOptions.put(StartupMessage.COMPRESSION, compressions); supportedOptions.put(StartupMessage.PROTOCOL_VERSIONS, ProtocolVersion.supportedVersions()); SupportedMessage supported = new SupportedMessage(supportedOptions); - supported.setStreamId(inbound.header.streamId); - outbound = supported.encode(inbound.header.version); + outbound = supported.encode(inbound.header.version, inbound.header.streamId); ctx.writeAndFlush(outbound); break; @@ -131,8 +131,8 @@ public class InitialConnectionHandler extends ByteToMessageDecoder if (null == cause) cause = new ServerError("Unexpected error establishing connection"); logger.warn("Writing response to STARTUP failed, unable to configure pipeline", cause); - ErrorMessage error = ErrorMessage.fromException(cause); - Envelope response = error.encode(inbound.header.version); + ErrorMessage error = ErrorMessage.fromExceptionNoStreamId(cause); + Envelope response = error.encode(inbound.header.version, inbound.header.streamId); ChannelPromise closeChannel = AsyncChannelPromise.withListener(ctx, f -> ctx.close()); ctx.writeAndFlush(response, closeChannel); if (ctx.channel().isOpen()) @@ -152,18 +152,20 @@ public class InitialConnectionHandler extends ByteToMessageDecoder final Message.Response response = Dispatcher.processRequest(ctx.channel(), startup, Overload.NONE, Dispatcher.RequestTime.forImmediateExecution()); - outbound = response.encode(inbound.header.version); + outbound = response.encode(inbound.header.version, inbound.header.streamId); ctx.writeAndFlush(outbound, promise); logger.trace("Configured pipeline: {}", ctx.pipeline()); break; default: ErrorMessage error = - ErrorMessage.fromException( + ErrorMessage.fromTransportException( new ProtocolException(String.format("Unexpected message %s, expecting STARTUP or OPTIONS", inbound.header.type))); - outbound = error.encode(inbound.header.version); - ctx.writeAndFlush(outbound); + outbound = error.encode(inbound.header.version, inbound.header.streamId); + // An unexpected message during initial connection setup leaves the connection in a + // corrupted state; send the error, then close the connection. + ctx.writeAndFlush(outbound).addListener(ChannelFutureListener.CLOSE); } } finally diff --git a/src/java/org/apache/cassandra/transport/Message.java b/src/java/org/apache/cassandra/transport/Message.java index d55e011b3a..117fdd35ef 100644 --- a/src/java/org/apache/cassandra/transport/Message.java +++ b/src/java/org/apache/cassandra/transport/Message.java @@ -65,6 +65,12 @@ public abstract class Message { protected static final Logger logger = LoggerFactory.getLogger(Message.class); + /** + * Sentinel default for a {@link Response}'s stream id; + * must be overwritten before {@link #encode}. + **/ + public static final int UNSET_STREAM_ID = Integer.MIN_VALUE; + public interface Codec extends CBCodec {} public enum Direction @@ -160,7 +166,6 @@ public abstract class Message public final Type type; protected Connection connection; - private int streamId; private Envelope source; private Map customPayload; protected ProtocolVersion forcedProtocolVersion = null; @@ -180,17 +185,6 @@ public abstract class Message return connection; } - public Message setStreamId(int streamId) - { - this.streamId = streamId; - return this; - } - - public int getStreamId() - { - return streamId; - } - public void setSource(Envelope source) { this.source = source; @@ -214,7 +208,7 @@ public abstract class Message @Override public String toString() { - return String.format("(%s:%s:%s)", type, streamId, connection == null ? "null" : connection.getVersion().asInt()); + return String.format("(%s:%s)", type, connection == null ? "null" : connection.getVersion().asInt()); } public static abstract class Request extends Message @@ -342,8 +336,16 @@ public abstract class Message } } - public Envelope encode(ProtocolVersion version) + public Envelope encode(ProtocolVersion version, int streamId) { + // A Response's stream id must be stamped before it is serialized to the wire. UNSET_STREAM_ID here + // means a server code path produced a response without routing information; sending it would risk + // delivering it to an unrelated in-flight request (CASSANDRA-21508). Fail fatally so the connection + // is torn down rather than mis-route a response. Checked before the try below so it is not caught and + // re-wrapped (which would carry the unset id forward). + if (streamId == UNSET_STREAM_ID) + throw ProtocolException.toFatalException(new ProtocolException("Attempted to encode a response with an unset stream id: " + this)); + int flags = Flag.none(); @SuppressWarnings("unchecked") Codec codec = (Codec)this.type.codec; @@ -430,11 +432,11 @@ public abstract class Message if (responseVersion.isBeta()) flags = Flag.add(flags, Flag.USE_BETA); - return Envelope.create(type, getStreamId(), responseVersion, flags, body); + return Envelope.create(type, streamId, responseVersion, flags, body); } catch (Throwable e) { - throw ErrorMessage.wrap(e, getStreamId()); + throw ErrorMessage.wrap(e, streamId); } } @@ -455,7 +457,6 @@ public abstract class Message throw new ProtocolException("Received frame with CUSTOM_PAYLOAD flag for native protocol version < 4"); Message message = inbound.header.type.codec.decode(inbound.body, inbound.header.version); - message.setStreamId(inbound.header.streamId); message.setSource(inbound); message.setCustomPayload(customPayload); diff --git a/src/java/org/apache/cassandra/transport/PipelineConfigurator.java b/src/java/org/apache/cassandra/transport/PipelineConfigurator.java index 3cb88bbb85..2824cfb32a 100644 --- a/src/java/org/apache/cassandra/transport/PipelineConfigurator.java +++ b/src/java/org/apache/cassandra/transport/PipelineConfigurator.java @@ -409,7 +409,7 @@ public class PipelineConfigurator pipeline.addBefore(INITIAL_HANDLER, MESSAGE_DECOMPRESSOR, Envelope.Decompressor.instance); pipeline.addBefore(INITIAL_HANDLER, MESSAGE_COMPRESSOR, Envelope.Compressor.instance); pipeline.addBefore(INITIAL_HANDLER, MESSAGE_DECODER, PreV5Handlers.ProtocolDecoder.instance); - pipeline.addBefore(INITIAL_HANDLER, MESSAGE_ENCODER, PreV5Handlers.ProtocolEncoder.instance); + pipeline.addBefore(INITIAL_HANDLER, MESSAGE_ENCODER, PreV5Handlers.EventMessageEncoder.instance); pipeline.addBefore(INITIAL_HANDLER, LEGACY_MESSAGE_PROCESSOR, new PreV5Handlers.LegacyDispatchHandler(dispatcher, queueBackpressure, limits)); pipeline.remove(INITIAL_HANDLER); onNegotiationComplete(pipeline); diff --git a/src/java/org/apache/cassandra/transport/PreV5Handlers.java b/src/java/org/apache/cassandra/transport/PreV5Handlers.java index f5b9d82f02..630e52ae62 100644 --- a/src/java/org/apache/cassandra/transport/PreV5Handlers.java +++ b/src/java/org/apache/cassandra/transport/PreV5Handlers.java @@ -24,6 +24,7 @@ import java.util.List; import javax.net.ssl.SSLException; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Predicate; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -40,6 +41,7 @@ import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.QueryState; import org.apache.cassandra.transport.ClientResourceLimits.Overload; import org.apache.cassandra.transport.messages.ErrorMessage; +import org.apache.cassandra.transport.messages.EventMessage; import org.apache.cassandra.utils.JVMStabilityInspector; import io.netty.channel.Channel; @@ -93,23 +95,26 @@ public class PreV5Handlers // The only reason we won't process this message is if checkLimits() throws an OverloadedException. // (i.e. Even if backpressure is applied, the current request is allowed to finish.) checkLimits(ctx, request); - dispatcher.dispatch(ctx.channel(), request, this::toFlushItem, backpressure); + dispatcher.dispatch(ctx.channel(), request, this::toFlushItem, ctx, backpressure); } // Acts as a Dispatcher.FlushItemConverter - private Flusher.FlushItem.Unframed toFlushItem(Channel channel, Message.Request request, Message.Response response) + private Flusher.FlushItem.Unframed toFlushItem(ChannelHandlerContext ctx, Channel channel, Message.Request request, Message.Response response) { - return new Flusher.FlushItem.Unframed(channel, response, request.getSource(), this::releaseItem); + ProtocolVersion version = getConnectionVersion(ctx); + Envelope requestEnvelope = request.getSource(); + Envelope responseEnvelope = response.encode(version, requestEnvelope.header.streamId); + return new Flusher.FlushItem.Unframed(channel, responseEnvelope, requestEnvelope, this::releaseItem); } - private void releaseItem(Flusher.FlushItem item) + private void releaseItem(Flusher.FlushItem item) { // Note: in contrast to the equivalent for V5 protocol, CQLMessageHandler::release(FlushItem item), // this does not release the FlushItem's Message.Response. In V4, the buffers for the response's body // and serialised header are emitted directly down the Netty pipeline from Envelope.Encoder, so // releasing them is handled by the pipeline itself. - long itemSize = item.request.header.bodySizeInBytes; - item.request.release(); + long itemSize = item.requestEnvelope.header.bodySizeInBytes; + item.requestEnvelope.release(); // since the request has been processed, decrement inflight payload at channel, endpoint and global levels channelPayloadBytesInFlight -= itemSize; @@ -307,14 +312,14 @@ public class PreV5Handlers * Simple adaptor to plug CQL message encoding into pre-V5 pipelines */ @ChannelHandler.Sharable - public static class ProtocolEncoder extends MessageToMessageEncoder + public static class EventMessageEncoder extends MessageToMessageEncoder { - public static final ProtocolEncoder instance = new ProtocolEncoder(); - private ProtocolEncoder(){} - public void encode(ChannelHandlerContext ctx, Message source, List results) + public static final EventMessageEncoder instance = new EventMessageEncoder(); + private EventMessageEncoder(){} + public void encode(ChannelHandlerContext ctx, EventMessage source, List results) { ProtocolVersion version = getConnectionVersion(ctx); - results.add(source.encode(version)); + results.add(source.encode(version, EventMessage.EVENT_MESSAGE_STREAM_ID)); } } @@ -337,13 +342,28 @@ public class PreV5Handlers if (ctx.channel().isOpen()) { Predicate handler = ExceptionHandlers.getUnexpectedExceptionHandler(ctx.channel(), false); - ErrorMessage errorMessage = ErrorMessage.fromException(cause, handler); - ChannelFuture future = ctx.writeAndFlush(errorMessage.encode(getConnectionVersion(ctx))); - // On protocol exception, close the channel as soon as the message have been sent. - // Most cases of PE are wrapped so the type check below is expected to fail more often than not. - // At this moment Fatal exceptions are not thrown in v4, but just as a precaustion we check for them here - if (isFatal(cause)) - future.addListener((ChannelFutureListener) f -> ctx.close()); + // No request in scope at the channel level; a WrappedException cause carries the frame's + // stream id and overrides this fallback. + ErrorMessage.WithStreamId withStreamId = ErrorMessage.fromException(cause, handler); + + if (withStreamId.streamId == Message.UNSET_STREAM_ID) + { + // No stream id could be recovered, so we have no request to route a response to. + // Close the connection rather than emit an unroutable frame (CASSANDRA-21508). + ctx.close(); + } + else + { + ErrorMessage errorMessage = withStreamId.message; + int streamId = withStreamId.streamId; + + ChannelFuture future = ctx.writeAndFlush(errorMessage.encode(getConnectionVersion(ctx), streamId)); + // On protocol exception, close the channel as soon as the message have been sent. + // Most cases of PE are wrapped so the type check below is expected to fail more often than not. + // At this moment Fatal exceptions are not thrown in v4, but just as a precaustion we check for them here + if (isFatal(cause)) + future.addListener((ChannelFutureListener) f -> ctx.close()); + } } SocketAddress remoteAddress = ctx.channel().remoteAddress(); @@ -393,7 +413,8 @@ public class PreV5Handlers } } - private static ProtocolVersion getConnectionVersion(ChannelHandlerContext ctx) + @VisibleForTesting + static ProtocolVersion getConnectionVersion(ChannelHandlerContext ctx) { Connection connection = ctx.channel().attr(Connection.attributeKey).get(); // The only case the connection can be null is when we send the initial STARTUP message diff --git a/src/java/org/apache/cassandra/transport/SimpleClient.java b/src/java/org/apache/cassandra/transport/SimpleClient.java index 5151a30a96..aee18ac227 100644 --- a/src/java/org/apache/cassandra/transport/SimpleClient.java +++ b/src/java/org/apache/cassandra/transport/SimpleClient.java @@ -352,7 +352,7 @@ public class SimpleClient implements Closeable for (int i = 0; i < requests.size(); i++) { Message.Request message = requests.get(i); - message.setStreamId(i); + message.setSource(new Envelope(Envelope.Header.dummy(i, message.type), null)); message.attach(connection); } lastWriteFuture = channel.writeAndFlush(requests); @@ -365,7 +365,7 @@ public class SimpleClient implements Closeable throw new RuntimeException("timeout"); if (msg instanceof ErrorMessage) throw new RuntimeException((Throwable) ((ErrorMessage) msg).error); - rrMap.put(requests.get(msg.getStreamId()), msg); + rrMap.put(requests.get(msg.getSource().header.streamId), msg); } } else @@ -383,6 +383,18 @@ public class SimpleClient implements Closeable } } + /** + * The stream id to frame an outbound client request with. SimpleClient carries the intended id on the + * request's (dummy) source envelope (see {@link #execute(List)} and callers that pipeline requests). + * When no source has been assigned, we fall back to 0, which is sufficient for the non-pipelined path + * where only a single request is ever in flight. + */ + private static int outboundStreamId(Message message) + { + Envelope source = message.getSource(); + return source == null ? 0 : source.header.streamId; + } + public interface EventHandler { void onEvent(Event event); @@ -438,37 +450,38 @@ public class SimpleClient implements Closeable this.largeMessageThreshold = largeMessageThreshold; } - protected void decode(ChannelHandlerContext ctx, Envelope response, List results) + @Override + protected void decode(ChannelHandlerContext ctx, Envelope request, List results) { - switch(response.header.type) + switch(request.header.type) { case READY: case AUTHENTICATE: - if (response.header.version.isGreaterOrEqualTo(ProtocolVersion.V5)) + if (request.header.version.isGreaterOrEqualTo(ProtocolVersion.V5)) { - configureModernPipeline(ctx, response, largeMessageThreshold); + configureModernPipeline(ctx, request, largeMessageThreshold); // consuming the message is done when setting up the pipeline } else { configureLegacyPipeline(ctx); // really just removes self from the pipeline, so pass this message on - ctx.pipeline().context(Envelope.Decoder.class).fireChannelRead(response); + ctx.pipeline().context(Envelope.Decoder.class).fireChannelRead(request); } break; case SUPPORTED: case ERROR: // just pass through - results.add(response); + results.add(request); break; default: - throw new ProtocolException(String.format("Unexpected %s response expecting " + + throw new ProtocolException(String.format("Unexpected %s request expecting " + "READY, AUTHENTICATE, ERROR or SUPPORTED", - response.header.type)); + request.header.type)); } } - private void configureModernPipeline(ChannelHandlerContext ctx, Envelope response, int largeMessageThreshold) + private void configureModernPipeline(ChannelHandlerContext ctx, Envelope request, int largeMessageThreshold) { logger.info("Configuring modern pipeline"); ChannelPipeline pipeline = ctx.pipeline(); @@ -490,7 +503,7 @@ public class SimpleClient implements Closeable CQLMessageHandler.MessageConsumer responseConsumer = new CQLMessageHandler.MessageConsumer() { - public void dispatch(Channel channel, Message.Response message, Dispatcher.FlushItemConverter toFlushItem, Overload backpressure) + public

void dispatch(Channel channel, Message.Response message, Dispatcher.FlushItemConverter

toFlushItem, P param, Overload backpressure) { responseHandler.handleResponse(channel, message); } @@ -585,15 +598,15 @@ public class SimpleClient implements Closeable ProtocolVersion version = connection == null ? ProtocolVersion.CURRENT : connection.getVersion(); SimpleFlusher flusher = new SimpleFlusher(frameEncoder, largeMessageThreshold); for (Message message : (List) msg) - flusher.enqueue(message.encode(version)); + flusher.enqueue(message.encode(version, outboundStreamId(message))); flusher.maybeWrite(ctx, promise); } }); pipeline.remove(this); - Message.Response message = messageDecoder.decode(ctx.channel(), response); - responseConsumer.dispatch(channel, message, (ch, req, resp) -> null, Overload.NONE); + Message.Response message = messageDecoder.decode(ctx.channel(), request); + responseConsumer.dispatch(channel, message, (p, ch, req, resp) -> null, null, Overload.NONE); } private FrameDecoder frameDecoder(ChannelHandlerContext ctx, BufferPoolAllocator allocator) @@ -638,7 +651,8 @@ public class SimpleClient implements Closeable // The only case the connection can be null is when we send the initial STARTUP message (client side thus) ProtocolVersion version = connection == null ? ProtocolVersion.CURRENT : connection.getVersion(); assert messages.size() == 1; - results.add(messages.get(0).encode(version)); + Message message = messages.get(0); + results.add(message.encode(version, outboundStreamId(message))); } } @@ -662,7 +676,7 @@ public class SimpleClient implements Closeable pipeline.addLast(HandlerNames.INITIAL_HANDLER, new InitialHandler(version, responseHandler, largeMessageThreshold)); pipeline.addLast(HandlerNames.MESSAGE_DECODER, PreV5Handlers.ProtocolDecoder.instance); pipeline.addLast(HandlerNames.MESSAGE_ENCODER, MessageBatchEncoder.instance); - pipeline.addLast(HandlerNames.RESPONSE_HANDLER, responseHandler); + pipeline.addLast(HandlerNames.RESPONSE_HANDLER, responseHandler); } } diff --git a/src/java/org/apache/cassandra/transport/messages/AuthUtil.java b/src/java/org/apache/cassandra/transport/messages/AuthUtil.java index 1c091c7278..2357a41923 100644 --- a/src/java/org/apache/cassandra/transport/messages/AuthUtil.java +++ b/src/java/org/apache/cassandra/transport/messages/AuthUtil.java @@ -87,7 +87,7 @@ public final class AuthUtil { ClientMetrics.instance.markAuthFailure(negotiator.getAuthenticationMode()); AuthEvents.instance.notifyAuthFailure(queryState, e); - return ErrorMessage.fromException(e); + return ErrorMessage.fromTransportException(e); } } diff --git a/src/java/org/apache/cassandra/transport/messages/BatchMessage.java b/src/java/org/apache/cassandra/transport/messages/BatchMessage.java index d40b6d01cb..be697c25dc 100644 --- a/src/java/org/apache/cassandra/transport/messages/BatchMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/BatchMessage.java @@ -236,7 +236,7 @@ public class BatchMessage extends Message.Request { QueryEvents.instance.notifyBatchFailure(prepared, batchType, queryOrIdList, values, options, state, e); JVMStabilityInspector.inspectThrowable(e); - return ErrorMessage.fromException(e); + return ErrorMessage.fromExceptionNoStreamId(e); } } diff --git a/src/java/org/apache/cassandra/transport/messages/ErrorMessage.java b/src/java/org/apache/cassandra/transport/messages/ErrorMessage.java index 36b7f34eb3..fbeb3c66ec 100644 --- a/src/java/org/apache/cassandra/transport/messages/ErrorMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/ErrorMessage.java @@ -423,25 +423,49 @@ public class ErrorMessage extends Message.Response this.error = error; } - private ErrorMessage(TransportException error, int streamId) + public static ErrorMessage fromTransportException(TransportException e) { - this(error); - setStreamId(streamId); + ErrorMessage message = new ErrorMessage(e); + if (e instanceof ProtocolException) + { + // if the driver attempted to connect with a protocol version not supported then + // respond with the appropiate version, see ProtocolVersion.decode() + ProtocolVersion forcedProtocolVersion = ((ProtocolException) e).getForcedProtocolVersion(); + if (forcedProtocolVersion != null) + message.forcedProtocolVersion = forcedProtocolVersion; + } + return message; } - public static ErrorMessage fromException(Throwable e) + public static ErrorMessage fromExceptionNoStreamId(Throwable e) { - return fromException(e, null); + return fromExceptionNoStreamId(e, null); } + public static ErrorMessage fromExceptionNoStreamId(Throwable e, Predicate unexpectedExceptionHandler) + { + return fromException(e, unexpectedExceptionHandler).message; + } + + public static class WithStreamId + { + public final ErrorMessage message; + public final int streamId; + + WithStreamId(ErrorMessage message, int streamId) + { + this.message = message; + this.streamId = streamId; + } + } /** * @param e the exception * @param unexpectedExceptionHandler a callback for handling unexpected exceptions. If null, or if this * returns false, the error is logged at ERROR level via sl4fj */ - public static ErrorMessage fromException(Throwable e, Predicate unexpectedExceptionHandler) + public static WithStreamId fromException(Throwable e, Predicate unexpectedExceptionHandler) { - int streamId = 0; + int streamId = UNSET_STREAM_ID; // Netty will wrap exceptions during decoding in a CodecException. If the cause was one of our ProtocolExceptions // or some other internal exception, extract that and use it. @@ -469,23 +493,15 @@ public class ErrorMessage extends Message.Response if (e instanceof TransportException) { - ErrorMessage message = new ErrorMessage((TransportException) e, streamId); - if (e instanceof ProtocolException) - { - // if the driver attempted to connect with a protocol version not supported then - // respond with the appropiate version, see ProtocolVersion.decode() - ProtocolVersion forcedProtocolVersion = ((ProtocolException) e).getForcedProtocolVersion(); - if (forcedProtocolVersion != null) - message.forcedProtocolVersion = forcedProtocolVersion; - } - return message; + ErrorMessage message = fromTransportException((TransportException) e); + return new WithStreamId(message, streamId); } // Unexpected exception if (unexpectedExceptionHandler == null || !unexpectedExceptionHandler.apply(e)) logger.error("Unexpected exception during request", e); - return new ErrorMessage(new ServerError(e), streamId); + return new WithStreamId(new ErrorMessage(new ServerError(e)), streamId); } @Override diff --git a/src/java/org/apache/cassandra/transport/messages/EventMessage.java b/src/java/org/apache/cassandra/transport/messages/EventMessage.java index 2a2df1dceb..77d958db54 100644 --- a/src/java/org/apache/cassandra/transport/messages/EventMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/EventMessage.java @@ -25,6 +25,8 @@ import io.netty.buffer.ByteBuf; public class EventMessage extends Message.Response { + public static final int EVENT_MESSAGE_STREAM_ID = -1; + public static final Message.Codec codec = new Message.Codec() { public EventMessage decode(ByteBuf body, ProtocolVersion version) @@ -49,7 +51,6 @@ public class EventMessage extends Message.Response { super(Message.Type.EVENT); this.event = event; - this.setStreamId(-1); } @Override diff --git a/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java b/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java index 0f6afe0fe1..eb4c384af7 100644 --- a/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java @@ -207,7 +207,7 @@ public class ExecuteMessage extends Message.Request { QueryEvents.instance.notifyExecuteFailure(prepared, options, state, e); JVMStabilityInspector.inspectThrowable(e); - return ErrorMessage.fromException(e); + return ErrorMessage.fromExceptionNoStreamId(e); } } diff --git a/src/java/org/apache/cassandra/transport/messages/PrepareMessage.java b/src/java/org/apache/cassandra/transport/messages/PrepareMessage.java index e92b69b16e..baf562127e 100644 --- a/src/java/org/apache/cassandra/transport/messages/PrepareMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/PrepareMessage.java @@ -51,12 +51,13 @@ public class PrepareMessage extends Message.Request { String query = CBUtil.readLongString(body); String keyspace = null; - if (version.isGreaterOrEqualTo(ProtocolVersion.V5)) { + if (version.isGreaterOrEqualTo(ProtocolVersion.V5)) + { // If flags grows, we may want to consider creating a PrepareOptions class with an internal codec // class that handles flags and options of the prepare message. Since there's only one right now, // we just take care of business here. - int flags = (int)body.readUnsignedInt(); + int flags = (int) body.readUnsignedInt(); if ((flags & 0x1) == 0x1) { keyspace = CBUtil.readString(body); @@ -74,8 +75,11 @@ public class PrepareMessage extends Message.Request { // If we have no keyspace, write out a 0-valued flag field. if (msg.keyspace == null) + { dest.writeInt(0x0); - else { + } + else + { dest.writeInt(0x1); CBUtil.writeAsciiString(msg.keyspace, dest); } @@ -134,7 +138,7 @@ public class PrepareMessage extends Message.Request { QueryEvents.instance.notifyPrepareFailure(null, query, state, e); JVMStabilityInspector.inspectThrowable(e); - return ErrorMessage.fromException(e); + return ErrorMessage.fromExceptionNoStreamId(e); } } diff --git a/src/java/org/apache/cassandra/transport/messages/QueryMessage.java b/src/java/org/apache/cassandra/transport/messages/QueryMessage.java index 61f3a9cfbd..d858266d7f 100644 --- a/src/java/org/apache/cassandra/transport/messages/QueryMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/QueryMessage.java @@ -129,7 +129,7 @@ public class QueryMessage extends Message.Request JVMStabilityInspector.inspectThrowable(e); if (!((e instanceof RequestValidationException) || (e instanceof RequestExecutionException))) logger.error("Unexpected error during query", e); - return ErrorMessage.fromException(e); + return ErrorMessage.fromExceptionNoStreamId(e); } } diff --git a/src/java/org/apache/cassandra/triggers/TriggerExecutor.java b/src/java/org/apache/cassandra/triggers/TriggerExecutor.java index e3ded966ad..f4e9798214 100644 --- a/src/java/org/apache/cassandra/triggers/TriggerExecutor.java +++ b/src/java/org/apache/cassandra/triggers/TriggerExecutor.java @@ -286,13 +286,33 @@ public class TriggerExecutor } } - public synchronized void loadTriggerClass(String triggerClass) throws Exception + public synchronized Class loadTriggerClass(String triggerClass) throws Exception { // Allow loading the class regardless of Config, since this could happen as part of TCM replay via // CreateTriggerStatement#apply. // Check that triggerClass is available on the classpath, but do not initialize the class since that would // execute static blocks. - customClassLoader.loadClass(triggerClass).getConstructor(); + Class trigger = loadTriggerClassWithoutInitialization(triggerClass); + // Validate that the class exposes a public no-argument constructor before it is accepted. + trigger.getConstructor(); + return trigger; + } + + private Class loadTriggerClassWithoutInitialization(String triggerClass) throws Exception + { + try + { + return FBUtilities.classForNameWithoutInitialization(triggerClass, + "trigger", + ITrigger.class, + customClassLoader); + } + catch (ConfigurationException e) + { + if (e.getCause() instanceof ClassNotFoundException) + throw (ClassNotFoundException) e.getCause(); + throw e; + } } public synchronized ITrigger loadTriggerInstance(String triggerClass) throws Exception @@ -306,6 +326,6 @@ public class TriggerExecutor // double check. if (cachedTriggers.get(triggerClass) != null) return cachedTriggers.get(triggerClass); - return (ITrigger) customClassLoader.loadClass(triggerClass).getConstructor().newInstance(); + return loadTriggerClassWithoutInitialization(triggerClass).getConstructor().newInstance(); } } diff --git a/src/java/org/apache/cassandra/utils/Clock.java b/src/java/org/apache/cassandra/utils/Clock.java index 1d7bd70368..145bab1120 100644 --- a/src/java/org/apache/cassandra/utils/Clock.java +++ b/src/java/org/apache/cassandra/utils/Clock.java @@ -63,7 +63,7 @@ public interface Clock try { outcome = "Using custom clock implementation: " + classname; - clock = (Clock) Class.forName(classname).newInstance(); + clock = FBUtilities.construct(classname, "clock", Clock.class); } catch (Throwable t) { diff --git a/src/java/org/apache/cassandra/utils/FBUtilities.java b/src/java/org/apache/cassandra/utils/FBUtilities.java index a0ec48390c..8bf13f9ea5 100644 --- a/src/java/org/apache/cassandra/utils/FBUtilities.java +++ b/src/java/org/apache/cassandra/utils/FBUtilities.java @@ -679,7 +679,7 @@ public class FBUtilities assert comparator.isPresent() : "Expected a comparator for local partitioner"; return new LocalPartitioner(comparator.get()); } - return FBUtilities.instanceOrConstruct(partitionerClassName, "partitioner"); + return FBUtilities.instanceOrConstruct(partitionerClassName, "partitioner", IPartitioner.class); } public static IAuditLogger newAuditLogger(String className, Map parameters) throws ConfigurationException @@ -689,8 +689,9 @@ public class FBUtilities try { - Class auditLoggerClass = FBUtilities.classForName(className, "Audit logger"); - return (IAuditLogger) auditLoggerClass.getConstructor(Map.class).newInstance(parameters); + Class auditLoggerClass = + FBUtilities.classForNameWithoutInitialization(className, "Audit logger", IAuditLogger.class); + return auditLoggerClass.getConstructor(Map.class).newInstance(parameters); } catch (Exception ex) { @@ -705,12 +706,16 @@ public class FBUtilities try { - Class sslContextFactoryClass = Class.forName(className); - return (ISslContextFactory) sslContextFactoryClass.getConstructor(Map.class).newInstance(parameters); + Class sslContextFactoryClass = + FBUtilities.classForNameWithoutInitialization(className, "ISslContextFactory", ISslContextFactory.class); + return sslContextFactoryClass.getConstructor(Map.class).newInstance(parameters); } catch (Exception ex) { - throw new ConfigurationException("Unable to create instance of ISslContextFactory for " + className, ex); + // Surface the underlying load failure (e.g. ClassNotFoundException) as the direct cause rather than the + // intermediate ConfigurationException that reports it. + Throwable cause = ex instanceof ConfigurationException && ex.getCause() != null ? ex.getCause() : ex; + throw new ConfigurationException("Unable to create instance of ISslContextFactory for " + className, cause); } } @@ -721,8 +726,9 @@ public class FBUtilities if (!className.contains(".")) className = "org.apache.cassandra.security." + className; - Class cryptoProviderClass = FBUtilities.classForName(className, "crypto provider class"); - return (AbstractCryptoProvider) cryptoProviderClass.getConstructor(Map.class).newInstance(Collections.unmodifiableMap(parameters)); + Class cryptoProviderClass = + FBUtilities.classForNameWithoutInitialization(className, "crypto provider class", AbstractCryptoProvider.class); + return cryptoProviderClass.getConstructor(Map.class).newInstance(Collections.unmodifiableMap(parameters)); } catch (Exception e) { @@ -741,8 +747,9 @@ public class FBUtilities if (!className.contains(".")) className = "org.apache.cassandra.io.compress." + className; - Class compressionProviderClass = FBUtilities.classForName(className, "compression service provider"); - return (AbstractCompressionProvider) compressionProviderClass.getConstructor().newInstance(); + Class compressionProviderClass = + FBUtilities.classForNameWithoutInitialization(className, "compression service provider", AbstractCompressionProvider.class); + return compressionProviderClass.getConstructor().newInstance(); } catch (ConfigurationException e) { @@ -755,6 +762,8 @@ public class FBUtilities } /** + * Loads and initializes a class. + * * @return The Class for the given name. * @param classname Fully qualified classname. * @param readable Descriptive noun for the role the class plays. @@ -772,6 +781,53 @@ public class FBUtilities } } + /** + * Loads a class without initializing it, then verifies it extends or implements the expected base type. + * + * @return The Class for the given name. + * @param classname Fully qualified classname. + * @param readable Descriptive noun for the role the class plays. + * @param expectedType Required superclass or interface. + * @throws ConfigurationException If the class cannot be found or is not assignable to {@code expectedType}. + */ + public static Class classForNameWithoutInitialization(String classname, + String readable, + Class expectedType) throws ConfigurationException + { + return classForNameWithoutInitialization(classname, readable, expectedType, FBUtilities.class.getClassLoader()); + } + + /** + * Loads a class without initializing it, then verifies it extends or implements the expected base type. + * + * @return The Class for the given name. + * @param classname Fully qualified classname. + * @param readable Descriptive noun for the role the class plays. + * @param expectedType Required superclass or interface. + * @param classLoader ClassLoader to use. + * @throws ConfigurationException If the class cannot be found or is not assignable to {@code expectedType}. + */ + public static Class classForNameWithoutInitialization(String classname, + String readable, + Class expectedType, + ClassLoader classLoader) throws ConfigurationException + { + try + { + Class klass = Class.forName(classname, false, classLoader); + if (!expectedType.isAssignableFrom(klass)) + throw new ConfigurationException(String.format("Invalid %s class '%s': must extend or implement %s", + readable, + classname, + expectedType.getName())); + return klass.asSubclass(expectedType); + } + catch (ClassNotFoundException | NoClassDefFoundError e) + { + throw new ConfigurationException(String.format("Unable to find %s class '%s'", readable, classname), e); + } + } + /** * Constructs an instance of the given class, which must have a no-arg or default constructor. * @param classname Fully qualified classname. @@ -781,6 +837,25 @@ public class FBUtilities public static T instanceOrConstruct(String classname, String readable) throws ConfigurationException { Class cls = FBUtilities.classForName(classname, readable); + return instanceOrConstruct(cls, classname, readable); + } + + /** + * Constructs an instance of the given class, or gets its static {@code instance} field, after verifying the + * class without initializing it. + * @param classname Fully qualified classname. + * @param readable Descriptive noun for the role the class plays. + * @param expectedType Required superclass or interface. + * @throws ConfigurationException If the class cannot be found or is not assignable to {@code expectedType}. + */ + public static T instanceOrConstruct(String classname, String readable, Class expectedType) throws ConfigurationException + { + Class cls = FBUtilities.classForNameWithoutInitialization(classname, readable, expectedType); + return instanceOrConstruct(cls, classname, readable); + } + + private static T instanceOrConstruct(Class cls, String classname, String readable) throws ConfigurationException + { try { Field instance = cls.getField("instance"); @@ -805,7 +880,20 @@ public class FBUtilities return construct(cls, classname, readable); } - private static T construct(Class cls, String classname, String readable) throws ConfigurationException + /** + * Constructs an instance of the given class after verifying it without initializing it. + * @param classname Fully qualified classname. + * @param readable Descriptive noun for the role the class plays. + * @param expectedType Required superclass or interface. + * @throws ConfigurationException If the class cannot be found or is not assignable to {@code expectedType}. + */ + public static T construct(String classname, String readable, Class expectedType) throws ConfigurationException + { + Class cls = FBUtilities.classForNameWithoutInitialization(classname, readable, expectedType); + return construct(cls, classname, readable); + } + + private static T construct(Class cls, String classname, String readable) throws ConfigurationException { try { @@ -1496,4 +1584,4 @@ public class FBUtilities }); return adapter; } -} \ No newline at end of file +} diff --git a/src/java/org/apache/cassandra/utils/JMXServerUtils.java b/src/java/org/apache/cassandra/utils/JMXServerUtils.java index 72851d74ad..85e5ec81f0 100644 --- a/src/java/org/apache/cassandra/utils/JMXServerUtils.java +++ b/src/java/org/apache/cassandra/utils/JMXServerUtils.java @@ -222,7 +222,7 @@ public class JMXServerUtils String authzProxyClass = options.authorizer; if (authzProxyClass != null) { - final InvocationHandler handler = FBUtilities.construct(authzProxyClass, "JMX authz proxy"); + final InvocationHandler handler = FBUtilities.construct(authzProxyClass, "JMX authz proxy", InvocationHandler.class); final Class[] interfaces = { MBeanServerForwarder.class }; Object proxy = Proxy.newProxyInstance(MBeanServerForwarder.class.getClassLoader(), interfaces, handler); diff --git a/src/java/org/apache/cassandra/utils/MBeanWrapper.java b/src/java/org/apache/cassandra/utils/MBeanWrapper.java index d1d82ca1f6..691d5170d6 100644 --- a/src/java/org/apache/cassandra/utils/MBeanWrapper.java +++ b/src/java/org/apache/cassandra/utils/MBeanWrapper.java @@ -78,7 +78,7 @@ public interface MBeanWrapper return new PlatformMBeanWrapper(); } } - return FBUtilities.construct(klass, "mbean"); + return FBUtilities.construct(klass, "mbean", MBeanWrapper.class); } // Passing true for graceful will log exceptions instead of rethrowing them diff --git a/src/java/org/apache/cassandra/utils/MergeIterator.java b/src/java/org/apache/cassandra/utils/MergeIterator.java index b156e20a0a..dff1677d61 100644 --- a/src/java/org/apache/cassandra/utils/MergeIterator.java +++ b/src/java/org/apache/cassandra/utils/MergeIterator.java @@ -21,6 +21,8 @@ import java.util.Comparator; import java.util.Iterator; import java.util.List; +import net.nicoulaj.compilecommand.annotations.DontInline; + import accord.utils.Invariants; /** Merges sorted input iterators which individually contain unique items. */ @@ -307,9 +309,36 @@ public abstract class MergeIterator extends AbstractIterator implem heap[currIdx] = heap[nextIdx]; currIdx = nextIdx; } + // If the candidate has no children in the binary-heap section it is already in its final position, so we can + // skip the (rarely needed) sink below. nextIdx is the candidate's left child; anything >= size means there + // are no children. The single-child (nextIdx == size - 1) and two-children cases still have to go through + // sinkInBinaryHeap, which compares against the child(ren) and may swap or update the equalParent flag. + nextIdx = (currIdx * 2) - (sortedSectionSize - 1); + if (nextIdx >= size) + { + heap[currIdx] = candidate; + return; + } + // The candidate did not settle within the sorted section; sink it through the binary-heap section. This is + // rarely reached for typical narrow, lightly-overlapping merges, so it is split into its own method to keep + // the hot sorted-section path above small enough to be inlined into advance(). + sinkInBinaryHeap(candidate, currIdx, size, sortedSectionSize); + } + + /** + * Sink {@code candidate} down the binary-heap section of the queue (the part below the sorted section), pulling + * up the lighter child at each level, and place it in its final position. + * + * Split out of {@link #replaceAndSink} so that the common case, where the candidate settles within the sorted + * section, stays compact and inlinable. + */ + @DontInline + private void sinkInBinaryHeap(Candidate candidate, int currIdx, final int size, final int sortedSectionSize) + { // If size <= SORTED_SECTION_SIZE, nextIdx below will be no less than size, // because currIdx == sortedSectionSize == size - 1 and nextIdx becomes // (size - 1) * 2) - (size - 1 - 1) == size. + int nextIdx; // Advance in the binary heap, pulling up the lighter element from the two at each level. while ((nextIdx = (currIdx * 2) - (sortedSectionSize - 1)) + 1 < size) diff --git a/src/java/org/apache/cassandra/utils/MonotonicClock.java b/src/java/org/apache/cassandra/utils/MonotonicClock.java index 2300cf2e89..0b63835120 100644 --- a/src/java/org/apache/cassandra/utils/MonotonicClock.java +++ b/src/java/org/apache/cassandra/utils/MonotonicClock.java @@ -94,7 +94,7 @@ public interface MonotonicClock try { logger.debug("Using custom clock implementation: {}", sclock); - return (MonotonicClock) Class.forName(sclock).newInstance(); + return FBUtilities.construct(sclock, "monotonic clock", MonotonicClock.class); } catch (Exception e) { @@ -113,7 +113,8 @@ public interface MonotonicClock try { logger.debug("Using custom clock implementation: {}", sclock); - Class clazz = (Class) Class.forName(sclock); + Class clazz = + FBUtilities.classForNameWithoutInitialization(sclock, "monotonic clock", MonotonicClock.class); if (SystemClock.class.equals(clazz) && SystemClock.class.equals(precise.getClass())) return precise; diff --git a/test/burn/org/apache/cassandra/transport/DriverBurnTest.java b/test/burn/org/apache/cassandra/transport/DriverBurnTest.java index 236d9b60dc..601ac6be4f 100644 --- a/test/burn/org/apache/cassandra/transport/DriverBurnTest.java +++ b/test/burn/org/apache/cassandra/transport/DriverBurnTest.java @@ -340,10 +340,10 @@ public class DriverBurnTest extends CQLTester SimpleStatement request = generateQueryStatement(0, requestCaps); ResultMessage.Rows response = generateRows(0, responseCaps); QueryMessage requestMessage = generateQueryMessage(0, requestCaps, version); - Envelope message = requestMessage.encode(version); + Envelope message = requestMessage.encode(version, 0); int requestSize = message.body.readableBytes(); message.release(); - message = response.encode(version); + message = response.encode(version, 0); int responseSize = message.body.readableBytes(); message.release(); Message.Type.QUERY.unsafeSetCodec(new Message.Codec() { diff --git a/test/burn/org/apache/cassandra/transport/SimpleClientPerfTest.java b/test/burn/org/apache/cassandra/transport/SimpleClientPerfTest.java index cfd54f34e1..5db4b9d928 100644 --- a/test/burn/org/apache/cassandra/transport/SimpleClientPerfTest.java +++ b/test/burn/org/apache/cassandra/transport/SimpleClientPerfTest.java @@ -155,10 +155,10 @@ public class SimpleClientPerfTest { ResultMessage.Rows response = generateRows(0, responseCaps); QueryMessage requestMessage = generateQueryMessage(0, requestCaps, version); - Envelope message = requestMessage.encode(version); + Envelope message = requestMessage.encode(version, 0); int requestSize = message.body.readableBytes(); message.release(); - message = response.encode(version); + message = response.encode(version, 0); int responseSize = message.body.readableBytes(); message.release(); diff --git a/test/distributed/org/apache/cassandra/distributed/test/DecommissionTest.java b/test/distributed/org/apache/cassandra/distributed/test/DecommissionTest.java index ad0120513b..41f8274ae5 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/DecommissionTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/DecommissionTest.java @@ -21,6 +21,7 @@ package org.apache.cassandra.distributed.test; import java.io.IOException; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; import net.bytebuddy.ByteBuddy; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; @@ -141,6 +142,7 @@ public class DecommissionTest extends TestBaseImpl public static class BB { + static final AtomicBoolean injected = new AtomicBoolean(false); public static void install(ClassLoader classLoader, Integer num) { if (num == 2) @@ -157,7 +159,7 @@ public class DecommissionTest extends TestBaseImpl public static void execute(NodeId leaving, PlacementDeltas startLeave, PlacementDeltas midLeave, PlacementDeltas finishLeave, @SuperCall Callable zuper) throws ExecutionException, InterruptedException { - if (!StorageService.instance.isDecommissionFailed()) + if (injected.compareAndSet(false, true)) throw new ExecutionException(new RuntimeException("simulated error in prepareUnbootstrapStreaming")); try diff --git a/test/distributed/org/apache/cassandra/distributed/test/GetEndpointsTest.java b/test/distributed/org/apache/cassandra/distributed/test/GetEndpointsTest.java index 214d6f698f..aa89d12417 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/GetEndpointsTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/GetEndpointsTest.java @@ -81,9 +81,9 @@ public class GetEndpointsTest extends TestBaseImpl for (IInvokableInstance i : cluster) { i.runOnInstance(() -> { - List endpoints = StorageService.instance.getNaturalEndpointsWithPort("system_cluster_metadata", "distributed_metadata_log", "1"); - assertEquals(endpoints.toString(), 3, endpoints.size()); - assertEquals(Replicas.stringify(ClusterMetadata.current().fullCMSMembersAsReplicas(), true), endpoints); + List replicas = StorageService.instance.getNaturalReplicasWithPort("system_cluster_metadata", "distributed_metadata_log", "1"); + assertEquals(replicas.toString(), 3, replicas.size()); + assertEquals(Replicas.stringify(ClusterMetadata.current().fullCMSMembersAsReplicas(), true), replicas); }); } diff --git a/test/distributed/org/apache/cassandra/distributed/test/GossipSettlesTest.java b/test/distributed/org/apache/cassandra/distributed/test/GossipSettlesTest.java index e86dcdf383..b67bbe5ed4 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/GossipSettlesTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/GossipSettlesTest.java @@ -19,6 +19,7 @@ package org.apache.cassandra.distributed.test; import java.net.InetAddress; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -32,6 +33,8 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.gms.FailureDetector; import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.locator.EndpointsForToken; +import org.apache.cassandra.locator.Replicas; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.SystemDistributedKeyspace; @@ -78,16 +81,22 @@ public class GossipSettlesTest extends TestBaseImpl Assert.assertEquals(addPortToValues(ss.getHostIdToEndpoint()), ss.getHostIdToEndpointWithPort()); Assert.assertEquals(addPortToKeys(ss.getLoadMap()), ss.getLoadMapWithPort()); Assert.assertEquals(addPortToList(ss.getLiveNodes()), ss.getLiveNodesWithPort()); - List naturalEndpointsAddedPort = ss.getNaturalEndpoints(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, + List naturalReplicasAddedPort = ss.getNaturalReplicas(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SystemDistributedKeyspace.VIEW_BUILD_STATUS, "dummy").stream() .map(e -> addStoragePortToIP(e.getHostAddress())).collect(Collectors.toList()); - Assert.assertEquals(naturalEndpointsAddedPort, - ss.getNaturalEndpointsWithPort(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, - SystemDistributedKeyspace.VIEW_BUILD_STATUS, "dummy")); - naturalEndpointsAddedPort = ss.getNaturalEndpoints(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, ByteBufferUtil.EMPTY_BYTE_BUFFER).stream() - .map(e -> addStoragePortToIP(e.getHostAddress())).collect(Collectors.toList()); - Assert.assertEquals(naturalEndpointsAddedPort, - ss.getNaturalEndpointsWithPort(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, ByteBufferUtil.EMPTY_BYTE_BUFFER)); + Assert.assertEquals(naturalReplicasAddedPort, + ss.getNaturalReplicasWithPort(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, + SystemDistributedKeyspace.VIEW_BUILD_STATUS, "dummy")); + + EndpointsForToken replicas = ss.getNaturalReplicasForToken(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, ByteBufferUtil.EMPTY_BYTE_BUFFER); + List inetList = new ArrayList<>(replicas.size()); + replicas.forEach(r -> inetList.add(r.endpoint().getAddress())); + + naturalReplicasAddedPort = inetList.stream().map(e -> addStoragePortToIP(e.getHostAddress())).collect(Collectors.toList()); + + List replicas2 = Replicas.stringify(ss.getNaturalReplicasForToken(SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, ByteBufferUtil.EMPTY_BYTE_BUFFER), true); + + Assert.assertEquals(naturalReplicasAddedPort, replicas2); // Difference in key type... convert to String and add the port to the older format diff --git a/test/distributed/org/apache/cassandra/distributed/test/StreamIdMisrouteTest.java b/test/distributed/org/apache/cassandra/distributed/test/StreamIdMisrouteTest.java new file mode 100644 index 0000000000..430fdb6de0 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/StreamIdMisrouteTest.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.util.ArrayList; +import java.util.Collections; +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 java.util.concurrent.atomic.AtomicBoolean; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.SuperCall; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.statements.SelectStatement; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.exceptions.OverloadedException; +import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.transport.Envelope; +import org.apache.cassandra.transport.Message; +import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.transport.SimpleClient; +import org.apache.cassandra.transport.messages.ErrorMessage; +import org.apache.cassandra.transport.messages.QueryMessage; +import org.apache.cassandra.transport.messages.ResultMessage; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; +import static org.apache.cassandra.config.DatabaseDescriptor.clientInitialization; +import static org.apache.cassandra.config.DatabaseDescriptor.getNativeTransportPort; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; + +/** + * Regression test for CASSANDRA-21508. A coordinator that load-sheds a request which waited past its + * native-transport queue deadline must stamp the OVERLOADED error with the timed-out request's own + * stream id. Before the fix the error went out on stream id 0, which misroutes it to an unrelated + * in-flight request and can escalate into the client-side "column-shift" read corruption (a value + * from one table decoded against another query's column definitions under the v5 skip-metadata + * optimization). + * + *

The original defect: {@code Dispatcher.processRequest} load-sheds a request that has waited in the + * Native-Transport-Requests queue longer than {@code native_transport_timeout} by returning + * + *

+ *   ErrorMessage.fromException(new OverloadedException("Query timed out before it could start"))
+ * 
+ *

+ * without calling {@code setStreamId(request.getStreamId())}. {@code ErrorMessage.streamId} therefore + * kept its default of 0, and the error frame was written to the client on stream id 0 instead of the + * timed-out request's real stream id. The fix routes every response through a central stamping step + * and makes {@code ErrorMessage.fromException} require the stream id at the call site. + * + *

{@link #loadShedErrorIsStampedWithRequestStreamId()} proves the fix on the wire: a request sent on + * a non-zero stream id is load-shed and the OVERLOADED error comes back on that same stream id (not 0). + * Against the unpatched server this assertion fails with the error arriving on stream id 0. + * + *

Why the stream id matters: on a busy connection stream id 0 is almost always in use, so an error + * mis-stamped with 0 is applied to an unrelated in-flight request. The client then frees and reuses + * stream id 0 for a new query, and when the original query's real rows arrive on it they are decoded + * positionally against the reusing query's cached column definitions (skip-metadata carries no column + * info) - shifting each value into the wrong column and either throwing in a codec or silently + * returning a plausible-but-wrong value. + */ +public class StreamIdMisrouteTest extends TestBaseImpl +{ + private static final int TIMED_OUT_STREAM_ID = 42; // any non-zero id; before the fix the reply was forced to 0 + + @BeforeClass + public static void initClientSide() + { + // SimpleClient runs in the test classloader; it needs DatabaseDescriptor initialized here to + // build the native-protocol pipeline. Without this, connect() fails with a ClosedChannelException. + clientInitialization(); + } + + /** + * Proves the fix directly: under native-transport queue backlog, the load-shed OVERLOADED error is + * returned on the stream id of the request that actually timed out, not on stream id 0. Against the + * unpatched server this fails with the error arriving on stream id 0. + */ + @Test + public void loadShedErrorIsStampedWithRequestStreamId() throws Throwable + { + try (Cluster cluster = init(Cluster.build().withNodes(1) + .withInstanceInitializer(SlowSelect::install) + .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL) + // one NTR thread so a slow query blocks the queue head + .set("native_transport_max_threads", 1) + // short deadline so a queued request is shed quickly + .set("native_transport_timeout", "150ms") + .set("read_request_timeout", "500ms") + .set("range_request_timeout", "500ms")) + .start())) + { + cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int PRIMARY KEY, v int)")); + + InetSocketHost host = hostAndPort(cluster); + ExecutorService executor = Executors.newCachedThreadPool(); + + // Two independent connections. A SimpleClient's response queue is a SynchronousQueue, so + // concurrent execute() calls on ONE client would race; we use one client to build the + // backlog and a second, dedicated client for the request whose stream id we assert on. + try (SimpleClient filler = SimpleClient.builder(host.address, host.port) + .protocolVersion(ProtocolVersion.V5).build().connect(false); + SimpleClient victim = SimpleClient.builder(host.address, host.port) + .protocolVersion(ProtocolVersion.V5).build().connect(false)) + { + // Warm up both connections while the server is still fast. + filler.execute(query(withKeyspace("SELECT * FROM %s.tbl"), 1)); + victim.execute(query(withKeyspace("SELECT * FROM %s.tbl"), 1)); + + // From here every non-internal SELECT sleeps 1s, far past the 150ms queue deadline. + cluster.get(1).runOnInstance(() -> Assert.assertTrue(SlowSelect.enabled.compareAndSet(false, true))); + + // Saturate the single NTR worker and pile several requests behind it, so anything + // enqueued now waits well beyond native_transport_timeout before a worker frees up. + List> backlog = new ArrayList<>(); + for (int i = 0; i < 8; i++) + { + int streamId = 10 + i; + backlog.add(executor.submit(() -> + filler.execute(query(withKeyspace("SELECT * FROM %s.tbl"), streamId), false))); + } + + // Let the backlog form and the queue-time clock run past the 150ms deadline. + TimeUnit.MILLISECONDS.sleep(800); + + // This request enqueues behind a queue that is already older than the deadline, so the + // worker load-sheds it immediately when it is dequeued. It goes out on stream id 42. + Message.Response shed = + victim.execute(query(withKeyspace("SELECT * FROM %s.tbl"), TIMED_OUT_STREAM_ID), false); + + Assert.assertTrue("Expected an OVERLOADED error, got: " + shed, + shed instanceof ErrorMessage + && ((ErrorMessage) shed).error instanceof OverloadedException); + + // The fix: the request went out on stream id 42, and the load-shed error now comes back + // on stream id 42 too because Dispatcher stamps every response with the request's id. + Assert.assertEquals("Load-shed OVERLOADED error should carry the request's own stream id (" + + TIMED_OUT_STREAM_ID + ')', + TIMED_OUT_STREAM_ID, shed.getSource().header.streamId); + + // Drain the backlog so the connection can close cleanly. + cluster.get(1).runOnInstance(() -> SlowSelect.enabled.set(false)); + for (Future f : backlog) + { + try + { + f.get(30, TimeUnit.SECONDS); + } + catch (Exception ignored) + { /* shed/timed out */ } + } + } + finally + { + cluster.get(1).runOnInstance(() -> SlowSelect.enabled.set(false)); + executor.shutdownNow(); + } + } + } + + // ------------------------------------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------------------------------------ + + private static QueryMessage query(String cql, int streamId) + { + QueryMessage msg = new QueryMessage(cql, QueryOptions.forInternalCalls( + org.apache.cassandra.db.ConsistencyLevel.ONE, Collections.emptyList())); + msg.setSource(new Envelope(Envelope.Header.dummy(streamId, Message.Type.QUERY), null)); + return msg; + } + + private static InetSocketHost hostAndPort(Cluster cluster) + { + // Node 1's native transport binds on its broadcast address; the port is whatever the in-JVM + // provisioning assigned (9042 for the default multi-interface strategy). Derive both from the + // instance config so this works regardless of provisioning strategy. + String address = cluster.get(1).config().broadcastAddress().getAddress().getHostAddress(); + int port = cluster.get(1).callOnInstance( + () -> getNativeTransportPort()); + return new InetSocketHost(address, port); + } + + private static final class InetSocketHost + { + final String address; + final int port; + + InetSocketHost(String address, int port) + { + this.address = address; + this.port = port; + } + } + + /** + * ByteBuddy interceptor that makes client-issued SELECTs sleep past the queue deadline, so a + * request queued behind one is load-shed by Dispatcher. Mirrors OverloadTest.SlowSelect. + */ + public static class SlowSelect + { + static final AtomicBoolean enabled = new AtomicBoolean(false); + + static void install(ClassLoader cl, int nodeNumber) + { + new ByteBuddy().rebase(SelectStatement.class) + .method(named("execute").and(takesArguments(QueryState.class, QueryOptions.class, Dispatcher.RequestTime.class))) + .intercept(MethodDelegation.to(SlowSelect.class)) + .make() + .load(cl, ClassLoadingStrategy.Default.INJECTION); + } + + @SuppressWarnings("unused") + public static ResultMessage.Rows execute(QueryState state, QueryOptions options, + Dispatcher.RequestTime requestTime, + @SuperCall Callable zuper) throws Exception + { + if (enabled.get() && !state.getClientState().isInternal) + TimeUnit.SECONDS.sleep(1); + return zuper.call(); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/UnableToParseClientMessageTest.java b/test/distributed/org/apache/cassandra/distributed/test/UnableToParseClientMessageTest.java index 2e2bce9611..f6755dae3d 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/UnableToParseClientMessageTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/UnableToParseClientMessageTest.java @@ -185,9 +185,9 @@ public class UnableToParseClientMessageTest extends TestBaseImpl } @Override - public Envelope encode(ProtocolVersion version) + public Envelope encode(ProtocolVersion version, int streamId) { - Envelope base = super.encode(version); + Envelope base = super.encode(version, streamId); return new CustomHeaderEnvelope(base.header, base.body, headerEncoded); } } @@ -228,7 +228,7 @@ public class UnableToParseClientMessageTest extends TestBaseImpl } @Override - public Envelope encode(ProtocolVersion version) + public Envelope encode(ProtocolVersion version, int streamId) { Codec originalCodec = type.codec; try @@ -255,7 +255,7 @@ public class UnableToParseClientMessageTest extends TestBaseImpl } }); - return super.encode(version); + return super.encode(version, streamId); } finally { diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/DecommissionTest.java b/test/distributed/org/apache/cassandra/distributed/test/ring/DecommissionTest.java index 307956828e..f6ba5c33a1 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/ring/DecommissionTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/ring/DecommissionTest.java @@ -53,8 +53,10 @@ import org.apache.cassandra.service.StorageService; import org.apache.cassandra.streaming.StreamSession; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.tcm.membership.NodeId; import org.apache.cassandra.tcm.membership.NodeVersion; +import org.apache.cassandra.tcm.transformations.PrepareLeave; import org.apache.cassandra.tcm.transformations.Startup; import org.apache.cassandra.utils.CassandraVersion; import org.apache.cassandra.utils.FBUtilities; @@ -62,6 +64,8 @@ import org.apache.cassandra.utils.FBUtilities; import static net.bytebuddy.matcher.ElementMatchers.named; import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.api.Feature.NETWORK; +import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeCommit; +import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseCommits; import static org.apache.cassandra.distributed.shared.NetworkTopology.dcAndRack; import static org.apache.cassandra.distributed.shared.NetworkTopology.networkTopology; import static org.apache.cassandra.distributed.test.ring.BootstrapTest.populate; @@ -84,6 +88,43 @@ public class DecommissionTest extends TestBaseImpl } } + @Test + public void testOperationModeOnDecomResume() throws Exception + { + // Node 2's first decomission attempt fails mid-stream (injected by BB), leaving it in + // DECOMISSION_FAILED. On resume, operationMode() must transition back to LEAVING before + // MID_LEAVE is committed. We pause the CMS just before that commit to assert the mode + // in the window where streaming has finished but the epoch has not yet advanced. + try (Cluster cluster = builder().withNodes(3) + .withConfig(config -> config.with(NETWORK, GOSSIP)) + .withInstanceInitializer(BB::install) + .start()) + { + populate(cluster, 0, 100, 1, 2, ConsistencyLevel.QUORUM); + + IInvokableInstance cmsNode = cluster.get(1); + IInvokableInstance leavingNode = cluster.get(2); + + // --force required: cie_internal keyspace has RF=3, decommission would fail replication check otherwise + leavingNode.nodetoolResult("decommission", "--force").asserts().failure(); + leavingNode.runOnInstance(() -> assertEquals(StorageService.Mode.DECOMMISSION_FAILED, StorageService.instance.operationMode())); + + Callable midLeavePaused = pauseBeforeCommit(cmsNode, e -> e instanceof PrepareLeave.MidLeave); + + Thread resumeThread = new Thread(() -> leavingNode.nodetoolResult("decommission").asserts().success()); + resumeThread.start(); + midLeavePaused.call(); + + leavingNode.runOnInstance(() -> + assertEquals("operationMode during resumed decommission streaming should be LEAVING, not DECOMMISSION_FAILED", + StorageService.Mode.LEAVING, + StorageService.instance.operationMode())); + + unpauseCommits(cmsNode); + resumeThread.join(TimeUnit.MINUTES.toMillis(2)); + } + } + @Test public void testAddressReuseAfterDecommission() throws IOException, ExecutionException, InterruptedException { diff --git a/test/distributed/org/apache/cassandra/distributed/test/tcm/CMSShutdownTest.java b/test/distributed/org/apache/cassandra/distributed/test/tcm/CMSShutdownTest.java index 6d8debdd7f..74dece0661 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/tcm/CMSShutdownTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/tcm/CMSShutdownTest.java @@ -50,7 +50,7 @@ public class CMSShutdownTest extends TestBaseImpl // This test simulates a CMS node attempting to commit an entry to the log but being unable // to obtain consensus from other CMS members while it is also shutting down itself. try (Cluster cluster = Cluster.build(2) - .withConfig(c -> c.with(Feature.values())) + .withConfig(c -> c.with(Feature.GOSSIP, Feature.NETWORK)) .withInstanceInitializer(BBHelper::install) .start()) { @@ -67,8 +67,15 @@ public class CMSShutdownTest extends TestBaseImpl // latch ensures that every commit will fail as if unable to obtain consensus // from other CMS members State.latch.countDown(); - for (; ;) - ClusterMetadataService.instance().commit(TriggerSnapshot.instance); + try + { + while (!Thread.currentThread().isInterrupted()) + ClusterMetadataService.instance().commit(TriggerSnapshot.instance); + } + catch (Throwable t) + { + // Commits fail by design; an interrupt or a stopped processor on shutdown ends them. + } } public static void scheduleCommits() diff --git a/test/microbench/org/apache/cassandra/test/microbench/AbstractTypeSerializationBench.java b/test/microbench/org/apache/cassandra/test/microbench/AbstractTypeSerializationBench.java new file mode 100644 index 0000000000..0e4452ad60 --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/AbstractTypeSerializationBench.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.test.microbench; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.BooleanType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UUIDType; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.utils.ByteBufferUtil; + +/** + * Measures value serialization when one call site sees the fixed- and variable-width types commonly used by a table. + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(value = 1, jvmArgsAppend = { "-Xmx1G", "-Djmh.executor=CUSTOM", "-Djmh.executor.class=org.apache.cassandra.test.microbench.FastThreadExecutor" }) +@Threads(1) +@State(Scope.Thread) +public class AbstractTypeSerializationBench +{ + private final AbstractType[] types = { Int32Type.instance, LongType.instance, BooleanType.instance, UUIDType.instance, + UTF8Type.instance, Int32Type.instance, LongType.instance, UTF8Type.instance }; + private final ByteBuffer[] values = { ByteBufferUtil.bytes(42), ByteBufferUtil.bytes(42L), ByteBuffer.wrap(new byte[] { 1 }), + ByteBuffer.wrap(new byte[16]), ByteBufferUtil.bytes("cassandra"), ByteBufferUtil.bytes(42), + ByteBufferUtil.bytes(42L), ByteBufferUtil.bytes("cassandra") }; + private final ByteBuffer[] serializedValues = new ByteBuffer[types.length]; + private final DataOutputBuffer out = new DataOutputBuffer(); + private int index; + + @Setup + public void setup() throws IOException + { + for (int i = 0; i < types.length; i++) + { + out.clear(); + types[i].writeValue(values[i], out); + serializedValues[i] = out.asNewBuffer(); + } + } + + @Benchmark + public int writeValue() throws IOException + { + int i = index++ & (types.length - 1); + out.clear(); + types[i].writeValue(values[i], out); + return out.getLength(); + } + + @Benchmark + public ByteBuffer readValue() throws IOException + { + int i = index++ & (types.length - 1); + return types[i].readBuffer(new DataInputBuffer(serializedValues[i], true)); + } +} diff --git a/test/resources/nodetool/help/getendpoints b/test/resources/nodetool/help/getendpoints index 7eb9aef60d..5c223feb21 100644 --- a/test/resources/nodetool/help/getendpoints +++ b/test/resources/nodetool/help/getendpoints @@ -1,5 +1,6 @@ NAME - nodetool getendpoints - Print the end points that owns the key + nodetool getendpoints - Print the end points that owns the key, + deprecated, use getreplicas instead SYNOPSIS nodetool [(-h | --host )] [(-p | --port )] @@ -34,4 +35,4 @@ OPTIONS

The keyspace, the table, and the partition key for which we need to - find the endpoint + find the replica (e.g., pk1:pk2:pk3 for compound keys) diff --git a/test/resources/nodetool/help/getreplicas b/test/resources/nodetool/help/getreplicas new file mode 100644 index 0000000000..bbe5c109e1 --- /dev/null +++ b/test/resources/nodetool/help/getreplicas @@ -0,0 +1,37 @@ +NAME + nodetool getreplicas - Print the replicas that own the key + +SYNOPSIS + nodetool [(-h | --host )] [(-p | --port )] + [(-pw | --password )] + [(-pwf | --password-file )] + [(-u | --username )] getreplicas + [(-pp | --print-port)] [--]
+ +OPTIONS + -h , --host + Node hostname or ip address + + -p , --port + Remote jmx agent port number + + -pp, --print-port + Operate in 4.0 mode with hosts disambiguated by port number + + -pw , --password + Remote jmx agent password + + -pwf , --password-file + Path to the JMX password file + + -u , --username + Remote jmx agent username + + -- + This option can be used to separate command-line options from the + list of argument, (useful when arguments might be mistaken for + command-line options + +
+ The keyspace, the table, and the partition key for which we need to + find the replica (e.g., pk1:pk2:pk3 for compound keys) diff --git a/test/resources/nodetool/help/invalidatepermissionscache b/test/resources/nodetool/help/invalidatepermissionscache index 67566462d1..8d956bdca0 100644 --- a/test/resources/nodetool/help/invalidatepermissionscache +++ b/test/resources/nodetool/help/invalidatepermissionscache @@ -10,7 +10,7 @@ SYNOPSIS [--all-tables] [--function ] [--functions-in-keyspace ] [--keyspace ] [--mbean ] [--role ] - [--table
] [--] + [--table
] [--] OPTIONS --all-functions @@ -69,6 +69,6 @@ OPTIONS list of argument, (useful when arguments might be mistaken for command-line options - + A role for which permissions to specified resources need to be invalidated diff --git a/test/resources/nodetool/help/nodetool b/test/resources/nodetool/help/nodetool index a3fe2c520e..3c8172f96b 100644 --- a/test/resources/nodetool/help/nodetool +++ b/test/resources/nodetool/help/nodetool @@ -64,12 +64,13 @@ The most commonly used nodetool commands are: getconcurrentcompactors Get the number of concurrent compactors in the system. getconcurrentviewbuilders Get the number of concurrent view builders in the system getdefaultrf Gets default keyspace replication factor. - getendpoints Print the end points that owns the key + getendpoints Print the end points that owns the key, deprecated, use getreplicas instead getfullquerylog Print configuration of fql if enabled, otherwise the configuration reflected in cassandra.yaml getguardrailsconfig Print runtime configuration of guardrails. getinterdcstreamthroughput Print the throughput cap for inter-datacenter streaming and entire SSTable inter-datacenter streaming in the systemin rounded megabits. For precise number, please, use option -d getlogginglevels Get the runtime logging levels getmaxhintwindow Print the max hint window in ms + getreplicas Print the replicas that own the key getseeds Get the currently in use seed node IP list excluding the node IP getsnapshotthrottle Print the snapshot_links_per_second throttle for snapshot/clearsnapshot getsstables Print the sstable filenames that own the key diff --git a/test/unit/org/apache/cassandra/auth/AuthConfigTest.java b/test/unit/org/apache/cassandra/auth/AuthConfigTest.java index ecbc72f0d5..90c10d16cf 100644 --- a/test/unit/org/apache/cassandra/auth/AuthConfigTest.java +++ b/test/unit/org/apache/cassandra/auth/AuthConfigTest.java @@ -31,7 +31,10 @@ import org.junit.Test; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.ParameterizedClass; +import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; import org.apache.cassandra.utils.MBeanWrapper; import static org.apache.cassandra.auth.AuthCache.MBEAN_NAME_BASE; @@ -39,6 +42,7 @@ import static org.apache.cassandra.auth.AuthTestUtils.loadCertificateChain; import static org.apache.cassandra.auth.IInternodeAuthenticator.InternodeConnectionDirection.INBOUND; import static org.apache.cassandra.config.YamlConfigurationLoaderTest.load; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -160,6 +164,75 @@ public class AuthConfigTest assertTrue(DatabaseDescriptor.getRoleManager().alterableOptions().containsAll(authenticator.getAlterableRoleOptions())); } + private static final String PROBE = ClassLoadingTestNonAssignable.class.getName(); + + private static Config baseConfig() + { + // Use the default (AllowAll) auth config so the non-probe auth components do not register JMX caches that + // would conflict across the per-field probe tests below. + Config config = load("cassandra.yaml"); + DatabaseDescriptor.unsafeDaemonInitialization(() -> config); + return config; + } + + private static void assertApplyAuthRejectsProbe() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + AuthConfig.reset(); + assertThatThrownBy(AuthConfig::applyAuth) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement"); + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testAuthenticatorWrongTypeRejectedWithoutInitializing() + { + Config config = baseConfig(); + config.authenticator = new ParameterizedClass(PROBE, Collections.emptyMap()); + assertApplyAuthRejectsProbe(); + } + + @Test + public void testAuthorizerWrongTypeRejectedWithoutInitializing() + { + Config config = baseConfig(); + config.authorizer = new ParameterizedClass(PROBE, Collections.emptyMap()); + assertApplyAuthRejectsProbe(); + } + + @Test + public void testRoleManagerWrongTypeRejectedWithoutInitializing() + { + Config config = baseConfig(); + config.role_manager = new ParameterizedClass(PROBE, Collections.emptyMap()); + assertApplyAuthRejectsProbe(); + } + + @Test + public void testInternodeAuthenticatorWrongTypeRejectedWithoutInitializing() + { + Config config = baseConfig(); + config.internode_authenticator = new ParameterizedClass(PROBE, Collections.emptyMap()); + assertApplyAuthRejectsProbe(); + } + + @Test + public void testNetworkAuthorizerWrongTypeRejectedWithoutInitializing() + { + Config config = baseConfig(); + config.network_authorizer = new ParameterizedClass(PROBE, Collections.emptyMap()); + assertApplyAuthRejectsProbe(); + } + + @Test + public void testCidrAuthorizerWrongTypeRejectedWithoutInitializing() + { + Config config = baseConfig(); + config.cidr_authorizer = new ParameterizedClass(PROBE, Collections.emptyMap()); + assertApplyAuthRejectsProbe(); + } + private void unregisterCaches() { safeUnregisterMbean(IDENTITIES_CACHE_MBEAN); diff --git a/test/unit/org/apache/cassandra/config/ClassLoadingSearchProbe.java b/test/unit/org/apache/cassandra/config/ClassLoadingSearchProbe.java new file mode 100644 index 0000000000..abfdfa278a --- /dev/null +++ b/test/unit/org/apache/cassandra/config/ClassLoadingSearchProbe.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.config; + +/** + * Search-package fall-through probe. Shares its simple name ({@code ClassLoadingSearchProbe}) with + * {@link org.apache.cassandra.utils.ClassLoadingSearchProbe}, but unlike that one it IS a {@link Runnable}, so it + * represents the "correct type under a later search package" case. A wrong-type match under an earlier package must + * not abort the search before this class is found. + */ +public final class ClassLoadingSearchProbe implements Runnable +{ + public ClassLoadingSearchProbe() + { + } + + @Override + public void run() + { + } +} diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java index 1704936544..6477fc25f2 100644 --- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java +++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java @@ -123,6 +123,8 @@ public class DatabaseDescriptorRefTest "org.apache.cassandra.config.Config$CorruptedTombstoneStrategy", "org.apache.cassandra.config.Config$BatchlogEndpointStrategy", "org.apache.cassandra.config.Config$TombstonesMetricGranularity", + "org.apache.cassandra.cql3.QueryOptions$DefaultReadThresholds", + "org.apache.cassandra.cql3.QueryOptions$ReadThresholds", "org.apache.cassandra.service.consensus.UnsupportedTransactionConsistencyLevel", "org.apache.cassandra.repair.autorepair.AutoRepairConfig", "org.apache.cassandra.repair.autorepair.AutoRepairConfig$Options", diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java index 5796c4032f..6f80a3dc6c 100644 --- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java +++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java @@ -47,8 +47,11 @@ import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.PathUtils; +import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.security.EncryptionContext; import org.apache.cassandra.security.EncryptionContextGenerator; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; import static org.apache.cassandra.config.CassandraRelevantProperties.ALLOW_UNLIMITED_CONCURRENT_VALIDATIONS; import static org.apache.cassandra.config.CassandraRelevantProperties.CONFIG_LOADER; @@ -133,6 +136,25 @@ public class DatabaseDescriptorTest } } + @Test + public void testCreateEndpointSnitchWrongTypeRejectedWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + assertThatThrownBy(() -> DatabaseDescriptor.createEndpointSnitch(ClassLoadingTestNonAssignable.class.getName())) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + IEndpointSnitch.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testCreateEndpointSnitchValidClassResolves() + { + IEndpointSnitch snitch = DatabaseDescriptor.createEndpointSnitch("SimpleSnitch"); + assertThat(snitch).isNotNull(); + } + @Test public void testRpcInterface() { diff --git a/test/unit/org/apache/cassandra/config/ParameterizedClassTest.java b/test/unit/org/apache/cassandra/config/ParameterizedClassTest.java index 732812aefd..4794d3de80 100644 --- a/test/unit/org/apache/cassandra/config/ParameterizedClassTest.java +++ b/test/unit/org/apache/cassandra/config/ParameterizedClassTest.java @@ -26,7 +26,10 @@ import org.junit.Test; import org.apache.cassandra.auth.AllowAllAuthorizer; import org.apache.cassandra.auth.IAuthorizer; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -71,6 +74,51 @@ public class ParameterizedClassTest assertNotNull(instance); } + @Test + public void testTypedNewInstanceRejectsWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + ParameterizedClass parameterizedClass = new ParameterizedClass(ClassLoadingTestNonAssignable.class.getName()); + + assertThatThrownBy(() -> ParameterizedClass.newInstance(parameterizedClass, null, Runnable.class)) + .hasMessageContaining("must extend or implement " + Runnable.class.getName()) + .isInstanceOf(ConfigurationException.class); + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testTwoArgNewInstanceDoesNotInitializeAtLoadTime() + { + // The 2-arg overload performs no type check, but it must still load without running . + // ClassLoadingTestNonAssignable has no usable constructor, so instantiation fails -- but only after the + // class has been loaded; loading must not have run its static initializer. + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + ParameterizedClass parameterizedClass = new ParameterizedClass(ClassLoadingTestNonAssignable.class.getName()); + + assertThatThrownBy(() -> ParameterizedClass.newInstance(parameterizedClass, null)) + .isInstanceOf(ConfigurationException.class); + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testNewInstanceSearchPackageFallThroughDoesNotAbortOnWrongType() + { + // org.apache.cassandra.utils.ClassLoadingSearchProbe (wrong type, not a Runnable) resolves under the earlier + // search package, while org.apache.cassandra.config.ClassLoadingSearchProbe (correct type, a Runnable) + // resolves under the later one. A wrong-type match under the earlier package must not abort the search. + ClassLoadingTestSupport.assertNotInitialized(org.apache.cassandra.utils.ClassLoadingSearchProbe.class); + ParameterizedClass parameterizedClass = new ParameterizedClass("ClassLoadingSearchProbe"); + + Runnable instance = ParameterizedClass.newInstance(parameterizedClass, + List.of("org.apache.cassandra.utils", + "org.apache.cassandra.config"), + Runnable.class); + assertNotNull(instance); + assertThat(instance).isInstanceOf(org.apache.cassandra.config.ClassLoadingSearchProbe.class); + // The wrong-type class that was probed under the earlier package must not have been initialized. + assertThat(ClassLoadingTestSupport.wasInitialized(org.apache.cassandra.utils.ClassLoadingSearchProbe.class)).isFalse(); + } + @Test public void testNewInstanceWithValidConstructorsFavorsMapConstructor() { diff --git a/test/unit/org/apache/cassandra/cql3/ReservedKeywordsTest.java b/test/unit/org/apache/cassandra/cql3/ReservedKeywordsTest.java index ef7a4b7ec2..0278a1d111 100644 --- a/test/unit/org/apache/cassandra/cql3/ReservedKeywordsTest.java +++ b/test/unit/org/apache/cassandra/cql3/ReservedKeywordsTest.java @@ -68,11 +68,42 @@ public class ReservedKeywordsTest asserts.assertAll(); } + /** + * Legacy USER and IDENTITY statements must accept unreserved keywords as names, just like + * role statements do ({@code roleName} accepts {@code unreserved_keyword}). Otherwise adding + * a new keyword to the grammar breaks existing deployments with a user or identity of that name. + */ + @Test + public void testUnreservedKeywordsAsUserNameAndIdentity() + { + SoftAssertions asserts = new SoftAssertions(); + for (var f : Cql_Lexer.class.getDeclaredFields()) + { + if (!Modifier.isStatic(f.getModifiers())) continue; + if (!f.getName().startsWith("K_")) continue; + String keyword = f.getName().replaceFirst("K_", ""); + if (ReservedKeywords.isReserved(keyword)) continue; + + for (String statement : new String[]{ "DROP USER %s", "DROP IDENTITY %s" }) + { + asserts.assertThat(parses(String.format(statement, keyword))) + .describedAs(String.format(statement, keyword)) + .isTrue(); + } + } + asserts.assertAll(); + } + private static boolean isAllowed(String keyword) + { + return parses(String.format("ALTER TABLE ks.t ADD %s TEXT", keyword)); + } + + private static boolean parses(String cql) { try { - QueryProcessor.parseStatement(String.format("ALTER TABLE ks.t ADD %s TEXT", keyword)); + QueryProcessor.parseStatement(cql); return true; } catch (SyntaxException ignore) diff --git a/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDCTest.java b/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDCTest.java index 6f56089259..59a3a46e54 100644 --- a/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDCTest.java +++ b/test/unit/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDCTest.java @@ -30,6 +30,7 @@ import java.util.concurrent.TimeUnit; import com.google.common.util.concurrent.Uninterruptibles; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; @@ -42,9 +43,11 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.RowUpdateBuilder; import org.apache.cassandra.db.commitlog.CommitLogSegment.CDCState; +import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.exceptions.CDCWriteException; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileReader; +import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; public class CommitLogSegmentManagerCDCTest extends CQLTester @@ -57,7 +60,11 @@ public class CommitLogSegmentManagerCDCTest extends CQLTester { ServerTestUtils.daemonInitialization(); DatabaseDescriptor.setCDCEnabled(true); - DatabaseDescriptor.setCDCTotalSpaceInMiB(1024); + // The bulk writes are sized to commitLogSegmentSize/3, so the suite's disk footprint scales + // with the segment size; shrink it to the 1 MiB floor. Cap the default CDC space too: bulkWrite + // writes ~2x the CDC limit, so this bounds how much the non-blocking cases flush to disk. + DatabaseDescriptor.setCommitLogSegmentSize(1); + DatabaseDescriptor.setCDCTotalSpaceInMiB(100); CQLTester.setUpClass(); } @@ -71,6 +78,24 @@ public class CommitLogSegmentManagerCDCTest extends CQLTester ((CommitLogSegmentManagerCDC)CommitLog.instance.segmentManager).updateCDCTotalSize(); } + @After + public void afterTest() throws Throwable + { + // Reclaim the SSTables this test wrote so data/ doesn't grow across methods; CQLTester#afterTest + // only clears schema-tracking state. Do NOT stop/delete the commit log here: super.afterTest() + // resets the CMS, whose schema re-application runs asynchronously on the TCM log follower, and + // deleting segments underneath it makes CDC hard-linking fail ("hard link to file that does not + // exist"). beforeTest() restarts the commit log for the next test instead. + Keyspace ks = Schema.instance.getKeyspaceInstance(keyspace()); + if (ks != null) + { + for (ColumnFamilyStore cfs : ks.getColumnFamilyStores()) + cfs.truncateBlockingWithoutSnapshot(); + LifecycleTransaction.waitForDeletions(); + } + super.afterTest(); + } + @Test public void testCDCWriteFailure() throws Throwable { @@ -456,10 +481,14 @@ public class CommitLogSegmentManagerCDCTest extends CQLTester { TableMetadata ccfm = Keyspace.open(keyspace()).getColumnFamilyStore(tableName).metadata(); boolean blockWrites = DatabaseDescriptor.getCDCBlockWrites(); - // Spin to make sure we hit CDC capacity + // Write enough to overflow the configured CDC limit so we reliably hit capacity. Derive the + // count from the CDC size and mutation size. Blocking mode stops + // early on the first CDCWriteException; non-blocking mode writes the whole run, recycling + // older segments (2x the fill count keeps it exercising recycling without over-writing). + int writes = 2 * (int) (DatabaseDescriptor.getCDCTotalSpace() / mutationSize); try { - for (int i = 0; i < 1000; i++) + for (int i = 0; i < writes; i++) { new RowUpdateBuilder(ccfm, 0, i) .add("data", randomizeBuffer(mutationSize)) diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailsConfigProviderTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailsConfigProviderTest.java index 79f6d51be0..46708f4317 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailsConfigProviderTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailsConfigProviderTest.java @@ -26,11 +26,25 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.GuardrailsOptions; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; import static java.lang.String.format; public class GuardrailsConfigProviderTest extends GuardrailTester { + @Test + public void testBuildWrongTypeRejectedWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + Assertions.assertThatThrownBy(() -> GuardrailsConfigProvider.build(ClassLoadingTestNonAssignable.class.getName())) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + GuardrailsConfigProvider.class.getName()); + + Assertions.assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + @Test public void testBuildCustom() throws Throwable { diff --git a/test/unit/org/apache/cassandra/db/guardrails/ValueGeneratorTest.java b/test/unit/org/apache/cassandra/db/guardrails/ValueGeneratorTest.java index 9afb60b966..0f4d298b87 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/ValueGeneratorTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/ValueGeneratorTest.java @@ -26,9 +26,12 @@ import javax.annotation.Nonnull; import org.junit.Test; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; import static java.lang.String.format; import static org.apache.cassandra.db.guardrails.ValueGenerator.GENERATOR_CLASS_NAME_KEY; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -68,6 +71,21 @@ public class ValueGeneratorTest .message().isEqualTo(format("Unable to create instance of generator of class %s: does not contain property 'expecting_true'", BooleanGenerator.class.getName())); } + @Test + public void testGeneratorWrongTypeRejectedWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + CustomGuardrailConfig config = new CustomGuardrailConfig(); + config.put(GENERATOR_CLASS_NAME_KEY, ClassLoadingTestNonAssignable.class.getName()); + + assertThatThrownBy(() -> ValueGenerator.getGenerator("probe generator", config)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + ValueGenerator.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + public static class BooleanGenerator extends ValueGenerator { private final boolean expectingTrue; diff --git a/test/unit/org/apache/cassandra/db/guardrails/ValueValidatorTest.java b/test/unit/org/apache/cassandra/db/guardrails/ValueValidatorTest.java index 7bc717e4bc..5480223996 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/ValueValidatorTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/ValueValidatorTest.java @@ -26,10 +26,13 @@ import org.junit.Test; import org.apache.cassandra.db.guardrails.ValueValidator.ValidationViolation; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; import static java.lang.Boolean.FALSE; import static java.lang.String.format; import static org.apache.cassandra.db.guardrails.ValueValidator.VALIDATOR_CLASS_NAME_KEY; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -83,6 +86,21 @@ public class ValueValidatorTest .message().isEqualTo(format("Unable to create instance of validator of class %s: does not contain property 'expecting_true'", BooleanValidator.class.getName())); } + @Test + public void testValidatorWrongTypeRejectedWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + CustomGuardrailConfig config = new CustomGuardrailConfig(); + config.put(VALIDATOR_CLASS_NAME_KEY, ClassLoadingTestNonAssignable.class.getName()); + + assertThatThrownBy(() -> ValueValidator.getValidator("probe validator", config)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + ValueValidator.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + public static class BooleanValidator extends ValueValidator { private final boolean expectingTrue; diff --git a/test/unit/org/apache/cassandra/db/marshal/AbstractTypeTest.java b/test/unit/org/apache/cassandra/db/marshal/AbstractTypeTest.java index 6a573def7d..2178a93981 100644 --- a/test/unit/org/apache/cassandra/db/marshal/AbstractTypeTest.java +++ b/test/unit/org/apache/cassandra/db/marshal/AbstractTypeTest.java @@ -739,6 +739,12 @@ public class AbstractTypeTest }}); } + @Test + public void valueLengthIfFixedIsNotFinal() throws NoSuchMethodException + { + assertThat(Modifier.isFinal(AbstractType.class.getMethod("valueLengthIfFixed").getModifiers())).isFalse(); + } + @Test @SuppressWarnings({"rawtypes", "unchecked"}) public void serde() diff --git a/test/unit/org/apache/cassandra/db/marshal/TypeParserTest.java b/test/unit/org/apache/cassandra/db/marshal/TypeParserTest.java index e9b2e3c764..fa50c1d02a 100644 --- a/test/unit/org/apache/cassandra/db/marshal/TypeParserTest.java +++ b/test/unit/org/apache/cassandra/db/marshal/TypeParserTest.java @@ -33,7 +33,10 @@ import org.apache.cassandra.dht.OrderPreservingPartitioner; import org.apache.cassandra.dht.RandomPartitioner; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.SyntaxException; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -102,6 +105,17 @@ public class TypeParserTest catch (SyntaxException e) {} } + @Test + public void testRejectsNonAbstractTypeWithoutInitializing() throws SyntaxException + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + assertThatThrownBy(() -> TypeParser.parse(ClassLoadingTestNonAssignable.class.getName())) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + AbstractType.class.getName()); + + assertFalse(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)); + } + @Test public void testParsePartitionerOrder() throws ConfigurationException, SyntaxException { diff --git a/test/unit/org/apache/cassandra/db/rows/RowsTest.java b/test/unit/org/apache/cassandra/db/rows/RowsTest.java index 06bf701a2f..5412f82ee0 100644 --- a/test/unit/org/apache/cassandra/db/rows/RowsTest.java +++ b/test/unit/org/apache/cassandra/db/rows/RowsTest.java @@ -282,6 +282,57 @@ public class RowsTest } } + private static Row liveRow(Clustering c, long ts, ByteBuffer vVal) + { + return rowWithCell(c, ts, BufferCell.live(v, ts, vVal)); + } + + private static Row rowWithCell(Clustering c, long ts, Cell cell) + { + Row.Builder builder = createBuilder(c); + builder.addPrimaryKeyLivenessInfo(LivenessInfo.create(ts)); + builder.addCell(cell); + return builder.build(); + } + + private static Row mergeRows(Row... rows) + { + boolean hasComplex = false; + for (Row row : rows) + hasComplex |= row.hasComplex(); + Row.Merger merger = new Row.Merger(rows.length, hasComplex); + for (int i = 0; i < rows.length; i++) + merger.add(i, rows[i]); + return merger.merge(DeletionTime.LIVE); + } + + @Test + public void testMergerMinLocalDeletionTime() + { + long now = FBUtilities.nowInSeconds(); + long ts = secondToTs(now); + + // All inputs are free of deletions and expiring data, so the merged row must be too. This is the fast path in + // Row.Merger#merge that reuses Cell.MAX_DELETION_TIME instead of recomputing it by scanning the merged btree. + Row mergedLive = mergeRows(liveRow(c1, ts, BB1), liveRow(c1, ts + 1, BB2)); + Assert.assertEquals(Cell.MAX_DELETION_TIME, mergedLive.minLocalDeletionTime()); + Assert.assertFalse(mergedLive.hasDeletion(now)); + + // One input carries an expiring cell that wins reconciliation (higher timestamp): the merged row must keep + // its expiration time rather than being reported as deletion-free. + Cell expiringCell = BufferCell.expiring(v, ts + 2, 100, now, BB3); + Row mergedExpiring = mergeRows(liveRow(c1, ts, BB1), rowWithCell(c1, ts + 2, expiringCell)); + Assert.assertEquals(expiringCell.localDeletionTime(), mergedExpiring.minLocalDeletionTime()); + Assert.assertFalse(mergedExpiring.hasDeletion(now)); + Assert.assertTrue(mergedExpiring.hasDeletion(expiringCell.localDeletionTime())); + + // Same for a tombstone cell winning reconciliation: the merged row must report a deletion. + Cell tombstoneCell = BufferCell.tombstone(v, ts + 2, now); + Row mergedTombstone = mergeRows(liveRow(c1, ts, BB1), rowWithCell(c1, ts + 2, tombstoneCell)); + Assert.assertEquals(Long.MIN_VALUE, mergedTombstone.minLocalDeletionTime()); + Assert.assertTrue(mergedTombstone.hasDeletion(now)); + } + @Test public void diff() { diff --git a/test/unit/org/apache/cassandra/diag/ClassLoadingTestDiagnosticEvent.java b/test/unit/org/apache/cassandra/diag/ClassLoadingTestDiagnosticEvent.java new file mode 100644 index 0000000000..13c880f942 --- /dev/null +++ b/test/unit/org/apache/cassandra/diag/ClassLoadingTestDiagnosticEvent.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.cassandra.diag; + +import java.io.Serializable; +import java.util.Map; + +/** + * A minimal, valid {@link DiagnosticEvent} subclass used by {@code DiagnosticEventPersistenceTest} to confirm that + * the type-checked load path in {@link DiagnosticEventPersistence} resolves a correct subtype. Lives in the + * {@code org.apache.cassandra} namespace so it passes the persistence package-prefix guard. + */ +public final class ClassLoadingTestDiagnosticEvent extends DiagnosticEvent +{ + public enum TestType { TEST } + + @Override + public Enum getType() + { + return TestType.TEST; + } + + @Override + public Map toMap() + { + return Map.of(); + } +} diff --git a/test/unit/org/apache/cassandra/diag/DiagnosticEventPersistenceTest.java b/test/unit/org/apache/cassandra/diag/DiagnosticEventPersistenceTest.java new file mode 100644 index 0000000000..9315054d61 --- /dev/null +++ b/test/unit/org/apache/cassandra/diag/DiagnosticEventPersistenceTest.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.cassandra.diag; + +import java.io.InvalidClassException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.junit.Test; + +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DiagnosticEventPersistenceTest +{ + /** + * Reflectively drives the (private) bespoke type-checked load path + * {@link DiagnosticEventPersistence#getEventClass(String)}. + */ + private static Class getEventClass(String eventClazz) throws Throwable + { + Method method = DiagnosticEventPersistence.class.getDeclaredMethod("getEventClass", String.class); + method.setAccessible(true); + try + { + return (Class) method.invoke(DiagnosticEventPersistence.instance(), eventClazz); + } + catch (InvocationTargetException e) + { + throw e.getCause(); + } + } + + @Test + public void testNonDiagnosticEventRejectedWithoutInitializing() throws Throwable + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + assertThatThrownBy(() -> getEventClass(ClassLoadingTestNonAssignable.class.getName())) + .isInstanceOf(InvalidClassException.class) + .hasMessageContaining("must be of type DiagnosticEvent"); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testValidDiagnosticEventSubclassResolves() throws Throwable + { + Class resolved = getEventClass(ClassLoadingTestDiagnosticEvent.class.getName()); + assertThat(resolved).isEqualTo(ClassLoadingTestDiagnosticEvent.class); + } +} diff --git a/test/unit/org/apache/cassandra/index/SecondaryIndexManagerTest.java b/test/unit/org/apache/cassandra/index/SecondaryIndexManagerTest.java index 6eaa6e7dcc..5bda8313e7 100644 --- a/test/unit/org/apache/cassandra/index/SecondaryIndexManagerTest.java +++ b/test/unit/org/apache/cassandra/index/SecondaryIndexManagerTest.java @@ -38,13 +38,17 @@ import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.db.compaction.CompactionInfo; import org.apache.cassandra.db.lifecycle.SSTableSet; +import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.notifications.SSTableAddedNotification; import org.apache.cassandra.schema.IndexMetadata; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.KillerForTests; import org.apache.cassandra.utils.concurrent.Refs; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -58,6 +62,17 @@ public class SecondaryIndexManagerTest extends CQLTester TestingIndex.clear(); } + @Test + public void rejectsNonIndexClassWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + assertThatThrownBy(() -> SecondaryIndexManager.loadIndexClass(ClassLoadingTestNonAssignable.class.getName())) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + Index.class.getName()); + + assertFalse(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)); + } + @Test public void createSasiAfterSai() { diff --git a/test/unit/org/apache/cassandra/index/sai/cql/StorageAttachedIndexDDLTest.java b/test/unit/org/apache/cassandra/index/sai/cql/StorageAttachedIndexDDLTest.java index 69f639238a..dedc26e435 100644 --- a/test/unit/org/apache/cassandra/index/sai/cql/StorageAttachedIndexDDLTest.java +++ b/test/unit/org/apache/cassandra/index/sai/cql/StorageAttachedIndexDDLTest.java @@ -55,8 +55,10 @@ import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.FloatType; import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.VectorType; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.index.Index; import org.apache.cassandra.index.SecondaryIndexManager; @@ -1429,6 +1431,27 @@ public class StorageAttachedIndexDDLTest extends SAITester assertEquals(Arrays.asList(2L, 1L), toSize.apply(iterator.next())); } + @Test + public void multiSegmentVectorIndexPassesChecksumValidation() + { + createTable("CREATE TABLE %s (pk int, val vector, PRIMARY KEY(pk))"); + + int vectorCount = 100; + for (int pk = 0; pk < vectorCount; pk++) + execute("INSERT INTO %s (pk, val) VALUES (" + pk + ", [" + pk + ".0, " + (pk + 1) + ".0, " + (pk + 2) + ".0])"); + + flush(); + + SegmentBuilder.updateLastValidSegmentRowId(17); // 17 rows per segment -> multi-segment build + IndexIdentifier vectorIndexIdentifier = createIndexIdentifier(createIndex("CREATE CUSTOM INDEX ON %s(val) USING 'StorageAttachedIndex'")); + IndexTermType vectorIndexTermType = createIndexTermType(VectorType.getInstance(FloatType.instance, 3)); + + // A vector index writes CompressedVectors.db, TermsData.db, and PostingLists.db in append + // mode with one SAI codec footer per segment (see OnHeapGraph.writeData). Multi-segment + // builds therefore need segment-aware checksum validation. + assertTrue(verifyChecksum(vectorIndexTermType, vectorIndexIdentifier)); + } + private void assertZeroSegmentBuilderUsage() { assertEquals("Segment memory limiter should revert to zero.", 0L, getSegmentBufferUsedBytes()); diff --git a/test/unit/org/apache/cassandra/index/sai/disk/v1/SegmentFlushTest.java b/test/unit/org/apache/cassandra/index/sai/disk/v1/SegmentFlushTest.java index e2837df331..d11bd3da95 100644 --- a/test/unit/org/apache/cassandra/index/sai/disk/v1/SegmentFlushTest.java +++ b/test/unit/org/apache/cassandra/index/sai/disk/v1/SegmentFlushTest.java @@ -18,15 +18,19 @@ package org.apache.cassandra.index.sai.disk.v1; import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.List; import com.google.common.base.Stopwatch; +import org.apache.lucene.index.CorruptIndexException; import org.junit.After; import org.junit.BeforeClass; import org.junit.Test; @@ -34,11 +38,13 @@ import org.junit.Test; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.marshal.TimestampType; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.rows.BTreeRow; import org.apache.cassandra.db.rows.BufferCell; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.index.sai.IndexValidation; import org.apache.cassandra.index.sai.SAITester; import org.apache.cassandra.index.sai.StorageAttachedIndex; import org.apache.cassandra.index.sai.disk.format.IndexComponent; @@ -61,6 +67,8 @@ import org.apache.cassandra.utils.bytecomparable.ByteSource; import static org.apache.cassandra.Util.dk; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class SegmentFlushTest { @@ -87,6 +95,147 @@ public class SegmentFlushTest SegmentBuilder.updateLastValidSegmentRowId(-1); // reset } + @Test + public void multiSegmentBalancedTreePassesChecksumValidation() throws IOException + { + Path tmpDir = Files.createTempDirectory("SegmentFlushTest"); + IndexDescriptor indexDescriptor = IndexDescriptor.create(new Descriptor(new File(tmpDir.toFile()), "ks", "cf", new SequenceBasedSSTableId(1)), Murmur3Partitioner.instance, SAITester.EMPTY_COMPARATOR); + ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "ts", TimestampType.instance, 1); + StorageAttachedIndex index = SAITester.createMockIndex(column); + + SSTableIndexWriter writer = new SSTableIndexWriter(indexDescriptor, index, V1OnDiskFormat.SEGMENT_BUILD_MEMORY_LIMITER, () -> true); + + List keys = Arrays.asList(dk("1"), dk("2")); + Collections.sort(keys); + + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(0)), createRow(column, TimestampType.instance.decompose(new Date(1_000L))), 0L); + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(1)), createRow(column, TimestampType.instance.decompose(new Date(2_000L))), SegmentBuilder.LAST_VALID_SEGMENT_ROW_ID + 1); + writer.complete(Stopwatch.createStarted()); + + // Will throw if checksum validation fails: + indexDescriptor.validatePerIndexComponents(index.termType(), index.identifier(), IndexValidation.CHECKSUM, true, true); + } + + @Test + public void multiSegmentTermsDataPassesChecksumValidation() throws IOException + { + Path tmpDir = Files.createTempDirectory("SegmentFlushTest"); + IndexDescriptor indexDescriptor = IndexDescriptor.create(new Descriptor(new File(tmpDir.toFile()), "ks", "cf", new SequenceBasedSSTableId(1)), Murmur3Partitioner.instance, SAITester.EMPTY_COMPARATOR); + ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "name", UTF8Type.instance, 1); + StorageAttachedIndex index = SAITester.createMockIndex(column); + + SSTableIndexWriter writer = new SSTableIndexWriter(indexDescriptor, index, V1OnDiskFormat.SEGMENT_BUILD_MEMORY_LIMITER, () -> true); + + List keys = Arrays.asList(dk("1"), dk("2")); + Collections.sort(keys); + + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(0)), createRow(column, UTF8Type.instance.decompose("a")), 0L); + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(1)), createRow(column, UTF8Type.instance.decompose("b")), SegmentBuilder.LAST_VALID_SEGMENT_ROW_ID + 1); + writer.complete(Stopwatch.createStarted()); + + // Will throw if checksum validation fails: + indexDescriptor.validatePerIndexComponents(index.termType(), index.identifier(), IndexValidation.CHECKSUM, true, true); + } + + @Test + public void multiSegmentBalancedTreePassesHeaderFooterValidation() throws IOException + { + Path tmpDir = Files.createTempDirectory("SegmentFlushTest"); + IndexDescriptor indexDescriptor = IndexDescriptor.create(new Descriptor(new File(tmpDir.toFile()), "ks", "cf", new SequenceBasedSSTableId(1)), Murmur3Partitioner.instance, SAITester.EMPTY_COMPARATOR); + ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "ts", TimestampType.instance, 1); + StorageAttachedIndex index = SAITester.createMockIndex(column); + + SSTableIndexWriter writer = new SSTableIndexWriter(indexDescriptor, index, V1OnDiskFormat.SEGMENT_BUILD_MEMORY_LIMITER, () -> true); + + List keys = Arrays.asList(dk("1"), dk("2")); + Collections.sort(keys); + + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(0)), createRow(column, TimestampType.instance.decompose(new Date(1_000L))), 0L); + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(1)), createRow(column, TimestampType.instance.decompose(new Date(2_000L))), SegmentBuilder.LAST_VALID_SEGMENT_ROW_ID + 1); + writer.complete(Stopwatch.createStarted()); + + indexDescriptor.validatePerIndexComponents(index.termType(), index.identifier(), IndexValidation.HEADER_FOOTER, false, true); + } + + @Test + public void multiSegmentBalancedTreeFailsChecksumOnFirstSegmentByteFlip() throws IOException + { + Path tmpDir = Files.createTempDirectory("SegmentFlushTest"); + IndexDescriptor indexDescriptor = IndexDescriptor.create(new Descriptor(new File(tmpDir.toFile()), "ks", "cf", new SequenceBasedSSTableId(1)), Murmur3Partitioner.instance, SAITester.EMPTY_COMPARATOR); + ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "ts", TimestampType.instance, 1); + StorageAttachedIndex index = SAITester.createMockIndex(column); + + SSTableIndexWriter writer = new SSTableIndexWriter(indexDescriptor, index, V1OnDiskFormat.SEGMENT_BUILD_MEMORY_LIMITER, () -> true); + + List keys = Arrays.asList(dk("1"), dk("2")); + Collections.sort(keys); + + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(0)), createRow(column, TimestampType.instance.decompose(new Date(1_000L))), 0L); + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(1)), createRow(column, TimestampType.instance.decompose(new Date(2_000L))), SegmentBuilder.LAST_VALID_SEGMENT_ROW_ID + 1); + writer.complete(Stopwatch.createStarted()); + + // Locate segment 0's payload extent so the flip lands inside it. Corrupting the FIRST + // (not last) segment specifically proves the validator inspects every segment -- a + // validator that only checked the trailing footer would miss this and silently pass. + MetadataSource source = MetadataSource.loadColumnMetadata(indexDescriptor, index.identifier()); + List segments = SegmentMetadata.load(source, indexDescriptor.primaryKeyFactory); + assertEquals(2, segments.size()); + SegmentMetadata.ComponentMetadata cm = segments.get(0).componentMetadatas.get(IndexComponent.BALANCED_TREE); + long flipPosition = cm.offset + cm.length / 2; + + File balancedTree = indexDescriptor.fileFor(IndexComponent.BALANCED_TREE, index.identifier()); + try (RandomAccessFile raf = new RandomAccessFile(balancedTree.toJavaIOFile(), "rw")) + { + raf.seek(flipPosition); + int original = raf.readByte(); + raf.seek(flipPosition); + raf.writeByte(original ^ 0xFF); + } + + try + { + indexDescriptor.validatePerIndexComponents(index.termType(), index.identifier(), IndexValidation.CHECKSUM, true, true); + fail("Expected corrupted first segment to fail checksum validation"); + } + catch (UncheckedIOException expected) + { + assertTrue("Expected CorruptIndexException cause; got " + expected.getCause(), expected.getCause() instanceof CorruptIndexException); + } + } + + @Test + public void multiSegmentBalancedTreeFailsChecksumOnAppendedGarbage() throws IOException + { + Path tmpDir = Files.createTempDirectory("SegmentFlushTest"); + IndexDescriptor indexDescriptor = IndexDescriptor.create(new Descriptor(new File(tmpDir.toFile()), "ks", "cf", new SequenceBasedSSTableId(1)), Murmur3Partitioner.instance, SAITester.EMPTY_COMPARATOR); + ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "ts", TimestampType.instance, 1); + StorageAttachedIndex index = SAITester.createMockIndex(column); + + SSTableIndexWriter writer = new SSTableIndexWriter(indexDescriptor, index, V1OnDiskFormat.SEGMENT_BUILD_MEMORY_LIMITER, () -> true); + + List keys = Arrays.asList(dk("1"), dk("2")); + Collections.sort(keys); + + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(0)), createRow(column, TimestampType.instance.decompose(new Date(1_000L))), 0L); + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(1)), createRow(column, TimestampType.instance.decompose(new Date(2_000L))), SegmentBuilder.LAST_VALID_SEGMENT_ROW_ID + 1); + writer.complete(Stopwatch.createStarted()); + + // Append 100 random bytes past the last segment's footer. The per-segment slice loop + // walks each segment's declared frame, then asserts that frameStart == input.length() + // once done. This corruption trips that post-loop invariant, not a per-segment CRC. + SAITester.CorruptionType.APPENDED_DATA.corrupt(indexDescriptor.fileFor(IndexComponent.BALANCED_TREE, index.identifier())); + + try + { + indexDescriptor.validatePerIndexComponents(index.termType(), index.identifier(), IndexValidation.CHECKSUM, true, true); + fail("Expected trailing garbage to fail checksum validation"); + } + catch (UncheckedIOException expected) + { + assertTrue("Expected CorruptIndexException cause; got " + expected.getCause(), expected.getCause() instanceof CorruptIndexException); + } + } + @Test public void testFlushBetweenRowIds() throws Exception { diff --git a/test/unit/org/apache/cassandra/index/sasi/conf/IndexModeTest.java b/test/unit/org/apache/cassandra/index/sasi/conf/IndexModeTest.java index 546e4cfff9..b4a02c92ba 100644 --- a/test/unit/org/apache/cassandra/index/sasi/conf/IndexModeTest.java +++ b/test/unit/org/apache/cassandra/index/sasi/conf/IndexModeTest.java @@ -31,10 +31,16 @@ import org.apache.cassandra.db.marshal.AsciiType; import org.apache.cassandra.db.marshal.BytesType; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer; +import org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer; import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.Mode; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class IndexModeTest @@ -185,8 +191,8 @@ public class IndexModeTest { ColumnMetadata cd = ColumnMetadata.regularColumn(cfm, ByteBufferUtil.bytes("TestColumnMetadata"), BytesType.instance, ColumnMetadata.NO_UNIQUE_ID); - IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("analyzer_class", "java.lang.Object")); - Assert.assertEquals(Object.class, result.analyzerClass); + IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("analyzer_class", NonTokenizingAnalyzer.class.getName())); + Assert.assertEquals(NonTokenizingAnalyzer.class, result.analyzerClass); Assert.assertTrue(result.isAnalyzed); Assert.assertFalse(result.isLiteral); Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes); @@ -198,16 +204,55 @@ public class IndexModeTest { ColumnMetadata cd = ColumnMetadata.regularColumn(cfm, ByteBufferUtil.bytes("TestColumnMetadata"), BytesType.instance, ColumnMetadata.NO_UNIQUE_ID); - IndexMode result = IndexMode.getMode(cd, ImmutableMap.of("analyzer_class", "java.lang.Object", + IndexMode result = IndexMode.getMode(cd, ImmutableMap.of("analyzer_class", NonTokenizingAnalyzer.class.getName(), "analyzed", "false")); - Assert.assertEquals(Object.class, result.analyzerClass); + Assert.assertEquals(NonTokenizingAnalyzer.class, result.analyzerClass); Assert.assertFalse(result.isAnalyzed); Assert.assertFalse(result.isLiteral); Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes); Assert.assertEquals(Mode.PREFIX, result.mode); } + @Test + public void test_bytesType_rejectsNonAnalyzerWithoutInitializing() + { + ColumnMetadata cd = ColumnMetadata.regularColumn(cfm, ByteBufferUtil.bytes("TestColumnMetadata"), BytesType.instance, ColumnMetadata.NO_UNIQUE_ID); + + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + assertThatThrownBy(() -> IndexMode.getMode(cd, Collections.singletonMap("analyzer_class", ClassLoadingTestNonAssignable.class.getName()))) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + AbstractAnalyzer.class.getName()); + + Assert.assertFalse(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)); + } + + @Test + public void test_bytesType_missingAnalyzerFallsBack() + { + ColumnMetadata cd = ColumnMetadata.regularColumn(cfm, ByteBufferUtil.bytes("TestColumnMetadata"), BytesType.instance, ColumnMetadata.NO_UNIQUE_ID); + + IndexMode result = IndexMode.getMode(cd, Collections.singletonMap("analyzer_class", "does.not.ExistAnalyzer")); + Assert.assertNull(result.analyzerClass); + Assert.assertFalse(result.isAnalyzed); + Assert.assertFalse(result.isLiteral); + Assert.assertEquals((long)(1073741824 * 0.15), result.maxCompactionFlushMemoryInBytes); + Assert.assertEquals(Mode.PREFIX, result.mode); + } + + @Test + public void test_validateAnalyzer_rejectsNonAnalyzerWithoutInitializing() + { + ColumnMetadata cd = ColumnMetadata.regularColumn(cfm, ByteBufferUtil.bytes("TestColumnMetadata"), UTF8Type.instance, ColumnMetadata.NO_UNIQUE_ID); + + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + assertThatThrownBy(() -> IndexMode.validateAnalyzer(Collections.singletonMap("analyzer_class", ClassLoadingTestNonAssignable.class.getName()), cd)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + AbstractAnalyzer.class.getName()); + + Assert.assertFalse(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)); + } + @Test public void test_bytesType_maxCompactionFlushMemoryInBytes() { diff --git a/test/unit/org/apache/cassandra/metrics/FreeMetricIdSetTrackerTest.java b/test/unit/org/apache/cassandra/metrics/FreeMetricIdSetTrackerTest.java new file mode 100644 index 0000000000..5b7ad96f95 --- /dev/null +++ b/test/unit/org/apache/cassandra/metrics/FreeMetricIdSetTrackerTest.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.metrics; + +import java.util.HashSet; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; + +import org.junit.Test; + +import org.apache.cassandra.metrics.ThreadLocalMetrics.FreeMetricIdSetTracker; + +import static org.junit.Assert.assertEquals; + +public class FreeMetricIdSetTrackerTest +{ + // free and release ids in interleaved portions, and verify that a later portion does not + // become reusable before its own two-cycle delay has elapsed - i.e. only the ids that are + // actually due are released, never the freshly freed ones. + @Test + public void testInterleavedFreeAndReleaseReleasesOnlyExpectedIds() + { + FreeMetricIdSetTracker tracker = getTrackerToTest(); + + // portion A: free, then one release cycle (A is now in the delayed buffer, not yet reusable) + Set portionA = ImmutableSet.of(1, 2, 3); + portionA.forEach(tracker::markAsFree); + tracker.triggerRecycling(); + assertEquals(0, tracker.getFreeMetricSetCardinality()); + + // portion B: free a fresh batch, then another release cycle. + // this cycle is A's second cycle (A becomes reusable) but only B's first (B must stay held). + Set portionB = ImmutableSet.of(10, 11); + portionB.forEach(tracker::markAsFree); + tracker.triggerRecycling(); + + // only portion A is released here - portion B must not leak out yet + assertEquals(portionA.size(), tracker.getFreeMetricSetCardinality()); + assertEquals(portionA, drainFreeIds(tracker)); + + // a further release cycle finally makes portion B reusable, and nothing else + tracker.triggerRecycling(); + assertEquals(portionB.size(), tracker.getFreeMetricSetCardinality()); + assertEquals(portionB, drainFreeIds(tracker)); + + assertEquals(0, tracker.getFreeMetricSetCardinality()); + assertEquals(-1, tracker.getFreeMetricId()); + } + + @Test + public void testTriggerRecyclingIsNoOpWhenNothingFreed() + { + FreeMetricIdSetTracker tracker = getTrackerToTest(); + + // triggering with an empty tracker must not produce phantom free ids + tracker.triggerRecycling(); + tracker.triggerRecycling(); + + assertEquals(0, tracker.getFreeMetricSetCardinality()); + assertEquals(-1, tracker.getFreeMetricId()); + } + + private static FreeMetricIdSetTracker getTrackerToTest() + { + return new FreeMetricIdSetTracker() + { + @Override + protected void scheduleCleanupTask() + { + // disable scheduling to make the test deterministic + } + }; + } + + /** + * Drains every id currently available for reuse out of the tracker. + */ + private static Set drainFreeIds(FreeMetricIdSetTracker tracker) + { + Set ids = new HashSet<>(); + int id; + while ((id = tracker.getFreeMetricId()) >= 0) + ids.add(id); + return ids; + } +} diff --git a/test/unit/org/apache/cassandra/metrics/ThreadLocalCounterTest.java b/test/unit/org/apache/cassandra/metrics/ThreadLocalCounterTest.java index 2597504522..03a0cb0640 100644 --- a/test/unit/org/apache/cassandra/metrics/ThreadLocalCounterTest.java +++ b/test/unit/org/apache/cassandra/metrics/ThreadLocalCounterTest.java @@ -90,7 +90,7 @@ public class ThreadLocalCounterTest LOGGER.info("id generator state: {}, free IDs: {}", ThreadLocalMetrics.idGenerator.get(), - ThreadLocalMetrics.freeMetricIdSet); + ThreadLocalMetrics.freeMetricIdSetTracker); LOGGER.info("iteration completed: {} / {}", iteration + 1, ITERATIONS_COUNT); } } diff --git a/test/unit/org/apache/cassandra/repair/autorepair/AutoRepairConfigTest.java b/test/unit/org/apache/cassandra/repair/autorepair/AutoRepairConfigTest.java index 188e0da30f..4dbdf24efb 100644 --- a/test/unit/org/apache/cassandra/repair/autorepair/AutoRepairConfigTest.java +++ b/test/unit/org/apache/cassandra/repair/autorepair/AutoRepairConfigTest.java @@ -36,7 +36,11 @@ import org.apache.cassandra.config.ParameterizedClass; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.repair.autorepair.AutoRepairConfig.Options; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -68,6 +72,19 @@ public class AutoRepairConfigTest extends CQLTester AutoRepair.SLEEP_IF_REPAIR_FINISHES_QUICKLY = new DurationSpec.IntSecondsBound("0s"); } + @Test + public void testTokenRangeSplitterWrongTypeRejectedWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + ParameterizedClass pc = new ParameterizedClass(ClassLoadingTestNonAssignable.class.getName(), Collections.emptyMap()); + assertThatThrownBy(() -> AutoRepairConfig.newAutoRepairTokenRangeSplitter(repairType, pc)) + .isInstanceOf(ConfigurationException.class) + .hasStackTraceContaining("must extend or implement " + IAutoRepairTokenRangeSplitter.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + @Test public void autoRepairConfigDefaultsAreNotNull() { diff --git a/test/unit/org/apache/cassandra/schema/CompactionParamsTest.java b/test/unit/org/apache/cassandra/schema/CompactionParamsTest.java new file mode 100644 index 0000000000..3831c2db25 --- /dev/null +++ b/test/unit/org/apache/cassandra/schema/CompactionParamsTest.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.cassandra.schema; + +import org.junit.Test; + +import org.apache.cassandra.db.compaction.AbstractCompactionStrategy; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class CompactionParamsTest +{ + @Test + public void testRejectsNonCompactionStrategyWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + assertThatThrownBy(() -> CompactionParams.classFromName(ClassLoadingTestNonAssignable.class.getName())) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + AbstractCompactionStrategy.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } +} diff --git a/test/unit/org/apache/cassandra/schema/CompressionParamsTest.java b/test/unit/org/apache/cassandra/schema/CompressionParamsTest.java index 05b98e86aa..e4a696e663 100644 --- a/test/unit/org/apache/cassandra/schema/CompressionParamsTest.java +++ b/test/unit/org/apache/cassandra/schema/CompressionParamsTest.java @@ -30,10 +30,14 @@ import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.compress.BufferType; import org.apache.cassandra.io.compress.ICompressor; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class CompressionParamsTest { @@ -97,6 +101,19 @@ public class CompressionParamsTest assertThat(params.klass()).isEqualTo(CustomTestCompressor.class); } + @Test + public void testRejectsNonCompressorWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + assertThatThrownBy(() -> CompressionParams.fromMap(Collections.singletonMap(CompressionParams.CLASS, + ClassLoadingTestNonAssignable.class.getName()))) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + ICompressor.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + public static class CustomTestCompressor implements ICompressor { public static CustomTestCompressor create(Map options) diff --git a/test/unit/org/apache/cassandra/schema/IndexMetadataTest.java b/test/unit/org/apache/cassandra/schema/IndexMetadataTest.java index 0bcd0dfc28..ef221141d8 100644 --- a/test/unit/org/apache/cassandra/schema/IndexMetadataTest.java +++ b/test/unit/org/apache/cassandra/schema/IndexMetadataTest.java @@ -20,10 +20,21 @@ */ package org.apache.cassandra.schema; +import java.util.Collections; + import org.junit.Assert; import org.junit.Test; import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.statements.schema.IndexTarget; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class IndexMetadataTest { @@ -33,4 +44,24 @@ public class IndexMetadataTest Assert.assertEquals("aB4__idx", IndexMetadata.generateDefaultIndexName("a B-4@!_+")); Assert.assertEquals("34_Ddd_F6_idx", IndexMetadata.generateDefaultIndexName("34_()Ddd", new ColumnIdentifier("#F%6*", true))); } + + @Test + public void testRejectsNonCustomIndexWithoutInitializing() + { + TableMetadata table = TableMetadata.builder("ks", "tbl") + .addPartitionKeyColumn("pk", UTF8Type.instance) + .build(); + IndexMetadata index = IndexMetadata.fromSchemaMetadata("idx", + IndexMetadata.Kind.CUSTOM, + Collections.singletonMap(IndexTarget.CUSTOM_INDEX_OPTION_NAME, + ClassLoadingTestNonAssignable.class.getName())); + + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + assertThatThrownBy(() -> index.validate(table)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + Index.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } } diff --git a/test/unit/org/apache/cassandra/schema/MemtableFactoryInvalidFieldType.java b/test/unit/org/apache/cassandra/schema/MemtableFactoryInvalidFieldType.java new file mode 100644 index 0000000000..823706fbf2 --- /dev/null +++ b/test/unit/org/apache/cassandra/schema/MemtableFactoryInvalidFieldType.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.schema; + +import org.apache.cassandra.utils.ClassLoadingTestSupport; + +public final class MemtableFactoryInvalidFieldType +{ + public static final Object FACTORY = new Object(); + + static + { + ClassLoadingTestSupport.markInitialized(MemtableFactoryInvalidFieldType.class); + } + + private MemtableFactoryInvalidFieldType() + { + } +} diff --git a/test/unit/org/apache/cassandra/schema/MemtableFactoryInvalidReturnType.java b/test/unit/org/apache/cassandra/schema/MemtableFactoryInvalidReturnType.java new file mode 100644 index 0000000000..6cb4989cba --- /dev/null +++ b/test/unit/org/apache/cassandra/schema/MemtableFactoryInvalidReturnType.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.cassandra.schema; + +import java.util.Map; + +import org.apache.cassandra.utils.ClassLoadingTestSupport; + +public final class MemtableFactoryInvalidReturnType +{ + static + { + ClassLoadingTestSupport.markInitialized(MemtableFactoryInvalidReturnType.class); + } + + private MemtableFactoryInvalidReturnType() + { + } + + public static Object factory(Map parameters) + { + return new Object(); + } +} diff --git a/test/unit/org/apache/cassandra/schema/MemtableParamsTest.java b/test/unit/org/apache/cassandra/schema/MemtableParamsTest.java index 75094ef5d1..0e21b050d9 100644 --- a/test/unit/org/apache/cassandra/schema/MemtableParamsTest.java +++ b/test/unit/org/apache/cassandra/schema/MemtableParamsTest.java @@ -18,6 +18,8 @@ package org.apache.cassandra.schema; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.LinkedHashMap; import java.util.Map; @@ -32,11 +34,13 @@ import org.apache.cassandra.config.InheritingClass; import org.apache.cassandra.config.ParameterizedClass; import org.apache.cassandra.db.memtable.SkipListMemtableFactory; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.utils.ClassLoadingTestSupport; import org.apache.cassandra.utils.ConfigGenBuilder; import static accord.utils.Property.qt; import static org.apache.cassandra.config.YamlConfigurationLoader.fromMap; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -52,6 +56,46 @@ public class MemtableParamsTest assertEquals(ImmutableMap.of("default", DEFAULT), map); } + @Test + public void testInvalidFactoryMethodDoesNotInitializeClass() throws Exception + { + ClassLoadingTestSupport.assertNotInitialized(MemtableFactoryInvalidReturnType.class); + + assertThatThrownBy(() -> getMemtableFactory(MemtableFactoryInvalidReturnType.class)) + .isInstanceOf(ConfigurationException.class) + .hasStackTraceContaining("must return"); + + assertThat(ClassLoadingTestSupport.wasInitialized(MemtableFactoryInvalidReturnType.class)).isFalse(); + } + + @Test + public void testInvalidFactoryFieldDoesNotInitializeClass() throws Exception + { + ClassLoadingTestSupport.assertNotInitialized(MemtableFactoryInvalidFieldType.class); + + assertThatThrownBy(() -> getMemtableFactory(MemtableFactoryInvalidFieldType.class)) + .isInstanceOf(ConfigurationException.class) + .hasStackTraceContaining("must be of type"); + + assertThat(ClassLoadingTestSupport.wasInitialized(MemtableFactoryInvalidFieldType.class)).isFalse(); + } + + private static void getMemtableFactory(Class memtableClass) throws Exception + { + Method method = MemtableParams.class.getDeclaredMethod("getMemtableFactory", ParameterizedClass.class); + method.setAccessible(true); + try + { + method.invoke(null, new ParameterizedClass(memtableClass.getName())); + } + catch (InvocationTargetException e) + { + if (e.getCause() instanceof ConfigurationException) + throw (ConfigurationException) e.getCause(); + throw e; + } + } + @Test public void testDefaultRemapped() { diff --git a/test/unit/org/apache/cassandra/schema/ReplicationParamsTest.java b/test/unit/org/apache/cassandra/schema/ReplicationParamsTest.java new file mode 100644 index 0000000000..119aa46975 --- /dev/null +++ b/test/unit/org/apache/cassandra/schema/ReplicationParamsTest.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cassandra.schema; + +import java.io.IOException; +import java.util.Collections; + +import org.junit.Test; + +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.tcm.serialization.Version; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ReplicationParamsTest +{ + @Test + public void testRejectsNonReplicationStrategyWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + assertThatThrownBy(() -> ReplicationParams.fromMap(Collections.singletonMap(ReplicationParams.CLASS, + ClassLoadingTestNonAssignable.class.getName()))) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + AbstractReplicationStrategy.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testSerializerRejectsNonReplicationStrategyWithoutInitializing() throws IOException + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + assertThatThrownBy(() -> ReplicationParams.serializer.deserialize(replicationParamsInput(), Version.V8)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + AbstractReplicationStrategy.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testMessageSerializerRejectsNonReplicationStrategyWithoutInitializing() throws IOException + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + assertThatThrownBy(() -> ReplicationParams.messageSerializer.deserialize(replicationParamsInput(), + MessagingService.current_version)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + AbstractReplicationStrategy.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + private static DataInputBuffer replicationParamsInput() throws IOException + { + try (DataOutputBuffer out = new DataOutputBuffer()) + { + out.writeUTF(ClassLoadingTestNonAssignable.class.getName()); + out.writeUnsignedVInt32(0); + return new DataInputBuffer(out.toByteArray()); + } + } +} diff --git a/test/unit/org/apache/cassandra/security/CipherFactoryTest.java b/test/unit/org/apache/cassandra/security/CipherFactoryTest.java index cb12d58558..4115af0575 100644 --- a/test/unit/org/apache/cassandra/security/CipherFactoryTest.java +++ b/test/unit/org/apache/cassandra/security/CipherFactoryTest.java @@ -35,7 +35,12 @@ import org.junit.Before; import org.junit.Test; import org.apache.cassandra.config.TransparentDataEncryptionOptions; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.utils.ClassLoadingTestNonAssignable; +import org.apache.cassandra.utils.ClassLoadingTestSupport; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; @@ -121,6 +126,23 @@ public class CipherFactoryTest Assert.assertFalse(c1 == c2); } + @Test + public void keyProviderWrongTypeRejectedWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + + TransparentDataEncryptionOptions options = EncryptionContextGenerator.createEncryptionOptions(); + options.key_provider.class_name = ClassLoadingTestNonAssignable.class.getName(); + + // CipherFactory wraps the load failure; the cause is the type-check ConfigurationException + assertThatThrownBy(() -> new CipherFactory(options)) + .isInstanceOf(RuntimeException.class) + .hasCauseInstanceOf(ConfigurationException.class) + .hasStackTraceContaining("must extend or implement " + KeyProvider.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + @Test(expected = AssertionError.class) public void getDecryptor_NullIv() throws IOException { diff --git a/test/unit/org/apache/cassandra/security/CryptoProviderTest.java b/test/unit/org/apache/cassandra/security/CryptoProviderTest.java index 2d9d8ae003..edf90acca1 100644 --- a/test/unit/org/apache/cassandra/security/CryptoProviderTest.java +++ b/test/unit/org/apache/cassandra/security/CryptoProviderTest.java @@ -167,7 +167,7 @@ public class CryptoProviderTest DatabaseDescriptor.getRawConfig().crypto_provider = new ParameterizedClass(DefaultCryptoProvider.class.getName(), of("k1", "v1", "k2", "v2")); - fbUtilitiesMock.when(() -> FBUtilities.classForName(DefaultCryptoProvider.class.getName(), "crypto provider class")) + fbUtilitiesMock.when(() -> FBUtilities.classForNameWithoutInitialization(DefaultCryptoProvider.class.getName(), "crypto provider class", AbstractCryptoProvider.class)) .thenThrow(new RuntimeException("exception from test")); fbUtilitiesMock.when(() -> FBUtilities.newCryptoProvider(anyString(), anyMap())).thenCallRealMethod(); diff --git a/test/unit/org/apache/cassandra/tools/nodetool/NodetoolClassHierarchyTest.java b/test/unit/org/apache/cassandra/tools/nodetool/NodetoolClassHierarchyTest.java index 4734a45c70..66e764aecf 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/NodetoolClassHierarchyTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/NodetoolClassHierarchyTest.java @@ -20,8 +20,11 @@ package org.apache.cassandra.tools.nodetool; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import java.util.function.Consumer; @@ -143,6 +146,82 @@ public class NodetoolClassHierarchyTest extends CQLTester failedCommands.isEmpty()); } + /** + * When command arguments are addressed by name rather than by CLI position, all arguments + * of a command share a single flat, case-insensitive key namespace: options are keyed by + * their normalized {@code paramLabel()} and names, positional parameters by their paramLabel. + * No two arguments of the same command may claim the same normalized key, otherwise one value + * is silently overwritten on write or misrouted on read (e.g. a command field and a + * {@code @Mixin} field that produce the same paramLabel). + */ + @Test + public void testCommandArgumentKeysAreUnambiguous() + { + CommandLine root = new CommandLine(NodetoolCommand.class); + Map> affected = new TreeMap<>(); + + commandTreeWalker(root, cmd -> { + List violations = collectAmbiguousArgumentKeys(cmd); + if (!violations.isEmpty()) + affected.put(fullCommandName(cmd), violations); + }); + + assertTrue("The following commands have ambiguous argument keys (options are keyed by " + + "normalized paramLabel and names, positional parameters by paramLabel, all in " + + "one case-insensitive namespace). Rename the field or set an explicit, unique " + + "paramLabel:\n" + + buildAffectedCommandMessage(affected), + affected.isEmpty()); + } + + private static List collectAmbiguousArgumentKeys(CommandLine cmd) + { + List violations = new ArrayList<>(); + Map keyOwners = new LinkedHashMap<>(); + + for (CommandLine.Model.OptionSpec option : cmd.getCommandSpec().options()) + { + if (option.usageHelp() || option.versionHelp()) + continue; + + String owner = "option " + String.join("/", option.names()); + // An option is addressable by its paramLabel and by every name/alias, so all of them + // must stay unambiguous, a set tolerates a paramLabel equal to one of its own names. + Set keys = new LinkedHashSet<>(); + keys.add(normalizeOptionName(option.paramLabel()).toLowerCase()); + for (String name : option.names()) + keys.add(normalizeOptionName(name).toLowerCase()); + + for (String key : keys) + claimArgumentKey(key, owner, keyOwners, violations); + } + + for (CommandLine.Model.PositionalParamSpec param : cmd.getCommandSpec().positionalParameters()) + { + String owner = String.format("parameter index=%s (%s)", param.index(), param.paramLabel()); + claimArgumentKey(normalizeOptionName(param.paramLabel()).toLowerCase(), owner, keyOwners, violations); + } + + return violations; + } + + private static void claimArgumentKey(String key, String owner, Map keyOwners, List violations) + { + String previousOwner = keyOwners.putIfAbsent(key, owner); + if (previousOwner != null) + violations.add(String.format("key '%s' is claimed by both %s and %s", key, previousOwner, owner)); + } + + /** Strips the leading dashes from an option name. */ + private static String normalizeOptionName(String name) + { + if (name.startsWith("--")) + return name.substring(2); + else if (name.startsWith("-")) + return name.substring(1); + return name; + } + /** * For a given command, follows the {@code @ParentCommand} chain and collects all * options and parameters declared on each parent. diff --git a/test/unit/org/apache/cassandra/transport/CBUtilReadValueNativeProtocolTest.java b/test/unit/org/apache/cassandra/transport/CBUtilReadValueNativeProtocolTest.java new file mode 100644 index 0000000000..d37facf884 --- /dev/null +++ b/test/unit/org/apache/cassandra/transport/CBUtilReadValueNativeProtocolTest.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.transport; + +import java.util.Objects; + +import org.assertj.core.api.Assertions; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.transport.messages.ErrorMessage; +import org.apache.cassandra.transport.messages.OptionsMessage; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * If a client sends an AUTH_RESPONSE whose declared SASL-token length is {@link Integer#MAX_VALUE}, + * {@link CBUtil#readValue(ByteBuf)} must reject it against the readable byte count rather than + * attempting the allocation. + *

+ * This reproduces a pre-authentication unbounded-allocation DoS: on unpatched code the read + * allocates {@code new byte[Integer.MAX_VALUE]} and the JVM throws OutOfMemoryError; on fixed + * code the client receives an ERROR containing "Cannot read value of length 2147483647" and the + * server keeps running. The V4 (pre-V5) decoder path is the one the original report exercised. + */ +public class CBUtilReadValueNativeProtocolTest extends NativeProtocolLimitsTestBase +{ + public CBUtilReadValueNativeProtocolTest() + { + // V4 / pre-V5 path is the one the report and the reproducer netcat PoC exercise. + super(ProtocolVersion.V4); + } + + @BeforeClass + public static void setUp() + { + requireNetwork(); + } + + @Test + public void authResponseWithUnboundedTokenLengthIsRejected() + { + ByteBuf maliciousBody = Unpooled.buffer(4); + maliciousBody.writeInt(Integer.MAX_VALUE); // 0x7f ff ff ff + + try (SimpleClient client = client()) + { + Message.Response response = client.execute(new CustomBodyMessage(Message.Type.AUTH_RESPONSE, + maliciousBody), + false); + + Assertions.assertThat(response.type) + .as("Server must respond with an ERROR for the malformed AUTH_RESPONSE") + .isEqualTo(Message.Type.ERROR); + Assertions.assertThat(((ErrorMessage) response).error.getMessage()) + .as("ERROR payload must reference the CBUtil bound check, not OutOfMemoryError") + .contains("Cannot read value of length 2147483647"); + + // Sanity-check the server is still alive after the malicious frame: a fresh + // OPTIONS roundtrip on a new connection should succeed. + try (SimpleClient probe = client()) + { + Message.Response options = probe.execute(new OptionsMessage(), true); + Assertions.assertThat(options.type).isEqualTo(Message.Type.SUPPORTED); + } + } + } + + /** + * Replaces AUTH_RESPONSE's encoder with one that writes a caller-supplied raw body, so + * SimpleClient transmits exactly the bytes we want for the test. The server still uses + * the original AuthResponse.Codec.decode on receive, which is what we want — that's the + * code path under test. + */ + private static final class CustomBodyMessage extends Message.Request + { + private final ByteBuf body; + + CustomBodyMessage(Message.Type type, ByteBuf body) + { + super(type); + this.body = Objects.requireNonNull(body); + } + + @Override + public Envelope encode(ProtocolVersion version, int streamId) + { + Message.Codec originalCodec = type.codec; + try + { + setCodec(type, new Message.Codec<>() + { + @Override + public Message decode(ByteBuf in, ProtocolVersion v) + { + return originalCodec.decode(in, v); + } + + @Override + public void encode(Message message, ByteBuf dest, ProtocolVersion v) + { + dest.writeBytes(body, body.readerIndex(), body.readableBytes()); + } + + @Override + public int encodedSize(Message message, ProtocolVersion v) + { + return body.readableBytes(); + } + }); + return super.encode(version, streamId); + } + finally + { + setCodec(type, originalCodec); + } + } + + @Override + protected Message.Response execute(QueryState queryState, + Dispatcher.RequestTime requestTime, + boolean traceRequest) + { + throw new AssertionError("execute not expected for a malformed AUTH_RESPONSE"); + } + } + + private static void setCodec(Message.Type type, Message.Codec codec) + { + try + { + type.unsafeSetCodec(codec); + } + catch (NoSuchFieldException | IllegalAccessException e) + { + throw new AssertionError(e); + } + } +} diff --git a/test/unit/org/apache/cassandra/transport/CQLConnectionTest.java b/test/unit/org/apache/cassandra/transport/CQLConnectionTest.java index 6780e4206e..1676850e06 100644 --- a/test/unit/org/apache/cassandra/transport/CQLConnectionTest.java +++ b/test/unit/org/apache/cassandra/transport/CQLConnectionTest.java @@ -20,7 +20,9 @@ package org.apache.cassandra.transport; import java.io.IOException; import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.ServerSocket; +import java.net.SocketAddress; import java.nio.ByteBuffer; import java.security.SecureRandom; import java.util.HashMap; @@ -81,18 +83,24 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.MessageToMessageEncoder; import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED; import static org.apache.cassandra.io.util.FileUtils.ONE_MIB; import static org.apache.cassandra.net.FramingTest.randomishBytes; import static org.apache.cassandra.transport.Flusher.MAX_FRAMED_PAYLOAD_SIZE; +import static org.apache.cassandra.transport.PreV5Handlers.getConnectionVersion; import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeCondition; import static org.apache.cassandra.utils.concurrent.NonBlockingRateLimiter.NO_OP_LIMITER; import static org.assertj.core.api.Assertions.assertThat; @@ -176,6 +184,45 @@ public class CQLConnectionTest observer.verifier().accept(0); } + @Test + public void handleIncompleteHeaderErrorDuringNegotiation() throws Throwable + { + // CASSANDRA-21508: a partial header (fewer than Header.LENGTH=9 bytes) carrying an unsupported + // protocol version gives the server no stream id to route an error back to. Rather than emit an + // unroutable error frame (which could be misapplied to another request), the server closes the + // connection. Here the client observes a closed connection with no error frame. + int messageCount = 0; + Codec codec = Codec.crc(alloc); + AllocationObserver observer = new AllocationObserver(); + InboundProxyHandler.Controller controller = new InboundProxyHandler.Controller(); + // Truncate the client's STARTUP to a partial header (< 9 bytes) and set an unsupported version. + controller.withPayloadTransform(msg -> { + ByteBuf bb = (ByteBuf) msg; + ByteBuf truncated = bb.copy(0, 4); + truncated.setByte(0, 99 & Envelope.PROTOCOL_VERSION_MASK); + bb.release(); + return truncated; + }); + + ServerConfigurator configurator = ServerConfigurator.builder() + .withAllocationObserver(observer) + .withProxyController(controller) + .build(); + Server server = server(configurator); + Client client = new Client(codec, messageCount); + server.start(); + client.connect(address, port); + + // The server cannot recover a stream id from the incomplete header, so it closes the connection + // without sending an error frame. + assertFalse(client.isConnected()); + assertThat(client.getConnectionError()).isNull(); + server.stop(); + + // the failure happens before any capacity is allocated + observer.verifier().accept(0); + } + @Test public void handleFrameCorruptionAfterNegotiation() throws Throwable { @@ -247,6 +294,43 @@ public class CQLConnectionTest testFrameCorruption(1, Codec.crc(alloc), envelopeProvider, corruptor, totalBytesPerEnvelope, errorCheck); } + @Test + public void fatalErrorFrameIsFlushedBeforeChannelClose() + { + // CASSANDRA-21508: the post-V5 exception handler must close the connection only AFTER the diagnostic + // error frame has been flushed. If it closes synchronously right after writeAndFlush, a write that + // can't drain immediately (TCP backpressure, TLS, large frame) is aborted and the client never sees + // the error. Simulate "write cannot complete synchronously" with an outbound handler that captures + // the write promise without completing it - the handler must not have closed the channel yet. + StallingOutboundHandler stall = new StallingOutboundHandler(); + EmbeddedChannel channel = new EmbeddedChannel() + { + // client_error_reporting_exclusions (SubnetGroups.contains) requires a real InetSocketAddress; + // EmbeddedChannel's default remote address is not one, so supply a loopback address. + @Override + protected SocketAddress remoteAddress0() + { + return new InetSocketAddress("127.0.0.1", 9042); + } + }; + channel.pipeline().addLast(stall); + channel.pipeline().addLast(ExceptionHandlers.postV5Handler(FrameEncoderCrc.instance.allocator(), + ProtocolVersion.V5)); + + // Fatal protocol error, no recoverable stream id -> handler encodes an error frame and fatally closes. + channel.pipeline().fireExceptionCaught(ProtocolException.toFatalException(new ProtocolException("boom"))); + + // The error frame was handed to the outbound pipeline... + assertNotNull("expected the handler to write an error frame", stall.writePromise); + // ...but since that write has not completed, the channel must still be open. The buggy synchronous + // ctx.close() would have already closed it here. + assertTrue("channel must not close before the error frame is flushed", channel.isOpen()); + + // Once the write completes, the deferred close fires. + stall.writePromise.setSuccess(); + assertFalse("channel should close after the error frame is flushed", channel.isOpen()); + } + @Test public void testAquireAndRelease() { @@ -322,6 +406,48 @@ public class CQLConnectionTest runTest(configurator, codec, messageCount, envelopeProvider, responseMatcher, observer.verifier()); } + @Test + public void testRecoverableBetaFlagEnvelopeErrors() + { + // CASSANDRA-21508: a V6 (beta) envelope header with the USE_BETA flag unset is an + // invalid but *recoverable* protocol error, exactly like an unknown opcode. The server should + // return an ERROR on the offending stream id and continue processing subsequent envelopes on the + // same connection. + + // every other message advertises V6 with USE_BETA unset and should error while extracting the header + IntPredicate shouldError = i -> i % 2 == 0; + testBetaFlagEnvelopeErrors(10, shouldError, Codec.crc(alloc)); + testBetaFlagEnvelopeErrors(10, shouldError, Codec.lz4(alloc)); + + testBetaFlagEnvelopeErrors(100, shouldError, Codec.crc(alloc)); + } + + private void testBetaFlagEnvelopeErrors(int messageCount, IntPredicate shouldError, Codec codec) + { + TestConsumer consumer = new TestConsumer(new ResultMessage.Void(), codec.encoder); + AllocationObserver observer = new AllocationObserver(false); + Message.Decoder decoder = new FixedDecoder(); + + // Mutate the erroring streams' headers to advertise the V6 (beta) version with the USE_BETA flag + // cleared. In the envelope header, byte 0 is the (direction & version) byte and byte 1 is the flags. + int betaBit = 1 << Envelope.Header.Flag.USE_BETA.ordinal(); + IntFunction envelopeProvider = mutatedEnvelopeProvider(shouldError, b -> { + b.put(0, (byte) ProtocolVersion.V6.asInt()); // REQUEST direction, V6 (beta) + b.put(1, (byte) (b.get(1) & ~betaBit)); // clear USE_BETA + }); + + Predicate responseMatcher = + h -> (shouldError.test(h.streamId) && h.type == Message.Type.ERROR) || h.type == Message.Type.RESULT; + + ServerConfigurator configurator = ServerConfigurator.builder() + .withConsumer(consumer) + .withAllocationObserver(observer) + .withDecoder(decoder) + .build(); + + runTest(configurator, codec, messageCount, envelopeProvider, responseMatcher, observer.verifier()); + } + @Test public void testUnrecoverableEnvelopeDecodingErrors() { @@ -580,6 +706,19 @@ public class CQLConnectionTest buf.setByte(index, buf.getByte(index) ^ (1 << 4)); } + private static class StallingOutboundHandler extends ChannelOutboundHandlerAdapter + { + volatile ChannelPromise writePromise; + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) + { + // Capture the write but neither complete nor forward it: simulates a socket that cannot drain + // synchronously. The handler's own finally releases the payload, so we don't touch msg. + writePromise = promise; + } + } + private static class MutableEnvelope extends Envelope { public MutableEnvelope(Envelope source) @@ -639,7 +778,6 @@ public class CQLConnectionTest Message.Request request = new OptionsMessage(); request.setSource(source); - request.setStreamId(source.header.streamId); Connection connection = channel.attr(Connection.attributeKey).get(); request.attach(connection); @@ -658,17 +796,17 @@ public class CQLConnectionTest TestConsumer(Message.Response fixedResponse, FrameEncoder frameEncoder) { this.fixedResponse = fixedResponse; - this.responseTemplate = fixedResponse.encode(ProtocolVersion.V5); + this.responseTemplate = fixedResponse.encode(ProtocolVersion.V5, 0); this.frameEncoder = frameEncoder; } - public void dispatch(Channel channel, Message.Request message, Dispatcher.FlushItemConverter toFlushItem, Overload backpressure) + public

void dispatch(Channel channel, Message.Request message, Dispatcher.FlushItemConverter

toFlushItem, P param, Overload backpressure) { if (flusher == null) flusher = new SimpleClient.SimpleFlusher(frameEncoder); Envelope response = Envelope.create(responseTemplate.header.type, - message.getStreamId(), + message.getSource().header.streamId, ProtocolVersion.V5, responseTemplate.header.flags, responseTemplate.body.copy()); @@ -677,7 +815,7 @@ public class CQLConnectionTest // and flush them to the outbound pipeline flusher.schedule(channel.pipeline().lastContext()); // this simulates the release of the allocated resources that a real flusher would do - Flusher.FlushItem.Framed item = (Flusher.FlushItem.Framed)toFlushItem.toFlushItem(channel, message, fixedResponse); + Flusher.FlushItem.Framed item = (Flusher.FlushItem.Framed)toFlushItem.toFlushItem(param, channel, message, fixedResponse); item.release(); } @@ -984,6 +1122,21 @@ public class CQLConnectionTest } } + /** + * Simple adaptor to plug CQL message encoding into pre-V5 pipelines + */ + @ChannelHandler.Sharable + public static class ProtocolEncoder extends MessageToMessageEncoder + { + public static final ProtocolEncoder instance = new ProtocolEncoder(); + private ProtocolEncoder(){} + public void encode(ChannelHandlerContext ctx, Message source, List results) + { + ProtocolVersion version = getConnectionVersion(ctx); + results.add(source.encode(version, 0)); + } + } + static class Client { private final Codec codec; @@ -1022,7 +1175,7 @@ public class CQLConnectionTest ChannelPipeline pipeline = channel.pipeline(); // Outbound handlers to enable us to send the initial STARTUP pipeline.addLast("envelopeEncoder", Envelope.Encoder.instance); - pipeline.addLast("messageEncoder", PreV5Handlers.ProtocolEncoder.instance); + pipeline.addLast("messageEncoder", ProtocolEncoder.instance); pipeline.addLast("envelopeDecoder", new Envelope.Decoder()); // Inbound handler to perform the handshake & modify the pipeline on receipt of a READY pipeline.addLast("handshake", new MessageToMessageDecoder() @@ -1100,6 +1253,17 @@ public class CQLConnectionTest flusher.schedule(channel.pipeline().lastContext()); ready.countDown(); } + + @Override + public void channelInactive(ChannelHandlerContext ctx) + { + // If the server closes the connection during negotiation (e.g. an unroutable + // protocol error that cannot be answered with a stream id), unblock connect() + // so the test observes the disconnection rather than hanging for a READY/ERROR. + connected = false; + ready.countDown(); + ctx.fireChannelInactive(); + } }); } }); diff --git a/test/unit/org/apache/cassandra/transport/ErrorMessageTest.java b/test/unit/org/apache/cassandra/transport/ErrorMessageTest.java index c0e3f5ed91..7c708e7f8e 100644 --- a/test/unit/org/apache/cassandra/transport/ErrorMessageTest.java +++ b/test/unit/org/apache/cassandra/transport/ErrorMessageTest.java @@ -67,7 +67,7 @@ public class ErrorMessageTest extends EncodeAndDecodeTestBase boolean dataPresent = false; ReadFailureException rfe = new ReadFailureException(consistencyLevel, receivedBlockFor, receivedBlockFor, dataPresent, failureReasonMap1); - ErrorMessage deserialized = encodeThenDecode(ErrorMessage.fromException(rfe), ProtocolVersion.V5); + ErrorMessage deserialized = encodeThenDecode(ErrorMessage.fromTransportException(rfe), ProtocolVersion.V5); ReadFailureException deserializedRfe = (ReadFailureException) deserialized.error; assertEquals(failureReasonMap1, deserializedRfe.failureReasonByEndpoint); @@ -85,7 +85,7 @@ public class ErrorMessageTest extends EncodeAndDecodeTestBase WriteType writeType = WriteType.SIMPLE; WriteFailureException wfe = new WriteFailureException(consistencyLevel, receivedBlockFor, receivedBlockFor, writeType, failureReasonMap2); - ErrorMessage deserialized = encodeThenDecode(ErrorMessage.fromException(wfe), ProtocolVersion.V5); + ErrorMessage deserialized = encodeThenDecode(ErrorMessage.fromTransportException(wfe), ProtocolVersion.V5); WriteFailureException deserializedWfe = (WriteFailureException) deserialized.error; assertEquals(failureReasonMap2, deserializedWfe.failureReasonByEndpoint); @@ -103,7 +103,7 @@ public class ErrorMessageTest extends EncodeAndDecodeTestBase ConsistencyLevel consistencyLevel = ConsistencyLevel.SERIAL; CasWriteTimeoutException ex = new CasWriteTimeoutException(WriteType.CAS, consistencyLevel, 0, receivedBlockFor, contentions); - ErrorMessage deserialized = encodeThenDecode(ErrorMessage.fromException(ex), ProtocolVersion.V5); + ErrorMessage deserialized = encodeThenDecode(ErrorMessage.fromTransportException(ex), ProtocolVersion.V5); assertTrue(deserialized.error instanceof CasWriteTimeoutException); CasWriteTimeoutException deserializedEx = (CasWriteTimeoutException) deserialized.error; @@ -124,7 +124,7 @@ public class ErrorMessageTest extends EncodeAndDecodeTestBase ConsistencyLevel consistencyLevel = ConsistencyLevel.SERIAL; CasWriteTimeoutException ex = new CasWriteTimeoutException(WriteType.CAS, consistencyLevel, receivedBlockFor, receivedBlockFor, contentions); - ErrorMessage deserialized = encodeThenDecode(ErrorMessage.fromException(ex), ProtocolVersion.V4); + ErrorMessage deserialized = encodeThenDecode(ErrorMessage.fromTransportException(ex), ProtocolVersion.V4); assertTrue(deserialized.error instanceof WriteTimeoutException); assertFalse(deserialized.error instanceof CasWriteTimeoutException); WriteTimeoutException deserializedEx = (WriteTimeoutException) deserialized.error; @@ -142,7 +142,7 @@ public class ErrorMessageTest extends EncodeAndDecodeTestBase ConsistencyLevel consistencyLevel = ConsistencyLevel.SERIAL; CasWriteUnknownResultException ex = new CasWriteUnknownResultException(consistencyLevel, receivedBlockFor, receivedBlockFor); - ErrorMessage deserialized = encodeThenDecode(ErrorMessage.fromException(ex), ProtocolVersion.V5); + ErrorMessage deserialized = encodeThenDecode(ErrorMessage.fromTransportException(ex), ProtocolVersion.V5); assertTrue(deserialized.error instanceof CasWriteUnknownResultException); CasWriteUnknownResultException deserializedEx = (CasWriteUnknownResultException) deserialized.error; @@ -160,7 +160,7 @@ public class ErrorMessageTest extends EncodeAndDecodeTestBase ConsistencyLevel consistencyLevel = ConsistencyLevel.SERIAL; CasWriteUnknownResultException ex = new CasWriteUnknownResultException(consistencyLevel, receivedBlockFor, receivedBlockFor); - ErrorMessage deserialized = encodeThenDecode(ErrorMessage.fromException(ex), ProtocolVersion.V4); + ErrorMessage deserialized = encodeThenDecode(ErrorMessage.fromTransportException(ex), ProtocolVersion.V4); assertTrue(deserialized.error instanceof WriteTimeoutException); assertFalse(deserialized.error instanceof CasWriteUnknownResultException); WriteTimeoutException deserializedEx = (WriteTimeoutException) deserialized.error; diff --git a/test/unit/org/apache/cassandra/transport/MessageDispatcherTest.java b/test/unit/org/apache/cassandra/transport/MessageDispatcherTest.java index 601a1b1cdb..5f73c5d544 100644 --- a/test/unit/org/apache/cassandra/transport/MessageDispatcherTest.java +++ b/test/unit/org/apache/cassandra/transport/MessageDispatcherTest.java @@ -157,7 +157,7 @@ public class MessageDispatcherTest public long tryAuth(Callable check, Message.Request request) throws Exception { long start = check.call(); - dispatch.dispatch(null, request, (channel,req,response) -> null, ClientResourceLimits.Overload.NONE); + dispatch.dispatch(null, request, (p, channel, req, response) -> null, null, ClientResourceLimits.Overload.NONE); // While this is timeout based, we should be *well below* a full second on any of this processing in any sane environment. long timeout = System.currentTimeMillis(); @@ -176,9 +176,10 @@ public class MessageDispatcherTest } @Override - void processRequest(Channel channel, +

void processRequest(Channel channel, Message.Request request, - FlushItemConverter forFlusher, + FlushItemConverter

forFlusher, + P param, ClientResourceLimits.Overload backpressure, RequestTime requestTime) { diff --git a/test/unit/org/apache/cassandra/transport/ProtocolErrorTest.java b/test/unit/org/apache/cassandra/transport/ProtocolErrorTest.java index a0c6694b6e..616536109e 100644 --- a/test/unit/org/apache/cassandra/transport/ProtocolErrorTest.java +++ b/test/unit/org/apache/cassandra/transport/ProtocolErrorTest.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.transport; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -73,7 +74,12 @@ public class ProtocolErrorTest { try { dec.decode(null, buf, results); Assert.fail("Expected protocol error"); - } catch (ProtocolException e) { + } catch (ErrorMessage.WrappedException e) { + // The stream id is recovered using the attempted version's header layout (CASSANDRA-21508): + // v1/v2 use a single-byte stream id (header byte 2, which is 0x00 here), whereas v3+ use a + // two-byte id (0x0001). See decodeRecoversSingleByteStreamIdForOldProtocolVersions. + int expectedStreamId = version < ProtocolVersion.V3.asInt() ? 0x00 : 0x01; + Assert.assertEquals(expectedStreamId, e.getStreamId()); Assert.assertTrue(e.getMessage().contains("Invalid or unsupported protocol version")); } } @@ -101,6 +107,108 @@ public class ProtocolErrorTest { } } + @Test + public void testIncompleteHeaderWithInvalidProtocolVersion() throws Exception + { + // CASSANDRA-21508: when fewer than a full header's worth of bytes have arrived AND the protocol + // version is unsupported, decode cannot trust/recover a stream id. It defers the protocol error and + // wraps it with Message.UNSET_STREAM_ID, so the channel-level exception handler closes the connection + // rather than emit an unroutable error frame. Exercises Envelope.Decoder.decode() lines 387-398. + Envelope.Decoder dec = new Envelope.Decoder(); + + List results = new ArrayList<>(); + // Unsupported version (two above CURRENT) with only part of a header - fewer than Header.LENGTH (9) bytes. + byte[] bytes = new byte[] { + (byte) REQUEST.addToVersion(ProtocolVersion.CURRENT.asInt() + 2), // direction & unsupported version + 0x00, // flags + 0x00, 0x2a, // partial header, truncated before the length field + }; + ByteBuf buf = Unpooled.wrappedBuffer(bytes); + try { + dec.decode(null, buf, results); + Assert.fail("Expected protocol error"); + } catch (ProtocolException e) { + Assert.assertTrue(e.getMessage().contains("Invalid or unsupported protocol version")); + } + } + + @Test + public void extractHeaderReturnsRecoverableErrorOnBetaFlagViolation() throws Exception + { + // CASSANDRA-21508: on the framed (V5+) path, a V6 (beta) envelope whose USE_BETA flag is + // unset is an invalid but *recoverable* protocol error. Envelope.Decoder.extractHeader documents that + // it never throws, and instead returns a HeaderExtractionResult carrying the frame's stream id, so the + // caller (CQLMessageHandler.processOneContainedMessage) can route an ERROR back on that stream and keep + // processing subsequent envelopes. + Envelope.Decoder dec = new Envelope.Decoder(); + + int streamId = 42; + // A complete 9-byte header advertising V6 (beta) as a REQUEST, with USE_BETA deliberately unset. + byte[] header = new byte[] { + (byte) REQUEST.addToVersion(ProtocolVersion.V6.asInt()), // direction & beta version + 0x00, // flags - USE_BETA deliberately unset + 0x00, 0x2a, // stream id = 42 + 0x05, // opcode = OPTIONS + 0x00, 0x00, 0x00, 0x00, // body length = 0 + }; + ByteBuffer buf = ByteBuffer.wrap(header); + + Envelope.Decoder.HeaderExtractionResult result; + try + { + result = dec.extractHeader(buf); + } + catch (ErrorMessage.WrappedException e) + { + throw new AssertionError("extractHeader must not throw on a USE_BETA violation; a recoverable " + + "protocol error escaped as a WrappedException and would tear down the " + + "connection instead of returning a routable error", e); + } + + Assert.assertFalse("A USE_BETA violation should be reported as a (recoverable) extraction error", + result.isSuccess()); + Assert.assertEquals("the frame's stream id must be preserved so the error can be routed back", + streamId, result.streamId()); + Assert.assertTrue("expected a USE_BETA protocol error, got: " + result.error().getMessage(), + result.error().getMessage().contains("USE_BETA flag is unset")); + } + + @Test + public void decodeRecoversSingleByteStreamIdForOldProtocolVersions() throws Exception + { + // CASSANDRA-21508: protocol versions 1 and 2 use a shorter header whose stream id is a single + // byte. When such a version is rejected, decode must recover the stream id using that layout. + // Reading a 16-bit value would splice the stream id byte together with the following opcode byte and + // recover a bogus id (e.g. stream 0 + STARTUP opcode 1 -> stream 1), routing the error to a stream the + // client never used. A v1/v2 client (which downgrades on such an error) would then never see it and + // its connection would time out - as observed in ProtocolNegotiationTest#olderVersionsAreUnsupported. + Envelope.Decoder dec = new Envelope.Decoder(); + + int streamId = 0x07; + List results = new ArrayList<>(); + // A full (>= Header.LENGTH bytes) v1 frame so decode reaches the stream-id recovery, not the + // incomplete-header path: single-byte stream id, STARTUP opcode, and one body byte. + byte[] bytes = new byte[] { + (byte) REQUEST.addToVersion(1), // unsupported v1 + 0x00, // flags + (byte) streamId, // v1/v2 single-byte stream id + 0x01, // opcode = STARTUP (would be spliced in as the low byte) + 0x00, 0x00, 0x00, 0x01, // body length = 1 + 0x00 // 1 body byte, so readableBytes == Header.LENGTH + }; + ByteBuf buf = Unpooled.wrappedBuffer(bytes); + try { + dec.decode(null, buf, results); + Assert.fail("Expected protocol error"); + } catch (ErrorMessage.WrappedException e) { + Assert.assertEquals("a v1/v2 stream id must be read as a single byte, not spliced with the opcode", + streamId, e.getStreamId()); + Assert.assertTrue("expected a ProtocolException cause, got: " + e.getCause(), + e.getCause() instanceof ProtocolException); + Assert.assertTrue(e.getMessage().contains("Invalid or unsupported protocol version")); + } + } + @Test public void testInvalidDirection() throws Exception { @@ -161,7 +269,7 @@ public class ProtocolErrorTest { public void testErrorMessageWithNullString() { // test for CASSANDRA-11167 - ErrorMessage msg = ErrorMessage.fromException(new ServerError((String) null)); + ErrorMessage msg = ErrorMessage.fromTransportException(new ServerError((String) null)); assert msg.toString().endsWith("null") : msg.toString(); int size = ErrorMessage.codec.encodedSize(msg, ProtocolVersion.CURRENT); ByteBuf buf = Unpooled.buffer(size); diff --git a/test/unit/org/apache/cassandra/transport/ProtocolNegotiationTest.java b/test/unit/org/apache/cassandra/transport/ProtocolNegotiationTest.java index 7611b51379..d41e424436 100644 --- a/test/unit/org/apache/cassandra/transport/ProtocolNegotiationTest.java +++ b/test/unit/org/apache/cassandra/transport/ProtocolNegotiationTest.java @@ -126,29 +126,31 @@ public class ProtocolNegotiationTest extends CQLTester for (int i = 0; i < 100; i++) { int streamId = random.nextInt(254) + 1; - options.setStreamId(streamId); + options.setSource(new Envelope(Envelope.Header.dummy(streamId, options.type), null)); Message.Response response = client.execute(options); + // The stream id is stamped onto the frame at encoding time; verify it round-trips by + // reading it back off the response's source envelope. assertEquals(String.format("StreamId mismatch; version: %s, seed: %s, iter: %s, expected: %s, actual: %s", - version, seed, i, streamId, response.getStreamId()), - streamId, response.getStreamId()); + version, seed, i, streamId, response.getSource().header.streamId), + streamId, response.getSource().header.streamId); } int streamId = random.nextInt(254) + 1; // STARTUP messages are handled by the initial connection handler StartupMessage startup = new StartupMessage(ImmutableMap.of(CQL_VERSION, QueryProcessor.CQL_VERSION.toString())); - startup.setStreamId(streamId); + startup.setSource(new Envelope(Envelope.Header.dummy(streamId, startup.type), null)); Message.Response response = client.execute(startup); assertEquals(String.format("StreamId mismatch after negotiation; version: %s, expected: %s, actual %s", - version, streamId, response.getStreamId()), - streamId, response.getStreamId()); + version, streamId, response.getSource().header.streamId), + streamId, response.getSource().header.streamId); // Following STARTUP, the version specific handlers are fully responsible for processing messages QueryMessage query = new QueryMessage("SELECT * FROM system.local", QueryOptions.DEFAULT); - query.setStreamId(streamId); + query.setSource(new Envelope(Envelope.Header.dummy(streamId, query.type), null)); response = client.execute(query); assertEquals(String.format("StreamId mismatch after negotiation; version: %s, expected: %s, actual %s", - version, streamId, response.getStreamId()), - streamId, response.getStreamId()); + version, streamId, response.getSource().header.streamId), + streamId, response.getSource().header.streamId); } catch (IOException e) { @@ -215,9 +217,9 @@ public class ProtocolNegotiationTest extends CQLTester QueryMessage query = new QueryMessage("SELECT * FROM system.local", QueryOptions.DEFAULT) { @Override - public Envelope encode(ProtocolVersion originalVersion) + public Envelope encode(ProtocolVersion originalVersion, int streamId) { - return super.encode(v); + return super.encode(v, streamId); } }; try diff --git a/test/unit/org/apache/cassandra/triggers/TriggerExecutorTest.java b/test/unit/org/apache/cassandra/triggers/TriggerExecutorTest.java index be1c617a7b..b7430d2d06 100644 --- a/test/unit/org/apache/cassandra/triggers/TriggerExecutorTest.java +++ b/test/unit/org/apache/cassandra/triggers/TriggerExecutorTest.java @@ -47,9 +47,11 @@ import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TriggerMetadata; import org.apache.cassandra.schema.Triggers; +import org.apache.cassandra.utils.ClassLoadingTestSupport; import org.apache.cassandra.utils.FBUtilities; import static org.apache.cassandra.utils.ByteBufferUtil.bytes; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -114,6 +116,18 @@ public class TriggerExecutorTest .withMessageContaining("Trigger class NotExistedTriggerClass couldn't be found."); } + @Test + public void nonTriggerClassRejectedWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(NonTrigger.class); + + assertThatExceptionOfType(ConfigurationException.class) + .isThrownBy(() -> TriggerExecutor.instance.loadTriggerClass(NonTrigger.class.getName())) + .withMessageContaining("must extend or implement " + ITrigger.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(NonTrigger.class)).isFalse(); + } + @Test public void noTriggerMutations() throws ConfigurationException, InvalidRequestException { @@ -326,6 +340,18 @@ public class TriggerExecutorTest } } + public static class NonTrigger + { + static + { + ClassLoadingTestSupport.markInitialized(NonTrigger.class); + } + + public NonTrigger() + { + } + } + public static class SameKeySameCfTrigger implements ITrigger { public Collection augment(Partition partition) diff --git a/test/unit/org/apache/cassandra/utils/ClassLoadingSearchProbe.java b/test/unit/org/apache/cassandra/utils/ClassLoadingSearchProbe.java new file mode 100644 index 0000000000..401d89eb36 --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/ClassLoadingSearchProbe.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.utils; + +/** + * Search-package fall-through probe. Shares its simple name ({@code ClassLoadingSearchProbe}) with + * {@link org.apache.cassandra.config.ClassLoadingSearchProbe}, but is NOT a {@link Runnable}, so it represents the + * "wrong type under an earlier search package" case. Its static initializer records initialization so tests can + * confirm the search resolves without initializing this class. + */ +public final class ClassLoadingSearchProbe +{ + static + { + ClassLoadingTestSupport.markInitialized(ClassLoadingSearchProbe.class); + } +} diff --git a/test/unit/org/apache/cassandra/utils/ClassLoadingTestNonAssignable.java b/test/unit/org/apache/cassandra/utils/ClassLoadingTestNonAssignable.java new file mode 100644 index 0000000000..8faa92286a --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/ClassLoadingTestNonAssignable.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cassandra.utils; + +public final class ClassLoadingTestNonAssignable +{ + static + { + ClassLoadingTestSupport.markInitialized(ClassLoadingTestNonAssignable.class); + } + + private ClassLoadingTestNonAssignable() + { + } +} diff --git a/test/unit/org/apache/cassandra/utils/ClassLoadingTestSupport.java b/test/unit/org/apache/cassandra/utils/ClassLoadingTestSupport.java new file mode 100644 index 0000000000..41d76473f9 --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/ClassLoadingTestSupport.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.cassandra.utils; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class ClassLoadingTestSupport +{ + private static final Set initializedClasses = ConcurrentHashMap.newKeySet(); + + private ClassLoadingTestSupport() + { + } + + public static void markInitialized(Class initializedClass) + { + initializedClasses.add(initializedClass.getName()); + } + + public static boolean wasInitialized(Class initializedClass) + { + return initializedClasses.contains(initializedClass.getName()); + } + + public static void assertNotInitialized(Class initializedClass) + { + if (wasInitialized(initializedClass)) + throw new AssertionError(initializedClass.getName() + " has already been initialized"); + } +} diff --git a/test/unit/org/apache/cassandra/utils/FBUtilitiesTest.java b/test/unit/org/apache/cassandra/utils/FBUtilitiesTest.java index c9451730f3..00e94230a0 100644 --- a/test/unit/org/apache/cassandra/utils/FBUtilitiesTest.java +++ b/test/unit/org/apache/cassandra/utils/FBUtilitiesTest.java @@ -46,6 +46,7 @@ import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.audit.IAuditLogger; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.marshal.AbstractType; @@ -59,8 +60,13 @@ import org.apache.cassandra.dht.LocalPartitioner; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.OrderPreservingPartitioner; import org.apache.cassandra.dht.RandomPartitioner; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.io.compress.AbstractCompressionProvider; +import org.apache.cassandra.security.AbstractCryptoProvider; +import org.apache.cassandra.security.ISslContextFactory; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -69,6 +75,84 @@ public class FBUtilitiesTest public static final Logger LOGGER = LoggerFactory.getLogger(FBUtilitiesTest.class); + @Test + public void testTypedClassForNameRejectsWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + assertThatThrownBy(() -> FBUtilities.classForNameWithoutInitialization(ClassLoadingTestNonAssignable.class.getName(), "test class", Runnable.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + Runnable.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testTypedConstructRejectsWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + assertThatThrownBy(() -> FBUtilities.construct(ClassLoadingTestNonAssignable.class.getName(), "test class", Runnable.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + Runnable.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testTypedInstanceOrConstructRejectsWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + assertThatThrownBy(() -> FBUtilities.instanceOrConstruct(ClassLoadingTestNonAssignable.class.getName(), "test class", Runnable.class)) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + Runnable.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testNewAuditLoggerRejectsWrongTypeWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + assertThatThrownBy(() -> FBUtilities.newAuditLogger(ClassLoadingTestNonAssignable.class.getName(), Map.of())) + .isInstanceOf(ConfigurationException.class) + .hasRootCauseInstanceOf(ConfigurationException.class) + .hasStackTraceContaining("must extend or implement " + IAuditLogger.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testNewSslContextFactoryRejectsWrongTypeWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + assertThatThrownBy(() -> FBUtilities.newSslContextFactory(ClassLoadingTestNonAssignable.class.getName(), Map.of())) + .isInstanceOf(ConfigurationException.class) + .hasStackTraceContaining("must extend or implement " + ISslContextFactory.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testNewCryptoProviderRejectsWrongTypeWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + assertThatThrownBy(() -> FBUtilities.newCryptoProvider(ClassLoadingTestNonAssignable.class.getName(), Map.of())) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining("must extend or implement " + AbstractCryptoProvider.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + + @Test + public void testNewCompressionProviderRejectsWrongTypeWithoutInitializing() + { + ClassLoadingTestSupport.assertNotInitialized(ClassLoadingTestNonAssignable.class); + assertThatThrownBy(() -> FBUtilities.newCompressionProvider(ClassLoadingTestNonAssignable.class.getName())) + .isInstanceOf(ConfigurationException.class) + .hasStackTraceContaining("must extend or implement " + AbstractCompressionProvider.class.getName()); + + assertThat(ClassLoadingTestSupport.wasInitialized(ClassLoadingTestNonAssignable.class)).isFalse(); + } + @Test public void testCompareByteSubArrays() { diff --git a/tools/sstableloader/src/org/apache/cassandra/tools/LoaderOptions.java b/tools/sstableloader/src/org/apache/cassandra/tools/LoaderOptions.java index 42da2d5af1..e7af625895 100644 --- a/tools/sstableloader/src/org/apache/cassandra/tools/LoaderOptions.java +++ b/tools/sstableloader/src/org/apache/cassandra/tools/LoaderOptions.java @@ -50,6 +50,7 @@ import org.apache.cassandra.config.YamlConfigurationLoader; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.util.File; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.FBUtilities; import static org.apache.cassandra.config.DataRateSpec.DataRateUnit.MEBIBYTES_PER_SECOND; import static org.apache.cassandra.config.EncryptionOptions.ClientEncryptionOptions.ClientAuth.REQUIRED; @@ -715,11 +716,12 @@ public class LoaderOptions { try { - Class authProviderClass = Class.forName(authProviderName); - Constructor constructor = authProviderClass.getConstructor(String.class, String.class); - authProvider = (AuthProvider)constructor.newInstance(user, passwd); + Class authProviderClass = + FBUtilities.classForNameWithoutInitialization(authProviderName, "auth provider", AuthProvider.class); + Constructor constructor = authProviderClass.getConstructor(String.class, String.class); + authProvider = constructor.newInstance(user, passwd); } - catch (ClassNotFoundException e) + catch (ConfigurationException e) { errorMsg("Unknown auth provider: " + e.getMessage(), getCmdLineOptions()); } @@ -746,9 +748,9 @@ public class LoaderOptions { try { - authProvider = (AuthProvider)Class.forName(authProviderName).newInstance(); + authProvider = FBUtilities.construct(authProviderName, "auth provider", AuthProvider.class); } - catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) + catch (ConfigurationException e) { errorMsg("Unknown auth provider: " + e.getMessage(), getCmdLineOptions()); } diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/OptionReplication.java b/tools/stress/src/org/apache/cassandra/stress/settings/OptionReplication.java index 141c645079..ff2d2ef2cc 100644 --- a/tools/stress/src/org/apache/cassandra/stress/settings/OptionReplication.java +++ b/tools/stress/src/org/apache/cassandra/stress/settings/OptionReplication.java @@ -28,6 +28,7 @@ import java.util.Map; import com.google.common.base.Function; import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.utils.FBUtilities; /** * For specifying replication options @@ -76,9 +77,9 @@ class OptionReplication extends OptionMulti { try { - Class clazz = Class.forName(fullname); - if (!AbstractReplicationStrategy.class.isAssignableFrom(clazz)) - throw new IllegalArgumentException(clazz + " is not a replication strategy"); + FBUtilities.classForNameWithoutInitialization(fullname, + "replication strategy", + AbstractReplicationStrategy.class); strategy = fullname; break; } catch (Exception ignore) diff --git a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsMode.java b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsMode.java index 89c26a874e..c59044a361 100644 --- a/tools/stress/src/org/apache/cassandra/stress/settings/SettingsMode.java +++ b/tools/stress/src/org/apache/cassandra/stress/settings/SettingsMode.java @@ -33,6 +33,7 @@ import com.datastax.driver.core.ProtocolOptions; import com.datastax.driver.core.ProtocolVersion; import org.apache.cassandra.stress.util.ResultLogger; +import org.apache.cassandra.utils.FBUtilities; import static java.lang.String.format; import static org.apache.cassandra.stress.settings.SettingsCredentials.CQL_PASSWORD_PROPERTY_KEY; @@ -91,9 +92,10 @@ public class SettingsMode implements Serializable { try { - Class clazz = Class.forName(authProviderClassname); - if (!AuthProvider.class.isAssignableFrom(clazz)) - throw new IllegalArgumentException(clazz + " is not a valid auth provider"); + Class clazz = + FBUtilities.classForNameWithoutInitialization(authProviderClassname, + "auth provider", + AuthProvider.class); // check we can instantiate it if (PlainTextAuthProvider.class.equals(clazz)) {