Merge branch 'apache:trunk' into liuisaac/CASSANDRA-19831/trunk

This commit is contained in:
Isaac Liu 2026-08-01 11:48:39 -04:00 committed by GitHub
commit da943add35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
185 changed files with 4389 additions and 790 deletions

View File

@ -78,7 +78,7 @@
<exclude name="test/data/jmxdump/cassandra-*-jmx.yaml"/>
<!-- Documentation files -->
<exclude name=".github/pull_request_template.md"/>
<exclude name=".github/workflows/code-check.yaml"/>
<exclude name=".github/workflows/**.yaml"/>
<exclude NAME="doc/modules/**/*"/>
<exclude NAME="src/java/**/*.md"/>
<exclude NAME="**/README*"/>

View File

@ -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

View File

@ -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
```
The jenkins-home volume is kept; delete it separately with `kubectl delete pvc cassius-jenkins`

View File

@ -18,6 +18,7 @@ bs4
dotenv
kubernetes
python-jenkins
pyyaml
requests
# optional for different clouds

View File

@ -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):

58
.github/workflows/jenkins-check.yaml vendored Normal file
View File

@ -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

97
.jenkins/Jenkinsfile vendored
View File

@ -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
if (isCanonical()) {
def agentScriptsUrl = "https://raw.githubusercontent.com/apache/cassandra-builds/trunk/jenkins-dsl/agent_scripts/"
if (isCanonical()) {
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,13 +587,59 @@ def cleanAgentDocker(job_name, agentScriptsUrl) {
}
def logAgentInfo(job_name, agentScriptsUrl) {
sh """#!/bin/bash
// 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"
"""
}
/////////////////////////////////////////
////// scripting support for summary ////
@ -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

View File

@ -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 <file>
```
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/<site>-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} <last-good-revision> -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

View File

@ -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.<your-domain>
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
@ -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

117
.jenkins/k8s/jenkins-test.sh Executable file
View File

@ -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}

View File

@ -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)
* Dont 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

View File

@ -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:

View File

@ -651,6 +651,64 @@
<jflex file="${build.src.java}/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerImpl.jflex" destdir="${build.src.gen-java}/" />
</target>
<!--
Copies a source class into ${build.src.gen-java} under a NEW class name while keeping it
in its ORIGINAL package, so it compiles into an independent class that still enjoys
package-private access to its neighbours.
This lets a hot call site use its own dedicated copy of a class (e.g. a RowMergeIterator
copy of MergeIterator) so that the virtual calls inside it stay mono-/bi-morphic and
remain inlinable, instead of turning megamorphic once every caller in the code base
funnels the shared original through the same bytecode.
The transformation is purely textual: every whole-word occurrence of the original simple
class name is rewritten to the new name. Matching on word boundaries keeps substrings
such as IMergeIterator intact. The package declaration is left untouched, so the copy
stays in the same package as the original and no imports need to change.
-->
<macrodef name="gen-java-copy">
<!-- Fully-qualified name of the class to copy, e.g. org.apache.cassandra.utils.MergeIterator -->
<attribute name="class"/>
<!-- New simple class name for the copy, e.g. RowMergeIterator -->
<attribute name="newName"/>
<sequential>
<local name="gen.src.rel"/>
<local name="gen.simple.name"/>
<!-- source file path relative to ${build.src.java}: dots -> slashes -->
<loadresource property="gen.src.rel">
<string value="@{class}"/>
<filterchain><tokenfilter><replaceregex pattern="\." replace="/" flags="g"/></tokenfilter></filterchain>
</loadresource>
<!-- original simple class name = last segment of the fully-qualified name -->
<loadresource property="gen.simple.name">
<string value="@{class}"/>
<filterchain><tokenfilter><replaceregex pattern="^.*\.([^.]+)$" replace="\1"/></tokenfilter></filterchain>
</loadresource>
<copy todir="${build.src.gen-java}" preservelastmodified="true">
<fileset dir="${build.src.java}" includes="${gen.src.rel}.java"/>
<!-- keep the original package directory, only rename the file -->
<mapper type="regexp" from="^(.*[\\/])[^\\/]+$" to="\1@{newName}.java"/>
<filterchain>
<tokenfilter>
<replaceregex pattern="\b${gen.simple.name}\b" replace="@{newName}" flags="g"/>
</tokenfilter>
</filterchain>
</copy>
</sequential>
</macrodef>
<!--
Generate the copies of hand-picked classes before compilation. Add a
<gen-java-copy class="..." newName="..."/> line here for every class that needs a
dedicated copy.
-->
<target name="gen-java-copies" depends="init"
description="Copy selected classes under new names (e.g. MergeIterator -> RowMergeIterator) into src/gen-java to avoid megamorphic calls">
<gen-java-copy class="org.apache.cassandra.utils.MergeIterator" newName="RowMergeIterator"/>
<gen-java-copy class="org.apache.cassandra.utils.MergeIterator" newName="UnfilteredMergeIterator"/>
<gen-java-copy class="org.apache.cassandra.utils.MergeIterator" newName="ComplexCellMergeIterator"/>
</target>
<!-- create properties file with C version -->
<target name="_createVersionPropFile" depends="_get-git-sha,set-cqlsh-version,_set-build-date">
<taskdef name="propertyfile" classname="org.apache.tools.ant.taskdefs.optional.PropertyFile"/>
@ -710,7 +768,7 @@
</javac>
</target>
<target depends="init,gen-cql3-grammar,generate-cql-html,generate-jflex-java"
<target depends="init,gen-cql3-grammar,generate-cql-html,generate-jflex-java,gen-java-copies"
name="build-project">
<echo message="${ant.project.name}: ${ant.file}"/>
<!-- Order matters! -->
@ -2173,7 +2231,7 @@
</target>
<!-- Generate IDEA project description files -->
<target name="generate-idea-files" depends="init,resolver-dist-lib,gen-cql3-grammar,generate-jflex-java,_createVersionPropFile" description="Generate IDEA files">
<target name="generate-idea-files" depends="init,resolver-dist-lib,gen-cql3-grammar,generate-jflex-java,gen-java-copies,_createVersionPropFile" description="Generate IDEA files">
<delete dir=".idea"/>
<delete file="${eclipse.project.name}.iml"/>
<mkdir dir=".idea"/>

View File

@ -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=<directory>`
The directory location of the `cassandra.yaml file`. The default

View File

@ -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:

View File

@ -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");}
;

View File

@ -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> T authInstantiate(ParameterizedClass authCls, Class<T> defaultCls) {
private static <T> T authInstantiate(ParameterizedClass authCls, Class<T> expectedType, Class<? extends T> 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

View File

@ -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);

View File

@ -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))

View File

@ -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<? extends SeedProvider> 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

View File

@ -64,7 +64,17 @@ public class ParameterizedClass
p.containsKey(PARAMETERS) ? (Map<String, String>)((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> K newInstance(ParameterizedClass parameterizedClass, List<String> searchPackages)
{
return newInstance(parameterizedClass, searchPackages, null);
}
static public <K> K newInstance(ParameterizedClass parameterizedClass, List<String> searchPackages, Class<K> 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));

View File

@ -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;

View File

@ -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);
}

View File

@ -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

View File

@ -116,7 +116,7 @@ public interface ClusteringBoundOrBoundary<V> extends ClusteringPrefix<V>
public ClusteringBoundOrBoundary<byte[]> deserialize(DataInputPlus in, int version, List<AbstractType<?>> types) throws IOException
{
Kind kind = Kind.values()[in.readByte()];
Kind kind = Kind.fromOrdinal(in.readByte());
return deserializeValues(in, kind, version, types);
}

View File

@ -84,6 +84,8 @@ public interface ClusteringPrefix<V> extends IMeasurableMemory, Clusterable<V>
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<V> extends IMeasurableMemory, Clusterable<V>
this.asByteComparable = asByteComparable;
}
public static Kind fromOrdinal(int ordinal)
{
return VALUES[ordinal];
}
/**
* Compares the 2 provided kind.
* <p>
@ -476,7 +483,7 @@ public interface ClusteringPrefix<V> extends IMeasurableMemory, Clusterable<V>
public void skip(DataInputPlus in, int version, List<AbstractType<?>> 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<V> extends IMeasurableMemory, Clusterable<V>
public ClusteringPrefix<byte[]> deserialize(DataInputPlus in, int version, List<AbstractType<?>> 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<V> extends IMeasurableMemory, Clusterable<V>
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;

View File

@ -178,7 +178,8 @@ public abstract class DeletionTime implements Comparable<DeletionTime>, 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)

View File

@ -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<Seekable, SelectionDeserializer> 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.");

View File

@ -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()

View File

@ -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:

View File

@ -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);

View File

@ -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

View File

@ -44,7 +44,17 @@ public abstract class ColumnSubselection implements Comparable<ColumnSubselectio
public static final Serializer serializer = new Serializer();
/* this enum is used in serialization; preserve order for compatibility */
private enum Kind { SLICE, ELEMENT }
private enum Kind
{
SLICE, ELEMENT;
private static final Kind[] VALUES = values();
static Kind fromOrdinal(int ordinal)
{
return VALUES[ordinal];
}
}
protected final ColumnMetadata column;
@ -229,7 +239,7 @@ public abstract class ColumnSubselection implements Comparable<ColumnSubselectio
}
}
Kind kind = Kind.values()[in.readUnsignedByte()];
Kind kind = Kind.fromOrdinal(in.readUnsignedByte());
switch (kind)
{
case SLICE:

View File

@ -102,7 +102,14 @@ public abstract class DataLimits
/** @deprecated See CASSANDRA-16582 */
@Deprecated(since = "4.0") SUPER_COLUMN_COUNTING_LIMIT, //Deprecated and unused in 4.0, stop publishing in 5.0, reclaim in 6.0
CQL_GROUP_BY_LIMIT,
CQL_GROUP_BY_PAGING_LIMIT,
CQL_GROUP_BY_PAGING_LIMIT;
private static final Kind[] VALUES = values();
public static Kind fromOrdinal(int ordinal)
{
return VALUES[ordinal];
}
}
public static DataLimits cqlLimits(int cqlRowLimit)
@ -1191,7 +1198,7 @@ public abstract class DataLimits
public DataLimits deserialize(DataInputPlus in, int version, TableMetadata metadata) throws IOException
{
Kind kind = Kind.values()[in.readUnsignedByte()];
Kind kind = Kind.fromOrdinal(in.readUnsignedByte());
switch (kind)
{
case CQL_LIMIT:

View File

@ -493,7 +493,17 @@ public class RowFilter implements Iterable<RowFilter.Expression>
// 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<RowFilter.Expression>
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)

View File

@ -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);
}
/**

View File

@ -143,8 +143,11 @@ public abstract class ValueGenerator<VALUE>
try
{
Class<? extends ValueGenerator> rawGeneratorClass =
FBUtilities.classForNameWithoutInitialization(className, "generator", ValueGenerator.class);
@SuppressWarnings("unchecked")
Class<? extends ValueGenerator<VALUE>> generatorClass =
FBUtilities.classForName(className, "generator");
(Class<? extends ValueGenerator<VALUE>>) rawGeneratorClass;
@SuppressWarnings("unchecked")
ValueGenerator<VALUE> generator = generatorClass.getConstructor(CustomGuardrailConfig.class)

View File

@ -127,8 +127,11 @@ public abstract class ValueValidator<VALUE>
try
{
Class<? extends ValueValidator> rawValidatorClass =
FBUtilities.classForNameWithoutInitialization(className, "validator", ValueValidator.class);
@SuppressWarnings("unchecked")
Class<? extends ValueValidator<VALUE>> validatorClass =
FBUtilities.classForName(className, "validator");
(Class<? extends ValueValidator<VALUE>>) rawValidatorClass;
@SuppressWarnings("unchecked")
ValueValidator<VALUE> validator = validatorClass.getConstructor(CustomGuardrailConfig.class)

View File

@ -39,7 +39,7 @@ public abstract class AbstractTimeUUIDType<T> extends TemporalType<T>
{
AbstractTimeUUIDType()
{
super(ComparisonType.CUSTOM);
super(ComparisonType.CUSTOM, 16);
} // singleton
@Override
@ -193,12 +193,6 @@ public abstract class AbstractTimeUUIDType<T> extends TemporalType<T>
return super.decomposeUntyped(value);
}
@Override
public int valueLengthIfFixed()
{
return 16;
}
@Override
public long toTimeInMillis(ByteBuffer value)
{

View File

@ -90,11 +90,18 @@ public abstract class AbstractType<T> implements Comparator<ByteBuffer>, 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<T> implements Comparator<ByteBuffer>, 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<T> implements Comparator<ByteBuffer>, Assignm
*/
public int valueLengthIfFixed()
{
return VARIABLE_LENGTH;
return valueLengthIfFixed;
}
/**
@ -508,7 +515,7 @@ public abstract class AbstractType<T> implements Comparator<ByteBuffer>, Assignm
*/
public final boolean isValueLengthFixed()
{
return valueLengthIfFixed() != VARIABLE_LENGTH;
return valueLengthIfFixed != VARIABLE_LENGTH;
}
/**
@ -570,7 +577,7 @@ public abstract class AbstractType<T> implements Comparator<ByteBuffer>, Assignm
public <V> void writeValue(V value, ValueAccessor<V> 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<T> implements Comparator<ByteBuffer>, Assignm
public <V> void writeValue(IndexedValueHolder<V> valueHolder, int i, ValueAccessor<V> 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<T> implements Comparator<ByteBuffer>, Assignm
public <V> long writtenLength(V value, ValueAccessor<V> 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<T> implements Comparator<ByteBuffer>, Assignm
public <V> long writtenLength(IndexedValueHolder<V> valueHolder, int i, ValueAccessor<V> 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<T> implements Comparator<ByteBuffer>, Assignm
public <V> V read(ValueAccessor<V> 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<T> implements Comparator<ByteBuffer>, Assignm
public void skipValue(DataInputPlus in) throws IOException
{
int length = valueLengthIfFixed();
int length = valueLengthIfFixed;
if (length >= 0)
in.skipBytesFully(length);
else

View File

@ -36,7 +36,7 @@ public class BooleanType extends AbstractType<Boolean>
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<Boolean>
return ARGUMENT_DESERIALIZER;
}
@Override
public int valueLengthIfFixed()
{
return 1;
}
@Override
public ByteBuffer getMaskedValue()
{

View File

@ -49,7 +49,7 @@ public class DateType extends AbstractType<Date>
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<Date>
return ARGUMENT_DESERIALIZER;
}
@Override
public int valueLengthIfFixed()
{
return 8;
}
@Override
public ByteBuffer getMaskedValue()
{

View File

@ -40,7 +40,7 @@ public class DoubleType extends NumberType<Double>
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<Double>
};
}
@Override
public int valueLengthIfFixed()
{
return 8;
}
@Override
public ByteBuffer add(Number left, Number right)
{

View File

@ -71,7 +71,7 @@ public class EmptyType extends AbstractType<Void>
public static final EmptyType instance = new EmptyType();
private EmptyType() {super(ComparisonType.CUSTOM);} // singleton
private EmptyType() {super(ComparisonType.CUSTOM, 0);} // singleton
@Override
public <V> ByteSource asComparableBytes(ValueAccessor<V> accessor, V data, ByteComparable.Version version)
@ -137,12 +137,6 @@ public class EmptyType extends AbstractType<Void>
throw new UnsupportedOperationException();
}
@Override
public int valueLengthIfFixed()
{
return 0;
}
@Override
public <V> long writtenLength(V value, ValueAccessor<V> accessor)
{

View File

@ -41,7 +41,7 @@ public class FloatType extends NumberType<Float>
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<Float>
};
}
@Override
public int valueLengthIfFixed()
{
return 4;
}
@Override
public ByteBuffer add(Number left, Number right)
{

View File

@ -43,7 +43,7 @@ public class Int32Type extends NumberType<Integer>
Int32Type()
{
super(ComparisonType.CUSTOM);
super(ComparisonType.CUSTOM, 4);
} // singleton
@Override
@ -152,12 +152,6 @@ public class Int32Type extends NumberType<Integer>
};
}
@Override
public int valueLengthIfFixed()
{
return 4;
}
@Override
public ByteBuffer add(Number left, Number right)
{

View File

@ -44,7 +44,7 @@ public class LexicalUUIDType extends AbstractType<UUID>
LexicalUUIDType()
{
super(ComparisonType.CUSTOM);
super(ComparisonType.CUSTOM, 16);
} // singleton
@Override
@ -148,12 +148,6 @@ public class LexicalUUIDType extends AbstractType<UUID>
return ARGUMENT_DESERIALIZER;
}
@Override
public int valueLengthIfFixed()
{
return 16;
}
@Override
public ByteBuffer getMaskedValue()
{

View File

@ -41,7 +41,7 @@ public class LongType extends NumberType<Long>
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<Long>
};
}
@Override
public int valueLengthIfFixed()
{
return 8;
}
@Override
public ByteBuffer add(Number left, Number right)
{

View File

@ -38,6 +38,11 @@ public abstract class MultiElementType<T> extends AbstractType<T>
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<T> extends AbstractType<T>
throw new UnsupportedOperationException(this + " does not support retrieving elements by key or index");
}
}

View File

@ -34,6 +34,11 @@ public abstract class NumberType<T extends Number> extends AbstractType<T>
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.

View File

@ -57,7 +57,7 @@ public class ReversedType<T> extends AbstractType<T>
private ReversedType(AbstractType<T> baseType)
{
super(ComparisonType.CUSTOM);
super(ComparisonType.CUSTOM, baseType.valueLengthIfFixed());
this.baseType = baseType;
}
@ -181,12 +181,6 @@ public class ReversedType<T> extends AbstractType<T>
return getInstance(baseType.withUpdatedUserType(udt));
}
@Override
public int valueLengthIfFixed()
{
return baseType.valueLengthIfFixed();
}
@Override
public boolean isReversed()
{

View File

@ -37,6 +37,11 @@ public abstract class TemporalType<T> extends AbstractType<T>
super(comparisonType);
}
protected TemporalType(ComparisonType comparisonType, int valueLengthIfFixed)
{
super(comparisonType, valueLengthIfFixed);
}
/**
* Returns the current temporal value.
* @return the current temporal value.

View File

@ -53,7 +53,7 @@ public class TimestampType extends TemporalType<Date>
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<Date>
return TimestampSerializer.instance;
}
@Override
public int valueLengthIfFixed()
{
return 8;
}
@Override
protected void validateDuration(Duration duration)
{

View File

@ -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<? extends AbstractType<?>> typeClass = FBUtilities.<AbstractType<?>>classForName(className, "abstract-type");
Class<? extends AbstractType<?>> 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<? extends AbstractType<?>> typeClass = FBUtilities.<AbstractType<?>>classForName(className, "abstract-type");
Class<? extends AbstractType<?>> typeClass = getAbstractTypeClass(compareWith);
if (PseudoUtf8Type.class.isAssignableFrom(typeClass))
{
if (StorageService.instance.isDaemonSetupCompleted())
@ -491,6 +489,19 @@ public class TypeParser
}
}
private static Class<? extends AbstractType<?>> 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<? extends AbstractType<?>> typeClass =
(Class<? extends AbstractType<?>>) FBUtilities.classForNameWithoutInitialization(className,
"abstract-type",
AbstractType.class);
return typeClass;
}
private static AbstractType<?> getRawAbstractType(Class<? extends AbstractType<?>> typeClass) throws ConfigurationException
{
try

View File

@ -56,7 +56,7 @@ public class UUIDType extends AbstractType<UUID>
UUIDType()
{
super(ComparisonType.CUSTOM);
super(ComparisonType.CUSTOM, 16);
}
@Override
@ -252,12 +252,6 @@ public class UUIDType extends AbstractType<UUID>
return (uuid.get(6) & 0xf0) >> 4;
}
@Override
public int valueLengthIfFixed()
{
return 16;
}
@Override
public ByteBuffer getMaskedValue()
{

View File

@ -82,20 +82,16 @@ public final class VectorType<T> extends MultiElementType<List<T>>
public final AbstractType<T> elementType;
public final int dimension;
private final TypeSerializer<T> elementSerializer;
private final int valueLengthIfFixed;
private final VectorSerializer serializer;
private VectorType(AbstractType<T> 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<T> extends MultiElementType<List<T>>
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

View File

@ -61,7 +61,18 @@ public abstract class AbstractCell<V> extends Cell<V>
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()

View File

@ -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<byte[]>
public class ArrayCell extends HeapAbstractCell<byte[]>
{
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<byte[]>
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<byte[]>
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<byte[]>
return ByteArrayAccessor.instance;
}
public CellPath path()
{
return path;
}
public Cell<?> withUpdatedColumn(ColumnMetadata newColumn)
{
@ -144,9 +121,4 @@ public class ArrayCell extends AbstractCell<byte[]>
return EMPTY_SIZE + ObjectSizes.sizeOfArray(value) - value.length + (path == null ? 0 : path.unsharedHeapSizeExcludingData());
}
@Override
protected int localDeletionTimeAsUnsignedInt()
{
return localDeletionTimeUnsignedInteger;
}
}

View File

@ -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))

View File

@ -30,17 +30,12 @@ import org.apache.cassandra.utils.memory.ByteBufferCloner;
import static java.lang.String.format;
public class BufferCell extends AbstractCell<ByteBuffer>
public class BufferCell extends HeapAbstractCell<ByteBuffer>
{
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<ByteBuffer>
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<ByteBuffer>
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<ByteBuffer>
return ByteBufferAccessor.instance;
}
public CellPath path()
{
return path;
}
public Cell<?> withUpdatedColumn(ColumnMetadata newColumn)
{
@ -163,10 +140,4 @@ public class BufferCell extends AbstractCell<ByteBuffer>
{
return EMPTY_SIZE + ObjectSizes.sizeOnHeapExcludingDataOf(value) + (path == null ? 0 : path.unsharedHeapSizeExcludingData());
}
@Override
protected int localDeletionTimeAsUnsignedInt()
{
return localDeletionTimeUnsignedInteger;
}
}

View File

@ -150,6 +150,8 @@ public abstract class Cell<V> extends ColumnData
return deletionTimeUnsignedIntegerToLong(localDeletionTimeAsUnsignedInt());
}
public abstract long minDeletionTime();
/**
* Whether the cell is a tombstone or not.
*

View File

@ -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<V> extends AbstractCell<V>
{
// 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;
}
}

View File

@ -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<ColumnData>, 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.
* <p>
* 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<ColumnData>, 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<ColumnData>, 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<ColumnData>, 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<ColumnData> merged = MergeIterator.get(columnDataIterators, ColumnData.comparator, columnDataReducer);
Iterator<ColumnData> merged = RowMergeIterator.get(columnDataIterators, ColumnData.comparator, columnDataReducer);
while (merged.hasNext())
{
ColumnData data = merged.next();
@ -819,8 +825,12 @@ public interface Row extends Unfiltered, Iterable<ColumnData>, IMeasurableMemory
try (BulkIterator<ColumnData> 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<ColumnData>, IMeasurableMemory
return rows;
}
private static class ColumnDataReducer extends MergeIterator.Reducer<ColumnData, ColumnData>
private static class ColumnDataReducer extends RowMergeIterator.Reducer<ColumnData, ColumnData>
{
private ColumnMetadata column;
private final List<ColumnData> versions;
private final ColumnData[] versions;
private int versionsSize;
private DeletionTime activeDeletion;
@ -854,7 +865,7 @@ public interface Row extends Unfiltered, Iterable<ColumnData>, 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<ColumnData>, 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<ColumnData>, IMeasurableMemory
if (column.isSimple())
{
Cell<?> merged = null;
for (int i=0, isize=versions.size(); i<isize; i++)
for (int i = 0; i < versionsSize; i++)
{
Cell<?> 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<ColumnData>, IMeasurableMemory
complexBuilder.newColumn(column);
complexCells.clear();
DeletionTime complexDeletion = DeletionTime.LIVE;
for (int i=0, isize=versions.size(); i<isize; i++)
for (int i = 0; i < versionsSize; i++)
{
ColumnData data = versions.get(i);
ColumnData data = versions[i];
ComplexColumnData cd = (ComplexColumnData)data;
if (cd.complexDeletion().supersedes(complexDeletion))
complexDeletion = cd.complexDeletion();
@ -923,7 +938,7 @@ public interface Row extends Unfiltered, Iterable<ColumnData>, IMeasurableMemory
cellReducer.setActiveDeletion(activeDeletion);
}
Iterator<Cell<?>> cells = MergeIterator.get(complexCells, Cell.comparator, cellReducer);
Iterator<Cell<?>> cells = ComplexCellMergeIterator.get(complexCells, Cell.comparator, cellReducer);
while (cells.hasNext())
{
Cell<?> merged = cells.next();
@ -937,11 +952,12 @@ public interface Row extends Unfiltered, Iterable<ColumnData>, IMeasurableMemory
protected void onKeyChange()
{
column = null;
versions.clear();
Arrays.fill(versions, 0, versionsSize, null);
versionsSize = 0;
}
}
private static class CellReducer extends MergeIterator.Reducer<Cell<?>, Cell<?>>
private static class CellReducer extends ComplexCellMergeIterator.Reducer<Cell<?>, Cell<?>>
{
private DeletionTime activeDeletion;
private Cell<?> merged;

View File

@ -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<Unfiltered, Unfiltered>
private class MergeReducer extends UnfilteredMergeIterator.Reducer<Unfiltered, Unfiltered>
{
private final MergeListener listener;

View File

@ -128,6 +128,7 @@ public final class DiagnosticEventPersistence
LastEventIdBroadcaster.instance().setLastEventId(event.getClass().getName(), store.getLastEventId());
}
@SuppressWarnings("unchecked")
private Class<DiagnosticEvent> 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<DiagnosticEvent> clazz = (Class<DiagnosticEvent>) 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<DiagnosticEvent>) clazz.asSubclass(DiagnosticEvent.class);
}
private DiagnosticEventStore<Long> getStore(Class cls)

View File

@ -921,6 +921,11 @@ public class SecondaryIndexManager implements IndexRegistry, INotificationConsum
return indexes.get(indexName);
}
static Class<? extends Index> 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<? extends Index> indexClass = FBUtilities.classForName(className, "Index");
Class<? extends Index> indexClass = loadIndexClass(className);
Constructor<? extends Index> ctor = indexClass.getConstructor(ColumnFamilyStore.class, IndexMetadata.class);
newIndex = ctor.newInstance(baseCfs, indexDef);
}

View File

@ -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
@ -79,38 +104,43 @@ public class IndexInputReader extends IndexInput
@Override
public void close()
{
try
{
input.close();
}
finally
{
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;
}
}

View File

@ -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<IndexComponent> 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.
* <p>
@ -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<SegmentMetadata> 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))
{
// 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<SegmentMetadata> 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);
}
}

View File

@ -103,6 +103,12 @@ public class CellWithSource<T> extends Cell<T>
return cell.localDeletionTime();
}
@Override
public long minDeletionTime()
{
return cell.minDeletionTime();
}
@Override
public boolean isTombstone()
{

View File

@ -213,6 +213,12 @@ public class RowWithSource implements Row
return row.hasDeletion(nowInSec);
}
@Override
public long minLocalDeletionTime()
{
return row.minLocalDeletionTime();
}
@Override
public SearchIterator<ColumnMetadata, ColumnData> searchIterator()
{

View File

@ -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<? extends AbstractAnalyzer> 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<? extends AbstractAnalyzer> 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<? extends AbstractAnalyzer> 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,26 +142,31 @@ public class IndexMode
}
boolean isAnalyzed = false;
Class analyzerClass = null;
try
{
Class<? extends AbstractAnalyzer> analyzerClass = null;
if (indexOptions.get(INDEX_ANALYZER_CLASS_OPTION) != null)
{
analyzerClass = Class.forName(indexOptions.get(INDEX_ANALYZER_CLASS_OPTION));
try
{
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));
}
catch (ConfigurationException e)
{
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));
}
}
else if (indexOptions.get(INDEX_ANALYZED_OPTION) != null)
{
isAnalyzed = Boolean.parseBoolean(indexOptions.get(INDEX_ANALYZED_OPTION));
}
}
catch (ClassNotFoundException 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));
}
boolean isLiteral = false;
try

View File

@ -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)

View File

@ -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);

View File

@ -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<AbstractReplicationStrategy> 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<AbstractReplicationStrategy> strategyClass =
(Class<AbstractReplicationStrategy>) FBUtilities.classForNameWithoutInitialization(className,
"replication strategy",
AbstractReplicationStrategy.class);
return strategyClass;
}

View File

@ -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<ThreadLocalMetrics> 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();
}
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
ScheduledExecutors.scheduledTasks.schedule(() -> {
synchronized (freeMetricIdSetGuard)
{
freeMetricIdSet.set(metricId);
delayedToFreeMetricIdSet.set(metricId);
scheduleCleanupTask();
}
// must be called while holding this monitor (from a synchronized method)
@VisibleForTesting
protected void scheduleCleanupTask()
{
try
{
if (cleanupTask == null)
cleanupTask = ScheduledExecutors.scheduledTasks.schedule(this::triggerRecycling, 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") +
'}';
}
}, 5, TimeUnit.SECONDS);
}
@VisibleForTesting
static int getAllocatedMetricsCount()
{
int freeCount;
synchronized (freeMetricIdSetGuard)
{
freeCount = freeMetricIdSet.cardinality();
}
int freeCount = freeMetricIdSetTracker.getFreeMetricSetCardinality();
return idGenerator.get() - freeCount;
}

View File

@ -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,

View File

@ -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
{

View File

@ -311,15 +311,7 @@ public final class CompactionParams
String className = name.contains(".")
? name
: "org.apache.cassandra.db.compaction." + name;
Class<AbstractCompactionStrategy> 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);
}
/*

View File

@ -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<? extends ICompressor> 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)
{
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<String, String> compressionOptions) throws ConfigurationException
private static ICompressor createCompressor(Class<? extends ICompressor> compressorClass, Map<String, String> compressionOptions) throws ConfigurationException
{
if (compressorClass == null)
{

View File

@ -148,9 +148,7 @@ public final class IndexMetadata
// Get the fully qualified class name:
String className = getIndexClassName();
Class<Index> 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<? extends Index> indexerClass = FBUtilities.classForNameWithoutInitialization(className, "custom indexer", Index.class);
validateCustomIndexOptions(table, indexerClass, options);
}
}

View File

@ -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<String, String> 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())

View File

@ -286,7 +286,7 @@ public final class ReplicationParams
Map<String, String> 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<String, String> 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, int version)

View File

@ -105,7 +105,7 @@ public abstract class AbstractCryptoProvider
return;
}
FBUtilities.classForName(getProviderClassAsString(), "crypto provider");
FBUtilities.classForNameWithoutInitialization(getProviderClassAsString(), "crypto provider", Provider.class);
String providerName = getProviderName();
int providerPosition = getProviderPosition(providerName);

View File

@ -40,6 +40,7 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.ImmediateExecutor;
import org.apache.cassandra.config.TransparentDataEncryptionOptions;
import org.apache.cassandra.utils.FBUtilities;
import io.netty.util.concurrent.FastThreadLocal;
@ -71,9 +72,10 @@ public class CipherFactory
try
{
secureRandom = SecureRandom.getInstance("SHA1PRNG");
Class<KeyProvider> keyProviderClass = (Class<KeyProvider>)Class.forName(options.key_provider.class_name);
Constructor ctor = keyProviderClass.getConstructor(TransparentDataEncryptionOptions.class);
keyProvider = (KeyProvider)ctor.newInstance(options);
Class<? extends KeyProvider> keyProviderClass =
FBUtilities.classForNameWithoutInitialization(options.key_provider.class_name, "key provider", KeyProvider.class);
Constructor<? extends KeyProvider> ctor = keyProviderClass.getConstructor(TransparentDataEncryptionOptions.class);
keyProvider = ctor.newInstance(options);
}
catch (Exception e)
{

View File

@ -147,9 +147,11 @@ public class CacheService implements CacheServiceMBean
? DatabaseDescriptor.getRowCacheClassName() : "org.apache.cassandra.cache.NopCacheProvider";
try
{
Class<CacheProvider<RowCacheKey, IRowCacheEntry>> cacheProviderClass =
(Class<CacheProvider<RowCacheKey, IRowCacheEntry>>) Class.forName(cacheProviderClassName);
cacheProvider = cacheProviderClass.newInstance();
Class<? extends CacheProvider> cacheProviderClass =
FBUtilities.classForNameWithoutInitialization(cacheProviderClassName, "row cache provider", CacheProvider.class);
@SuppressWarnings("unchecked")
CacheProvider<RowCacheKey, IRowCacheEntry> typedCacheProvider = cacheProviderClass.newInstance();
cacheProvider = typedCacheProvider;
}
catch (Exception e)
{

View File

@ -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());
}

View File

@ -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);
}
}

View File

@ -3403,6 +3403,11 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
*/
@Deprecated(since = "4.0")
public List<InetAddress> getNaturalEndpoints(String keyspaceName, String cf, String key)
{
return getNaturalReplicas(keyspaceName, cf, key);
}
public List<InetAddress> getNaturalReplicas(String keyspaceName, String cf, String key)
{
EndpointsForToken replicas = getNaturalReplicasForToken(keyspaceName, cf, key);
List<InetAddress> inetList = new ArrayList<>(replicas.size());
@ -3410,11 +3415,16 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
return inetList;
}
public List<String> getNaturalEndpointsWithPort(String keyspaceName, String cf, String key)
public List<String> getNaturalReplicasWithPort(String keyspaceName, String cf, String key)
{
return Replicas.stringify(getNaturalReplicasForToken(keyspaceName, cf, key), true);
}
public List<String> getNaturalEndpointsWithPort(String keyspaceName, String cf, String key)
{
return getNaturalReplicasWithPort(keyspaceName, cf, key);
}
/** @deprecated See CASSANDRA-7544 */
@Deprecated(since = "4.0")
public List<InetAddress> getNaturalEndpoints(String keyspaceName, ByteBuffer key)

View File

@ -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<InetAddress> getNaturalEndpoints(String keyspaceName, String cf, String key);
public List<String> getNaturalEndpointsWithPort(String keyspaceName, String cf, String key);
/** @deprecated See CASSANDRA-17665 */
@Deprecated(since = "7.0") public List<String> getNaturalEndpointsWithPort(String keyspaceName, String cf, String key);
/** @deprecated See CASSANDRA-7544 */
@Deprecated(since = "4.0") public List<InetAddress> getNaturalEndpoints(String keyspaceName, ByteBuffer key);
public List<String> getNaturalEndpointsWithPort(String keysapceName, ByteBuffer key);
/** @deprecated See CASSANDRA-17665 */
@Deprecated(since = "7.0") public List<String> 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<InetAddress> getNaturalReplicas(String keyspaceName, String cf, String key);
public List<String> getNaturalReplicasWithPort(String keyspaceName, String cf, String key);
/**
* @deprecated use {@link #takeSnapshot(String tag, Map options, String... entities)} instead. See CASSANDRA-10907

View File

@ -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();

View File

@ -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
{

View File

@ -41,7 +41,7 @@ public class ExtensionKey<V, K extends ExtensionValue<V>> 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<ExtensionKey<?, ?>>
@ -58,7 +58,9 @@ public class ExtensionKey<V, K extends ExtensionValue<V>> extends MetadataKey
{
String id = in.readUTF();
String valType = in.readUTF();
return new ExtensionKey(id, FBUtilities.classForName(valType, "value type"));
Class<? extends ExtensionValue> valueType =
FBUtilities.classForNameWithoutInitialization(valType, "value type", ExtensionValue.class);
return new ExtensionKey(id, valueType);
}
@Override
@ -68,4 +70,3 @@ public class ExtensionKey<V, K extends ExtensionValue<V>> extends MetadataKey
}
}
}

View File

@ -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
{

View File

@ -1200,14 +1200,14 @@ public class NodeProbe implements AutoCloseable
ssProxy.setHintedHandoffThrottleInKB(throttleInKB);
}
public List<String> getEndpointsWithPort(String keyspace, String cf, String key)
public List<String> getReplicasWithPort(String keyspace, String cf, String key)
{
return ssProxy.getNaturalEndpointsWithPort(keyspace, cf, key);
return ssProxy.getNaturalReplicasWithPort(keyspace, cf, key);
}
public List<InetAddress> getEndpoints(String keyspace, String cf, String key)
public List<InetAddress> getReplicas(String keyspace, String cf, String key)
{
return ssProxy.getNaturalEndpoints(keyspace, cf, key);
return ssProxy.getNaturalReplicas(keyspace, cf, key);
}
public List<String> getSSTables(String keyspace, String cf, String key, boolean hexFormat)

View File

@ -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 = "<keyspace> <table> <key>", description = "The keyspace, the table, and the partition key for which we need to find the endpoint")
private List<String> 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<InetAddress> endpoints = probe.getEndpoints(ks, table, key);
for (InetAddress endpoint : endpoints)
{
probe.output().out.println(endpoint.getHostAddress());
}
}
}
}

View File

@ -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 = "<keyspace> <table> <key>", 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<String> 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<InetAddress> replicas = probe.getReplicas(ks, table, key);
for (InetAddress replica : replicas)
{
probe.output().out.println(replica.getHostAddress());
}
}
}
}

View File

@ -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

View File

@ -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,

View File

@ -393,6 +393,7 @@ public class Sjk extends AbstractCommand
List<Class<?>> 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;

View File

@ -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());
}

Some files were not shown because too many files have changed in this diff Show More